using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
namespace PowderCoating.Infrastructure.Services;
public partial class SeedDataService
{
///
/// Seeds inventory transaction history for a company, comprising four categories of
/// transactions: opening balance (Initial), three months of powder restocks (Purchase),
/// per-job powder consumption for every completed/ready/delivered job (JobUsage),
/// and one waste plus one manual adjustment record.
///
///
///
/// Idempotency: returns 0 immediately if any non-deleted inventory transactions already
/// exist for this company, preventing duplicate ledger entries on re-seed.
///
///
/// All transactions are accumulated in a single in-memory list and persisted in one
/// AddRangeAsync / SaveChangesAsync call for efficiency. The trade-off is that
/// BalanceAfter values on individual transactions are approximations based on
/// QuantityOnHand at seed time rather than a running sum — they are good enough
/// for demo display purposes but should not be trusted as exact ledger balances.
///
///
/// JobUsage quantities are stored as negative numbers (e.g. −5.5 lbs) following
/// the production convention where negative quantity = stock consumed.
///
///
/// The color-matching helper PickItem() attempts to link each completed job to an
/// inventory item whose color name starts with the same word as the job's first item's color
/// (e.g. "Matte Black" → key "matte"). If no match is found it falls back to round-robin
/// rotation so every completed job still gets a usage transaction.
///
///
/// The tenant company to seed inventory transactions for.
/// Total number of transaction records inserted, or 0 if already seeded or no inventory items exist.
private async Task SeedInventoryTransactionsAsync(Company company)
{
var existingCount = await _context.Set()
.IgnoreQueryFilters()
.CountAsync(t => t.CompanyId == company.Id && !t.IsDeleted);
if (existingCount > 0)
return 0;
// ── Load inventory items (powder coats) ───────────────────────────────
var items = await _context.Set()
.IgnoreQueryFilters()
.Where(i => i.CompanyId == company.Id && !i.IsDeleted && i.IsActive)
.ToListAsync();
if (items.Count == 0) return 0;
// Load completed/delivered jobs to generate usage transactions against
var completedJobs = await _context.Set()
.IgnoreQueryFilters()
.Where(j => j.CompanyId == company.Id && !j.IsDeleted
&& (j.JobStatus.StatusCode == "COMPLETED"
|| j.JobStatus.StatusCode == "READY_FOR_PICKUP"
|| j.JobStatus.StatusCode == "DELIVERED"))
.Include(j => j.JobStatus)
.OrderBy(j => j.Id)
.ToListAsync();
var now = DateTime.UtcNow;
var seeded = 0;
var txns = new List();
// ── Initial stock transactions (set opening balances) ─────────────────
// These reflect inventory on hand from day-1; dated 90 days ago
foreach (var item in items)
{
var openingQty = item.QuantityOnHand > 0 ? item.QuantityOnHand : 25m;
txns.Add(new InventoryTransaction
{
InventoryItemId = item.Id,
TransactionType = InventoryTransactionType.Initial,
Quantity = openingQty,
UnitCost = item.UnitCost,
TotalCost = Math.Round(openingQty * item.UnitCost, 2),
TransactionDate = now.AddDays(-90),
Reference = "Opening balance",
Notes = "Initial stock entry",
BalanceAfter = openingQty,
CompanyId = company.Id,
CreatedAt = now.AddDays(-90)
});
}
// ── Purchase transactions — 3 months of restocks ──────────────────────
// Simulate monthly powder purchases for top items
var powderItems = items.Take(8).ToList(); // focus on powder coat items
foreach (var (offset, qtyMult) in new (int daysAgo, decimal mult)[] {
(85, 1.2m), (55, 1.0m), (25, 0.9m) })
{
foreach (var item in powderItems.Take(4)) // 4 items per purchase cycle
{
var qty = Math.Round(25m * qtyMult, 0);
txns.Add(new InventoryTransaction
{
InventoryItemId = item.Id,
TransactionType = InventoryTransactionType.Purchase,
Quantity = qty,
UnitCost = item.UnitCost,
TotalCost = Math.Round(qty * item.UnitCost, 2),
TransactionDate = now.AddDays(-offset),
Reference = $"PO-{now.AddDays(-offset):yyMM}-{item.Id:D3}",
Notes = "Scheduled restock",
BalanceAfter = qty + (item.QuantityOnHand > 0 ? item.QuantityOnHand : 25m),
CompanyId = company.Id,
CreatedAt = now.AddDays(-offset)
});
}
}
// ── JobUsage transactions — powder consumed for completed jobs ─────────
// Each completed job consumes powder from one or two inventory items.
// Spread consumption across the past 3 months.
var colorMap = new Dictionary();
foreach (var item in items)
{
var key = (item.ColorName ?? item.Name).ToLowerInvariant().Split(' ')[0];
colorMap.TryAdd(key, item);
}
// Fallback: rotate through items if no color match.
// PickItem is a local function rather than a LINQ expression because it needs to
// mutate the outer itemIdx counter across multiple calls.
int itemIdx = 0;
InventoryItem PickItem(string? colorName)
{
if (!string.IsNullOrEmpty(colorName))
{
var key = colorName.ToLowerInvariant().Split(' ')[0];
if (colorMap.TryGetValue(key, out var matched) && matched != null)
return matched;
}
return items[itemIdx++ % items.Count];
}
foreach (var (job, idx) in completedJobs.Select((j, i) => (j, i)))
{
// Completed date spread: most within the last 60 days
var daysAgo = 10 + (idx % 55);
var usageDate = now.AddDays(-daysAgo);
// Pick a color-matched powder item (or rotate)
var firstItem = job.JobItems?.FirstOrDefault();
var inv = PickItem(firstItem?.ColorName);
// Powder used: 3–15 lbs depending on job size
var lbsUsed = Math.Round(3m + (idx % 5) * 2.5m, 1);
if (lbsUsed < 0.5m) lbsUsed = 0.5m;
txns.Add(new InventoryTransaction
{
InventoryItemId = inv.Id,
TransactionType = InventoryTransactionType.JobUsage,
Quantity = -lbsUsed, // negative = consumed
UnitCost = inv.UnitCost,
TotalCost = Math.Round(lbsUsed * inv.UnitCost, 2),
TransactionDate = usageDate,
Reference = job.JobNumber,
Notes = $"Powder used — {job.JobNumber}",
BalanceAfter = Math.Max(0, inv.QuantityOnHand - lbsUsed),
CompanyId = company.Id,
CreatedAt = usageDate
});
}
// ── Waste/adjustment transactions ─────────────────────────────────────
if (items.Count >= 2)
{
txns.Add(new InventoryTransaction
{
InventoryItemId = items[0].Id,
TransactionType = InventoryTransactionType.Waste,
Quantity = -1.5m,
UnitCost = items[0].UnitCost,
TotalCost = Math.Round(1.5m * items[0].UnitCost, 2),
TransactionDate = now.AddDays(-45),
Reference = "Waste",
Notes = "Contaminated powder — disposed",
BalanceAfter = Math.Max(0, items[0].QuantityOnHand - 1.5m),
CompanyId = company.Id,
CreatedAt = now.AddDays(-45)
});
txns.Add(new InventoryTransaction
{
InventoryItemId = items[1].Id,
TransactionType = InventoryTransactionType.Adjustment,
Quantity = 2.0m,
UnitCost = items[1].UnitCost,
TotalCost = Math.Round(2.0m * items[1].UnitCost, 2),
TransactionDate = now.AddDays(-30),
Reference = "Adjustment",
Notes = "Physical count correction",
BalanceAfter = items[1].QuantityOnHand + 2.0m,
CompanyId = company.Id,
CreatedAt = now.AddDays(-30)
});
}
await _context.Set().AddRangeAsync(txns);
await _context.SaveChangesAsync();
seeded = txns.Count;
return seeded;
}
}