Fix 4 post-review issues found in accounting module audit
- Drop orphan VendorCreditId1 column from VendorCreditApplications (was scaffolded by EF because WithMany() lacked inverse navigation name; fixed WithMany() → WithMany(vc => vc.Applications) in ApplicationDbContext) - Wire EarlyPaymentDiscount fields through full data path: added EarlyPaymentDiscountPercent/Days to CreateInvoiceDto, hidden inputs to Invoice Create view, and JS to populate from customer AJAX response - Add missing [HttpGet] attribute to TaxRatesController.Index - Document GenerateNow architecture exception with XML rationale Migration DropOrphanVendorCreditId1 applied. Build: 0 errors, 168 warnings. Unit tests: 200/200 passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,10 @@ public class CreateInvoiceDto
|
|||||||
public string? InternalNotes { get; set; }
|
public string? InternalNotes { get; set; }
|
||||||
public string? Terms { get; set; }
|
public string? Terms { get; set; }
|
||||||
public string? CustomerPO { get; set; }
|
public string? CustomerPO { get; set; }
|
||||||
|
/// <summary>Early-payment discount percentage parsed from the customer's payment terms (e.g., 2.0 for "2/10 Net 30"). Informational — does not auto-apply.</summary>
|
||||||
|
public decimal EarlyPaymentDiscountPercent { get; set; }
|
||||||
|
/// <summary>Number of days within which the early-payment discount applies (e.g., 10 for "2/10 Net 30").</summary>
|
||||||
|
public int EarlyPaymentDiscountDays { get; set; }
|
||||||
public List<CreateInvoiceItemDto> InvoiceItems { get; set; } = new();
|
public List<CreateInvoiceItemDto> InvoiceItems { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -667,7 +667,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
|||||||
.OnDelete(DeleteBehavior.NoAction);
|
.OnDelete(DeleteBehavior.NoAction);
|
||||||
modelBuilder.Entity<VendorCreditApplication>()
|
modelBuilder.Entity<VendorCreditApplication>()
|
||||||
.HasOne(vca => vca.VendorCredit)
|
.HasOne(vca => vca.VendorCredit)
|
||||||
.WithMany()
|
.WithMany(vc => vc.Applications)
|
||||||
.HasForeignKey(vca => vca.VendorCreditId)
|
.HasForeignKey(vca => vca.VendorCreditId)
|
||||||
.OnDelete(DeleteBehavior.NoAction);
|
.OnDelete(DeleteBehavior.NoAction);
|
||||||
|
|
||||||
|
|||||||
Generated
+10177
File diff suppressed because it is too large
Load Diff
+91
@@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DropOrphanVendorCreditId1 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_VendorCredits_VendorCreditId1",
|
||||||
|
table: "VendorCreditApplications");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_VendorCreditApplications_VendorCreditId1",
|
||||||
|
table: "VendorCreditApplications");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "VendorCreditId1",
|
||||||
|
table: "VendorCreditApplications");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 15, 25, 26, 831, DateTimeKind.Utc).AddTicks(199));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 15, 25, 26, 831, DateTimeKind.Utc).AddTicks(205));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 15, 25, 26, 831, DateTimeKind.Utc).AddTicks(206));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "VendorCreditId1",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 15, 1, 54, 769, DateTimeKind.Utc).AddTicks(6262));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 15, 1, 54, 769, DateTimeKind.Utc).AddTicks(6270));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 15, 1, 54, 769, DateTimeKind.Utc).AddTicks(6271));
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VendorCreditApplications_VendorCreditId1",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
column: "VendorCreditId1");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_VendorCredits_VendorCreditId1",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
column: "VendorCreditId1",
|
||||||
|
principalTable: "VendorCredits",
|
||||||
|
principalColumn: "Id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6293,7 +6293,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 5, 10, 15, 1, 54, 769, DateTimeKind.Utc).AddTicks(6262),
|
CreatedAt = new DateTime(2026, 5, 10, 15, 25, 26, 831, DateTimeKind.Utc).AddTicks(199),
|
||||||
Description = "Standard pricing for regular customers",
|
Description = "Standard pricing for regular customers",
|
||||||
DiscountPercent = 0m,
|
DiscountPercent = 0m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -6304,7 +6304,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 5, 10, 15, 1, 54, 769, DateTimeKind.Utc).AddTicks(6270),
|
CreatedAt = new DateTime(2026, 5, 10, 15, 25, 26, 831, DateTimeKind.Utc).AddTicks(205),
|
||||||
Description = "5% discount for preferred customers",
|
Description = "5% discount for preferred customers",
|
||||||
DiscountPercent = 5m,
|
DiscountPercent = 5m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -6315,7 +6315,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 3,
|
Id = 3,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 5, 10, 15, 1, 54, 769, DateTimeKind.Utc).AddTicks(6271),
|
CreatedAt = new DateTime(2026, 5, 10, 15, 25, 26, 831, DateTimeKind.Utc).AddTicks(206),
|
||||||
Description = "10% discount for premium customers",
|
Description = "10% discount for premium customers",
|
||||||
DiscountPercent = 10m,
|
DiscountPercent = 10m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -8171,17 +8171,12 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
b.Property<int>("VendorCreditId")
|
b.Property<int>("VendorCreditId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int?>("VendorCreditId1")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("BillId");
|
b.HasIndex("BillId");
|
||||||
|
|
||||||
b.HasIndex("VendorCreditId");
|
b.HasIndex("VendorCreditId");
|
||||||
|
|
||||||
b.HasIndex("VendorCreditId1");
|
|
||||||
|
|
||||||
b.ToTable("VendorCreditApplications");
|
b.ToTable("VendorCreditApplications");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -9883,15 +9878,11 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("PowderCoating.Core.Entities.VendorCredit", "VendorCredit")
|
b.HasOne("PowderCoating.Core.Entities.VendorCredit", "VendorCredit")
|
||||||
.WithMany()
|
.WithMany("Applications")
|
||||||
.HasForeignKey("VendorCreditId")
|
.HasForeignKey("VendorCreditId")
|
||||||
.OnDelete(DeleteBehavior.NoAction)
|
.OnDelete(DeleteBehavior.NoAction)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("PowderCoating.Core.Entities.VendorCredit", null)
|
|
||||||
.WithMany("Applications")
|
|
||||||
.HasForeignKey("VendorCreditId1");
|
|
||||||
|
|
||||||
b.Navigation("Bill");
|
b.Navigation("Bill");
|
||||||
|
|
||||||
b.Navigation("VendorCredit");
|
b.Navigation("VendorCredit");
|
||||||
|
|||||||
@@ -174,6 +174,15 @@ public class RecurringTemplatesController : Controller
|
|||||||
/// Forces immediate generation of the next occurrence and advances NextFireDate,
|
/// Forces immediate generation of the next occurrence and advances NextFireDate,
|
||||||
/// regardless of the scheduled date. Useful for testing a new template or catching up
|
/// regardless of the scheduled date. Useful for testing a new template or catching up
|
||||||
/// after a configuration change.
|
/// after a configuration change.
|
||||||
|
/// <para>
|
||||||
|
/// INTENTIONAL EXCEPTION to the no-DbContext-in-controllers rule: this action must
|
||||||
|
/// execute the same multi-step entity creation that <see cref="RecurringTransactionService"/>
|
||||||
|
/// does in a background scope. Creating an inner DI scope here avoids duplicating that
|
||||||
|
/// service's interface (which would require exposing a public synchronous Fire method on a
|
||||||
|
/// singleton BackgroundService, which is unsafe). The scope is disposed after the action
|
||||||
|
/// completes, so no DbContext leak occurs. This pattern mirrors how BackgroundService
|
||||||
|
/// itself resolves scoped services.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
@@ -184,6 +193,8 @@ public class RecurringTemplatesController : Controller
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Intentional: create a fresh DI scope so the inner DbContext is isolated from the
|
||||||
|
// request's IUnitOfWork context. See summary above for rationale.
|
||||||
using var scope = HttpContext.RequestServices.CreateScope();
|
using var scope = HttpContext.RequestServices.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<PowderCoating.Infrastructure.Data.ApplicationDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<PowderCoating.Infrastructure.Data.ApplicationDbContext>();
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class TaxRatesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Lists all tax rates for the current company.</summary>
|
/// <summary>Lists all tax rates for the current company.</summary>
|
||||||
|
[HttpGet]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var rates = await _unitOfWork.TaxRates.GetAllAsync();
|
var rates = await _unitOfWork.TaxRates.GetAllAsync();
|
||||||
|
|||||||
@@ -59,6 +59,8 @@
|
|||||||
<input type="hidden" asp-for="PreparedById" />
|
<input type="hidden" asp-for="PreparedById" />
|
||||||
<input type="hidden" asp-for="JobId" />
|
<input type="hidden" asp-for="JobId" />
|
||||||
<input type="hidden" asp-for="CustomerId" id="hiddenCustomerId" />
|
<input type="hidden" asp-for="CustomerId" id="hiddenCustomerId" />
|
||||||
|
<input type="hidden" asp-for="EarlyPaymentDiscountPercent" id="EarlyPaymentDiscountPercent" />
|
||||||
|
<input type="hidden" asp-for="EarlyPaymentDiscountDays" id="EarlyPaymentDiscountDays" />
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- LEFT: Main form -->
|
<!-- LEFT: Main form -->
|
||||||
@@ -473,8 +475,10 @@
|
|||||||
// Trigger due date recalculation (invoice-due-date.js listens to 'change')
|
// Trigger due date recalculation (invoice-due-date.js listens to 'change')
|
||||||
termsSelect.dispatchEvent(new Event('change'));
|
termsSelect.dispatchEvent(new Event('change'));
|
||||||
}
|
}
|
||||||
// Show/hide early payment discount notice
|
// Show/hide early payment discount notice and persist values
|
||||||
const discountEl = document.getElementById('earlyPaymentDiscountNotice');
|
const discountEl = document.getElementById('earlyPaymentDiscountNotice');
|
||||||
|
const discountPctField = document.getElementById('EarlyPaymentDiscountPercent');
|
||||||
|
const discountDaysField = document.getElementById('EarlyPaymentDiscountDays');
|
||||||
if (discountEl) {
|
if (discountEl) {
|
||||||
if (data.earlyPaymentDiscountPercent > 0) {
|
if (data.earlyPaymentDiscountPercent > 0) {
|
||||||
discountEl.textContent = `${data.earlyPaymentDiscountPercent}% discount if paid within ${data.earlyPaymentDiscountDays} days`;
|
discountEl.textContent = `${data.earlyPaymentDiscountPercent}% discount if paid within ${data.earlyPaymentDiscountDays} days`;
|
||||||
@@ -483,6 +487,8 @@
|
|||||||
discountEl.classList.add('d-none');
|
discountEl.classList.add('d-none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (discountPctField) discountPctField.value = data.earlyPaymentDiscountPercent ?? 0;
|
||||||
|
if (discountDaysField) discountDaysField.value = data.earlyPaymentDiscountDays ?? 0;
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
// Fetch tax rate for the selected customer
|
// Fetch tax rate for the selected customer
|
||||||
|
|||||||
Reference in New Issue
Block a user