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 {