sales & grn fixers

This commit is contained in:
2026-10-04 15:31:59 +05:30
parent cea433e320
commit 3d3dbbbb59
52 changed files with 20761 additions and 482 deletions
@@ -0,0 +1,42 @@
using ERPCore.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ERPCore.Infra.Persistence.Configurations;
/// <summary>
/// Configures the singleton GRN-configuration row. The check constraint is what makes
/// "singleton" a database guarantee rather than a convention (mirrors SalesConfig).
/// </summary>
public sealed class GrnConfigConfiguration : IEntityTypeConfiguration<GrnConfig>
{
public void Configure(EntityTypeBuilder<GrnConfig> builder)
{
builder.ToTable("grn_config", t =>
t.HasCheckConstraint("ck_grn_config_singleton", $"\"ConfigId\" = {GrnConfig.SingletonId}"));
builder.HasKey(c => c.ConfigId);
// The id is fixed, never generated — there is exactly one row, seeded by DataSeeder.
builder.Property(c => c.ConfigId).ValueGeneratedNever();
// Sentinel = the CLR initializer (true), so an explicit false is still written on insert
// instead of being swallowed by the database default.
builder.Property(c => c.AllowWarehouseSelection).IsRequired().HasDefaultValue(true).HasSentinel(true);
builder.Property(c => c.AllowDiscounts).IsRequired().HasDefaultValue(true).HasSentinel(true);
builder.Property(c => c.AllowVat).IsRequired().HasDefaultValue(true).HasSentinel(true);
builder.HasOne(c => c.DefaultWarehouse)
.WithMany()
.HasForeignKey(c => c.DefaultWarehouseId)
.OnDelete(DeleteBehavior.SetNull);
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
builder.Property(c => c.RowVersion).IsRowVersion();
builder.HasOne(c => c.UpdatedByUser)
.WithMany()
.HasForeignKey(c => c.UpdatedBy)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -47,6 +47,7 @@ public static class DataSeeder
dirty |= await SeedProductConfigAsync(db, ct);
dirty |= await SeedBranchConfigAsync(db, ct);
dirty |= await SeedSalesConfigAsync(db, ct);
dirty |= await SeedGrnConfigAsync(db, ct);
dirty |= await SeedNotificationConfigAsync(db, ct);
dirty |= await SeedSalesMastersAsync(db, ct);
dirty |= await SeedSalesStockAsync(db, ct);
@@ -144,6 +145,16 @@ public static class DataSeeder
return true;
}
/// <summary>Seeds the GRN configuration singleton — warehouse selection on, no default warehouse,
/// so New GRN keeps its warehouse picker until an admin opts in from Settings → Configuration → GRN Configuration.</summary>
private static async Task<bool> SeedGrnConfigAsync(ErpDbContext db, CancellationToken ct)
{
if (await db.GrnConfig.AnyAsync(c => c.ConfigId == GrnConfig.SingletonId, ct)) return false;
db.GrnConfig.Add(new GrnConfig { ConfigId = GrnConfig.SingletonId });
return true;
}
private static async Task<bool> SeedNotificationConfigAsync(ErpDbContext db, CancellationToken ct)
{
if (await db.NotificationConfig.AnyAsync(c => c.NotificationConfigId == NotificationConfig.SingletonId, ct)) return false;
@@ -46,6 +46,7 @@ public class ErpDbContext : DbContext
/// <summary>Singleton row gating whether this deployment uses branches at all.</summary>
public DbSet<BranchConfig> BranchConfig => Set<BranchConfig>();
public DbSet<SalesConfig> SalesConfig => Set<SalesConfig>();
public DbSet<GrnConfig> GrnConfig => Set<GrnConfig>();
/// <summary>Singleton row for notification delivery preferences.</summary>
public DbSet<NotificationConfig> NotificationConfig => Set<NotificationConfig>();
public DbSet<NotificationConfigModule> NotificationConfigModules => Set<NotificationConfigModule>();