using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PowderCoating.Core.Entities; using PowderCoating.Core.Interfaces; using PowderCoating.Shared.Constants; namespace PowderCoating.Web.Controllers; /// /// Manages named tax rates used to pre-fill the tax percent field on invoices when a taxable /// customer is selected. Only one rate may be marked as default at a time; that default is /// auto-applied via the GetTaxRateForCustomer AJAX endpoint on the Invoice Create form. /// [Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)] public class TaxRatesController : Controller { private readonly IUnitOfWork _unitOfWork; private readonly ITenantContext _tenantContext; private readonly ILogger _logger; public TaxRatesController( IUnitOfWork unitOfWork, ITenantContext tenantContext, ILogger logger) { _unitOfWork = unitOfWork; _tenantContext = tenantContext; _logger = logger; } /// Lists all tax rates for the current company. [HttpGet] public async Task Index() { var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; var rates = await _unitOfWork.TaxRates.FindAsync(r => r.CompanyId == companyId); return View(rates.OrderBy(r => r.Name).ToList()); } [HttpGet] public IActionResult Create() => View(new TaxRate()); /// Creates a new tax rate. Enforces that only one rate is the default. [HttpPost] [ValidateAntiForgeryToken] public async Task Create(TaxRate model) { if (!ModelState.IsValid) return View(model); if (model.IsDefault) await ClearOtherDefaultsAsync(0); await _unitOfWork.TaxRates.AddAsync(model); await _unitOfWork.CompleteAsync(); TempData["Success"] = $"Tax rate \"{model.Name}\" created."; return RedirectToAction(nameof(Index)); } [HttpGet] public async Task Edit(int id) { var rate = await _unitOfWork.TaxRates.GetByIdAsync(id); if (rate == null) return NotFound(); return View(rate); } /// Updates an existing tax rate. Clears default flag on other rates when IsDefault is set. [HttpPost] [ValidateAntiForgeryToken] public async Task Edit(int id, TaxRate model) { if (id != model.Id) return BadRequest(); if (!ModelState.IsValid) return View(model); var rate = await _unitOfWork.TaxRates.GetByIdAsync(id); if (rate == null) return NotFound(); if (model.IsDefault && !rate.IsDefault) await ClearOtherDefaultsAsync(id); rate.Name = model.Name; rate.Rate = model.Rate; rate.State = model.State; rate.Description = model.Description; rate.IsDefault = model.IsDefault; rate.IsActive = model.IsActive; await _unitOfWork.CompleteAsync(); TempData["Success"] = $"Tax rate \"{rate.Name}\" updated."; return RedirectToAction(nameof(Index)); } /// Toggles IsActive without a full page reload. [HttpPost] [ValidateAntiForgeryToken] public async Task ToggleActive(int id) { var rate = await _unitOfWork.TaxRates.GetByIdAsync(id); if (rate == null) return NotFound(); rate.IsActive = !rate.IsActive; if (!rate.IsActive) rate.IsDefault = false; await _unitOfWork.CompleteAsync(); return RedirectToAction(nameof(Index)); } /// Soft-deletes a tax rate. Blocked when the rate is currently the default. [HttpPost] [ValidateAntiForgeryToken] public async Task Delete(int id) { var rate = await _unitOfWork.TaxRates.GetByIdAsync(id); if (rate == null) return NotFound(); if (rate.IsDefault) { TempData["Error"] = "Cannot delete the default tax rate. Set another rate as default first."; return RedirectToAction(nameof(Index)); } await _unitOfWork.TaxRates.SoftDeleteAsync(id); await _unitOfWork.CompleteAsync(); TempData["Success"] = $"Tax rate \"{rate.Name}\" deleted."; return RedirectToAction(nameof(Index)); } /// /// Clears IsDefault on all rates except the one with . /// Called before saving a newly-designated default to enforce the single-default invariant. /// private async Task ClearOtherDefaultsAsync(int exceptId) { var others = await _unitOfWork.TaxRates.FindAsync(r => r.IsDefault && r.Id != exceptId); foreach (var r in others) r.IsDefault = false; } }