diff --git a/src/PowderCoating.Web/Controllers/JobsController.cs b/src/PowderCoating.Web/Controllers/JobsController.cs
index 2fd3a07..2ba68e1 100644
--- a/src/PowderCoating.Web/Controllers/JobsController.cs
+++ b/src/PowderCoating.Web/Controllers/JobsController.cs
@@ -422,7 +422,7 @@ public class JobsController : Controller
// Populate Edit Items wizard data (inline modal on Details page)
var wizardCosts = await _pricingService.GetOperatingCostsAsync(job.CompanyId);
await PopulateJobItemDropDownsAsync(job.CompanyId, wizardCosts?.OvenOperatingCostPerHour ?? 45m);
- ViewBag.WizardTaxPercent = wizardCosts?.TaxPercent ?? 0m;
+ ViewBag.WizardTaxPercent = await GetEffectiveTaxPercentAsync(job.CustomerId, wizardCosts?.TaxPercent ?? 0m);
// Display the pricing snapshot stored when items were last saved.
// Never recalculate on load — operating cost changes must not retroactively alter existing jobs.
@@ -1130,7 +1130,7 @@ public class JobsController : Controller
}
var totals = await _pricingService.CalculateQuoteTotalsAsync(
dto.JobItems, companyId, dto.CustomerId,
- createCosts?.TaxPercent ?? 0m,
+ await GetEffectiveTaxPercentAsync(dto.CustomerId, createCosts?.TaxPercent ?? 0m),
dto.DiscountType, dto.DiscountValue, dto.IsRushJob, createOvenRate, job.OvenBatches, job.OvenCycleMinutes);
job.FinalPrice = totals.Total;
@@ -1598,7 +1598,7 @@ public class JobsController : Controller
}
var totals = await _pricingService.CalculateQuoteTotalsAsync(
dto.JobItems, companyId, dto.CustomerId,
- editCosts?.TaxPercent ?? 0m,
+ await GetEffectiveTaxPercentAsync(dto.CustomerId, editCosts?.TaxPercent ?? 0m),
dto.DiscountType, dto.DiscountValue, dto.IsRushJob, editOvenRate, job.OvenBatches, job.OvenCycleMinutes);
job.FinalPrice = totals.Total;
job.OvenBatchCost = totals.OvenBatchCost;
@@ -2930,7 +2930,7 @@ public class JobsController : Controller
JobId = job.Id,
JobNumber = job.JobNumber,
CustomerId = job.CustomerId,
- TaxPercent = costs?.TaxPercent ?? 0m,
+ TaxPercent = await GetEffectiveTaxPercentAsync(job.CustomerId, costs?.TaxPercent ?? 0m),
OvenCostId = job.OvenCostId,
OvenBatches = job.OvenBatches > 0 ? job.OvenBatches : 1,
OvenCycleMinutes = job.OvenCycleMinutes,
@@ -2967,7 +2967,7 @@ public class JobsController : Controller
{
ModelState.AddModelError("", "Please add at least one job item.");
var costs = await _pricingService.GetOperatingCostsAsync(currentUser.CompanyId);
- model.TaxPercent = costs?.TaxPercent ?? 0m;
+ model.TaxPercent = await GetEffectiveTaxPercentAsync(job.CustomerId, costs?.TaxPercent ?? 0m);
await PopulateJobItemDropDownsAsync(currentUser.CompanyId, costs?.OvenOperatingCostPerHour ?? 45m);
ViewBag.ComplexitySimplePercent = costs?.ComplexitySimplePercent ?? 0m;
ViewBag.ComplexityModeratePercent = costs?.ComplexityModeratePercent ?? 5m;
@@ -3020,9 +3020,11 @@ public class JobsController : Controller
if (oven != null && oven.CompanyId == currentUser.CompanyId)
ovenRateOverride = oven.CostPerHour;
}
+ var updateCosts = await _pricingService.GetOperatingCostsAsync(currentUser.CompanyId);
var totals = await _pricingService.CalculateQuoteTotalsAsync(
model.JobItems, currentUser.CompanyId, job.CustomerId,
- model.TaxPercent, job.DiscountType.ToString(), job.DiscountValue, job.IsRushJob,
+ await GetEffectiveTaxPercentAsync(job.CustomerId, updateCosts?.TaxPercent ?? 0m),
+ job.DiscountType.ToString(), job.DiscountValue, job.IsRushJob,
ovenRateOverride, job.OvenBatches, job.OvenCycleMinutes);
job.FinalPrice = totals.Total;
@@ -3043,7 +3045,7 @@ public class JobsController : Controller
_logger.LogError(ex, "Error updating items for job {JobId}", job.Id);
TempData["Error"] = "An error occurred while saving job items.";
var costs = await _pricingService.GetOperatingCostsAsync(currentUser.CompanyId);
- model.TaxPercent = costs?.TaxPercent ?? 0m;
+ model.TaxPercent = await GetEffectiveTaxPercentAsync(job.CustomerId, costs?.TaxPercent ?? 0m);
await PopulateJobItemDropDownsAsync(currentUser.CompanyId, costs?.OvenOperatingCostPerHour ?? 45m);
return View("EditItems", model);
}
@@ -3110,7 +3112,7 @@ public class JobsController : Controller
}
var totals = await _pricingService.CalculateQuoteTotalsAsync(
remainingDtos, currentUser.CompanyId, job.CustomerId,
- costs?.TaxPercent ?? 0m,
+ await GetEffectiveTaxPercentAsync(job.CustomerId, costs?.TaxPercent ?? 0m),
job.DiscountType.ToString(), job.DiscountValue, job.IsRushJob,
deleteOvenRate, job.OvenBatches, job.OvenCycleMinutes);
job.FinalPrice = totals.Total;
@@ -3239,6 +3241,21 @@ public class JobsController : Controller
/// Converts a into the DTO used for both display and JSON snapshot storage.
/// All save paths (Create, Edit, UpdateItems, DeleteJobItem) call this so the snapshot is always consistent.
///
+ ///
+ /// Returns the effective tax rate for a job, respecting customer tax-exempt status.
+ /// Always call this instead of using costs.TaxPercent directly so tax-exempt customers
+ /// are never charged tax when a job is saved or recalculated.
+ ///
+ private async Task GetEffectiveTaxPercentAsync(int? customerId, decimal companyDefaultRate)
+ {
+ if (customerId is > 0)
+ {
+ var customer = await _unitOfWork.Customers.GetByIdAsync(customerId.Value);
+ if (customer?.IsTaxExempt == true) return 0m;
+ }
+ return companyDefaultRate;
+ }
+
private static QuotePricingBreakdownDto BuildPricingSnapshotDto(QuotePricingResult pr) =>
new QuotePricingBreakdownDto
{