Files
PowderCoatingLogix/src/PowderCoating.Infrastructure/Services/SeedDataService.AiPredictions.cs
T
spouliot c0e4a66126 Phase 4: Past appointments + AI prediction demo data
- Appointments: add ~25 past appointments (last 90 days) with Completed,
  Cancelled, No Show, and Rescheduled statuses; completed records carry
  ActualStartTime/ActualEndTime with realistic variance; cancel/no-show
  notes explain why; customer label falls back to ContactFirst/LastName
  for residential customers
- Fix future appointment title for residential customers (was always using
  CompanyName which is null for individuals)
- New SeedDataService.AiPredictions.cs: seeds 8 AiItemPrediction records
  (varied complexity/confidence/tags/reasoning) and attaches them to the
  first 8 eligible QuoteItems, marking those items IsAiItem=true; 3 of 8
  have UserOverrodeEstimate=true for AI Accuracy report demo
- SeedDataService.cs: wire SeedAiPredictionsAsync after Invoices
- Remove.cs: collect QuoteItem.AiPredictionId FKs before deleting items,
  then delete orphaned AiItemPrediction records after quotes are removed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 22:40:12 -04:00

141 lines
7.9 KiB
C#

using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
namespace PowderCoating.Infrastructure.Services;
public partial class SeedDataService
{
/// <summary>
/// Seeds 8 <see cref="AiItemPrediction"/> demo records and attaches them to the first
/// 8 eligible <see cref="QuoteItem"/> records, marking those items as AI-analysed.
/// </summary>
/// <remarks>
/// <para>
/// "Eligible" means <c>SurfaceAreaSqFt &gt; 0</c>, not a labor item, and not already
/// linked to a prediction. This ensures the seeder is safe to run even if a partial seed
/// left some items pre-linked.
/// </para>
/// <para>
/// Each prediction record captures a realistic AI analysis: predicted surface area,
/// estimated minutes, complexity tier, unit price, confidence level, reasoning text, and
/// comma-separated AI tags. Three of the eight items have <c>UserOverrodeEstimate = true</c>
/// to demonstrate the override-tracking feature on the AI Accuracy report.
/// </para>
/// <para>
/// Items are updated in-place with <c>IsAiItem = true</c> and the FK
/// <c>AiPredictionId</c> pointing to the new prediction. <c>SaveChangesAsync</c> is called
/// per item so any single FK conflict (unlikely in a fresh seed) does not abort the others.
/// </para>
/// Idempotency: returns 0 immediately if any AiItemPrediction records already exist for
/// the company.
/// </remarks>
/// <param name="company">The tenant company to seed predictions for.</param>
/// <returns>Number of prediction records inserted, or 0 if already seeded.</returns>
private async Task<int> SeedAiPredictionsAsync(Company company)
{
var existingCount = await _context.Set<AiItemPrediction>()
.IgnoreQueryFilters()
.CountAsync(p => p.CompanyId == company.Id && !p.IsDeleted);
if (existingCount > 0)
return 0;
// Grab the first 8 eligible quote items ordered by id for determinism
var quoteItems = await _context.Set<QuoteItem>()
.IgnoreQueryFilters()
.Include(qi => qi.Quote)
.Where(qi => qi.CompanyId == company.Id
&& !qi.IsDeleted
&& qi.SurfaceAreaSqFt > 0
&& !qi.IsLaborItem
&& qi.AiPredictionId == null)
.OrderBy(qi => qi.Id)
.Take(8)
.ToListAsync();
if (quoteItems.Count == 0)
return 0;
// Per-slot prediction specs — deterministic, varied across complexity/confidence tiers.
// PredictedSqFt is intentionally close but NOT identical to the actual item SqFt so the
// AI Accuracy report shows realistic prediction deltas.
var specs = new[]
{
// slot 0 — complex automotive, AI nailed it
( sqft: 13.5m, mins: 88, complexity: "Complex", confidence: "High",
price: 125.00m, tags: "automotive,tubular,custom", rounds: 1, overrode: false,
reasoning: "Detected a tubular motorcycle frame with multiple weld joints. High complexity due to intricate geometry and masking requirements around bearing surfaces. Confidence high — similar frames appear frequently in training data." ),
// slot 1 — wheel set, quick read, accepted as-is
( sqft: 11.2m, mins: 42, complexity: "Simple", confidence: "High",
price: 98.00m, tags: "automotive,wheels,aluminum", rounds: 1, overrode: false,
reasoning: "Four aluminum wheels, uniform shape, minimal masking needed. Straightforward batch candidate for the main oven. Estimated surface area based on standard 18\" wheel profile." ),
// slot 2 — bumper job, user bumped sqft slightly
( sqft: 14.8m, mins: 62, complexity: "Moderate", confidence: "Medium",
price: 135.00m, tags: "automotive,bumper,off-road", rounds: 2, overrode: true,
reasoning: "Steel off-road bumper and rock sliders. Moderate complexity — flat stock with mounting tabs. Second image round requested for accurate rock slider dimensions. User adjusted surface area slightly after physical measurement." ),
// slot 3 — large gate, low confidence, user corrected price
( sqft: 34.2m, mins: 195, complexity: "Complex", confidence: "Low",
price: 310.00m, tags: "architectural,gate,ornamental", rounds: 2, overrode: true,
reasoning: "Wrought iron entry gate with decorative scrollwork. Low confidence due to depth ambiguity in photos — scrollwork surface area is difficult to estimate from images alone. Recommend physical measurement before finalising price. User overrode unit price after measuring on-site." ),
// slot 4 — patio furniture, solid read
( sqft: 22.8m, mins: 52, complexity: "Moderate", confidence: "High",
price: 195.00m, tags: "furniture,outdoor,patio", rounds: 1, overrode: false,
reasoning: "Six-piece patio furniture set: four chairs, one table, one side table. Powder-coated tubular steel, standard outdoor finish. Good photo coverage — confidence high. Recommend Textured Beige or Satin Bronze for exterior durability." ),
// slot 5 — handrail, accepted price
( sqft: 39.0m, mins: 118, complexity: "Moderate", confidence: "High",
price: 342.00m, tags: "architectural,handrail,railing", rounds: 1, overrode: false,
reasoning: "40-foot steel handrail system, square tube construction. Consistent profile makes area calculation straightforward. Standard Gloss Black most common finish for this application — confirmed with customer." ),
// slot 6 — brake calipers, small & simple
( sqft: 3.8m, mins: 30, complexity: "Simple", confidence: "High",
price: 65.00m, tags: "automotive,brake,caliper", rounds: 1, overrode: false,
reasoning: "Set of four brake calipers, cast iron with machined mating surfaces. Masking required on piston bores and bleed nipples. Candy Red most requested finish. High confidence — calipers are a common item with well-established pricing." ),
// slot 7 — bicycle frame, two-round conversation
( sqft: 6.1m, mins: 65, complexity: "Moderate", confidence: "Medium",
price: 82.00m, tags: "recreational,bicycle,frame", rounds: 2, overrode: true,
reasoning: "Road bicycle frame, aluminium alloy. Second image round needed to assess cable routing channels and dropout geometry. Moderate complexity due to small-radius bends. User adjusted surface area after AI initially underestimated top-tube length." )
};
var seeded = 0;
for (int i = 0; i < quoteItems.Count && i < specs.Length; i++)
{
var item = quoteItems[i];
var s = specs[i];
var prediction = new AiItemPrediction
{
PredictedSurfaceAreaSqFt = s.sqft,
PredictedMinutes = s.mins,
PredictedComplexity = s.complexity,
PredictedUnitPrice = s.price,
Confidence = s.confidence,
Reasoning = s.reasoning,
AiTags = s.tags,
ConversationRounds = s.rounds,
UserOverrodeEstimate = s.overrode,
CompanyId = company.Id,
CreatedAt = item.CreatedAt
};
await _context.Set<AiItemPrediction>().AddAsync(prediction);
await _context.SaveChangesAsync();
seeded++;
// Mark the quote item as AI-analysed and link the prediction
item.IsAiItem = true;
item.AiPredictionId = prediction.Id;
item.AiTags = s.tags;
await _context.SaveChangesAsync();
}
return seeded;
}
}