Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,324 @@
namespace PowderCoating.Application.DTOs.AI;
// ── Shared ────────────────────────────────────────────────────────────────────
/// <summary>Lightweight account summary passed to AI for account matching.</summary>
public class AccountSummary
{
public int Id { get; set; }
public string AccountNumber { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string AccountType { get; set; } = string.Empty; // "Expense", "CostOfGoods", "Asset"
public string? AccountSubType { get; set; }
}
// ── Feature 1: Receipt / Bill Scanning ───────────────────────────────────────
public class ScannedLineItem
{
public string Description { get; set; } = string.Empty;
public decimal Amount { get; set; }
public int? SuggestedAccountId { get; set; }
public string? SuggestedAccountName { get; set; }
}
public class ReceiptScanResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public string? VendorName { get; set; }
public string? Date { get; set; } // ISO 8601 date string
public decimal? Total { get; set; }
public string? InvoiceNumber { get; set; }
public List<ScannedLineItem> LineItems { get; set; } = new();
}
/// <summary>Internal JSON schema that Claude returns for receipt scans.</summary>
public class ClaudeReceiptResponse
{
public string? VendorName { get; set; }
public string? Date { get; set; }
public decimal? Total { get; set; }
public string? InvoiceNumber { get; set; }
public List<ClaudeReceiptLineItem> LineItems { get; set; } = new();
}
public class ClaudeReceiptLineItem
{
public string Description { get; set; } = string.Empty;
public decimal Amount { get; set; }
public int? SuggestedAccountId { get; set; }
public string? SuggestedAccountName { get; set; }
}
// ── Feature 2: AR Follow-up Email Drafts ─────────────────────────────────────
public class OverdueInvoice
{
public string InvoiceNumber { get; set; } = string.Empty;
public decimal Amount { get; set; }
public int DaysOverdue { get; set; }
}
public class ArFollowUpRequest
{
public string CustomerName { get; set; } = string.Empty;
public string CompanyName { get; set; } = string.Empty;
public decimal AmountOwed { get; set; }
public int DaysOverdue { get; set; }
public List<OverdueInvoice> Invoices { get; set; } = new();
}
public class ArFollowUpResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public string Subject { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
}
/// <summary>Internal JSON schema that Claude returns for AR email drafts.</summary>
public class ClaudeArEmailResponse
{
public string Subject { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
}
// ── Feature 3: Smart Account Categorization ──────────────────────────────────
public class AccountSuggestionRequest
{
public string? VendorName { get; set; }
public string? Description { get; set; }
public decimal Amount { get; set; }
public List<AccountSummary> AvailableAccounts { get; set; } = new();
}
public class AccountSuggestion
{
public int AccountId { get; set; }
public string AccountName { get; set; } = string.Empty;
public double Confidence { get; set; }
public string? Reasoning { get; set; }
}
public class AccountSuggestionResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public int? SuggestedAccountId { get; set; }
public string? SuggestedAccountName { get; set; }
public string? Reasoning { get; set; }
public List<AccountSuggestion> Alternatives { get; set; } = new();
}
/// <summary>Internal JSON schema that Claude returns for account suggestions.</summary>
public class ClaudeAccountSuggestionResponse
{
public int? SuggestedAccountId { get; set; }
public string? SuggestedAccountName { get; set; }
public string? Reasoning { get; set; }
public List<ClaudeAccountAlternative> Alternatives { get; set; } = new();
}
public class ClaudeAccountAlternative
{
public int AccountId { get; set; }
public string AccountName { get; set; } = string.Empty;
public double Confidence { get; set; }
public string? Reasoning { get; set; }
}
// ── Feature 4: Plain-English Financial Summary ────────────────────────────────
public class ExpenseByCategory
{
public string Category { get; set; } = string.Empty;
public decimal Amount { get; set; }
}
public class FinancialSummaryRequest
{
public string CompanyName { get; set; } = string.Empty;
public string Period { get; set; } = string.Empty; // e.g. "Last 6 months"
public decimal TotalRevenue { get; set; }
public decimal TotalExpenses { get; set; }
public decimal NetIncome { get; set; }
public decimal PriorMonthRevenue { get; set; }
public decimal PriorMonthExpenses { get; set; }
public decimal TotalArOutstanding { get; set; }
public decimal ArOverdue30Days { get; set; }
public int OverdueInvoiceCount { get; set; }
public List<ExpenseByCategory> ExpensesByCategory { get; set; } = new();
}
public class FinancialSummaryResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
/// <summary>Markdown bullet lines (plain English, no jargon).</summary>
public List<string> Bullets { get; set; } = new();
/// <summary>"positive", "neutral", or "concerning".</summary>
public string Sentiment { get; set; } = "neutral";
}
/// <summary>Internal JSON schema that Claude returns for financial summaries.</summary>
public class ClaudeFinancialSummaryResponse
{
public List<string> Bullets { get; set; } = new();
public string Sentiment { get; set; } = "neutral";
}
// ── Feature 5: Cash Flow Forecast ─────────────────────────────────────────────
public class CashFlowArItem
{
public string CustomerName { get; set; } = string.Empty;
public string InvoiceNumber { get; set; } = string.Empty;
public decimal BalanceDue { get; set; }
public string? DueDateIso { get; set; }
public int DaysOverdue { get; set; }
public int AvgDaysToPay { get; set; }
}
public class CashFlowApItem
{
public string VendorName { get; set; } = string.Empty;
public string BillNumber { get; set; } = string.Empty;
public decimal BalanceDue { get; set; }
public string? DueDateIso { get; set; }
}
public class CashFlowJobItem
{
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public decimal EstimatedValue { get; set; }
}
public class CashFlowForecastRequest
{
public string CompanyName { get; set; } = string.Empty;
public string AsOfDate { get; set; } = string.Empty;
public List<CashFlowArItem> OpenInvoices { get; set; } = new();
public List<CashFlowApItem> OpenBills { get; set; } = new();
public List<CashFlowJobItem> ActiveJobs { get; set; } = new();
}
public class CashFlowPeriod
{
public decimal ExpectedInflows { get; set; }
public decimal ExpectedOutflows { get; set; }
public decimal NetCashFlow { get; set; }
public List<string> KeyItems { get; set; } = new();
}
public class CashFlowForecastResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public CashFlowPeriod Next30Days { get; set; } = new();
public CashFlowPeriod Next60Days { get; set; } = new();
public CashFlowPeriod Next90Days { get; set; } = new();
public List<string> Insights { get; set; } = new();
/// <summary>"strong", "moderate", "tight", or "concerning"</summary>
public string Outlook { get; set; } = "moderate";
}
/// <summary>Internal JSON schema that Claude returns for cash flow forecasts.</summary>
public class ClaudeCashFlowResponse
{
public ClaudeCashFlowPeriod Next30Days { get; set; } = new();
public ClaudeCashFlowPeriod Next60Days { get; set; } = new();
public ClaudeCashFlowPeriod Next90Days { get; set; } = new();
public List<string> Insights { get; set; } = new();
public string Outlook { get; set; } = "moderate";
}
public class ClaudeCashFlowPeriod
{
public decimal ExpectedInflows { get; set; }
public decimal ExpectedOutflows { get; set; }
public decimal NetCashFlow { get; set; }
public List<string> KeyItems { get; set; } = new();
}
// ── Feature 6: Anomaly / Duplicate Detection ──────────────────────────────────
public class AnomalyBillSummary
{
public int Id { get; set; }
public string BillNumber { get; set; } = string.Empty;
public string VendorName { get; set; } = string.Empty;
public decimal Total { get; set; }
public string BillDateIso { get; set; } = string.Empty;
public string? VendorInvoiceNumber { get; set; }
}
public class AnomalyVendorHistory
{
public string VendorName { get; set; } = string.Empty;
public decimal AverageInvoiceAmount { get; set; }
public decimal AverageMonthlySpend { get; set; }
public int InvoiceCount { get; set; }
}
public class AnomalyAccountTrend
{
public string AccountName { get; set; } = string.Empty;
public decimal ThisMonthAmount { get; set; }
public decimal LastMonthAmount { get; set; }
public decimal AverageMonthlyAmount { get; set; }
}
public class AnomalyDetectionRequest
{
public string CompanyName { get; set; } = string.Empty;
public List<AnomalyBillSummary> RecentBills { get; set; } = new();
public List<AnomalyVendorHistory> VendorHistory { get; set; } = new();
public List<AnomalyAccountTrend> AccountTrends { get; set; } = new();
}
public class AnomalyFlag
{
/// <summary>"duplicate", "amount_spike", "unusual_vendor", "account_overrun"</summary>
public string Type { get; set; } = string.Empty;
/// <summary>"critical", "warning", "info"</summary>
public string Severity { get; set; } = "warning";
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string? RecommendedAction { get; set; }
public string? BillNumber { get; set; }
}
public class AnomalyDetectionResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public List<AnomalyFlag> Flags { get; set; } = new();
public int CriticalCount { get; set; }
public int WarningCount { get; set; }
public int InfoCount { get; set; }
}
/// <summary>Internal JSON schema that Claude returns for anomaly detection.</summary>
public class ClaudeAnomalyResponse
{
public List<ClaudeAnomalyFlag> Flags { get; set; } = new();
}
public class ClaudeAnomalyFlag
{
public string Type { get; set; } = string.Empty;
public string Severity { get; set; } = "warning";
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string? RecommendedAction { get; set; }
public string? BillNumber { get; set; }
}
@@ -0,0 +1,181 @@
namespace PowderCoating.Application.DTOs.AI;
/// <summary>Request payload sent from JS wizard to the AiAnalyzeItem endpoint.</summary>
public class AiAnalyzeItemRequest
{
/// <summary>TempIds of already-uploaded photos.</summary>
public List<string> PhotoTempIds { get; set; } = new();
// User-supplied context
public string ReferenceDimension { get; set; } = string.Empty; // e.g. "longest edge is 18 inches"
public int Quantity { get; set; } = 1;
public string DesiredColor { get; set; } = string.Empty;
public int CoatCount { get; set; } = 1;
/// <summary>Material type — helps AI factor in prep, outgassing, cure time (e.g. "Cast Iron", "Aluminum").</summary>
public string? MaterialType { get; set; }
/// <summary>Approximate weight in lbs per piece — used to factor heavy-handling surcharge into time estimate.</summary>
public decimal? EstimatedWeightLbs { get; set; }
// Follow-up support (null on first call)
public List<AiConversationTurn>? ConversationHistory { get; set; }
public string? FollowUpAnswer { get; set; }
public int CompanyId { get; set; }
/// <summary>
/// Id of the <see cref="CompanyBlastSetup"/> the user selected before analyzing.
/// When set, the controller loads that setup and passes it to the AI service so the
/// correct blast rate is injected into the prompt.
/// </summary>
public int? BlastSetupId { get; set; }
}
public class AiConversationTurn
{
public string Role { get; set; } = string.Empty; // "user" | "assistant"
public string Content { get; set; } = string.Empty;
}
/// <summary>Result returned to the JS wizard from AI analysis.</summary>
public class AiAnalyzeItemResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
// When NeedsFollowUp is true, the wizard shows FollowUpQuestion to the user
public bool NeedsFollowUp { get; set; }
public string? FollowUpQuestion { get; set; }
public int FollowUpRound { get; set; } // 1 or 2
// Final item data (populated when NeedsFollowUp is false)
public string? Description { get; set; }
public decimal SurfaceAreaSqFt { get; set; }
public string Complexity { get; set; } = "Moderate"; // Simple|Moderate|Complex|Extreme
public int EstimatedMinutes { get; set; }
public string? AiReasoning { get; set; }
public string Confidence { get; set; } = "Medium"; // Low|Medium|High
// Pricing preview (filled by server using company operating costs)
public decimal EstimatedUnitPrice { get; set; }
public decimal EstimatedTotal { get; set; }
public decimal PowderCostPerLb { get; set; }
public decimal CoverageSqFtPerLb { get; set; } = 30m;
public decimal TransferEfficiency { get; set; } = 65m;
// Raw conversation to pass back for follow-up round 2
public List<AiConversationTurn> ConversationHistory { get; set; } = new();
// AI-generated standardized tags from fixed taxonomy
public List<string> Tags { get; set; } = new();
// Historical price benchmark from completed jobs
public AiBenchmarkResult? Benchmark { get; set; }
// ID of the persisted AiItemPrediction record — returned on final result (not follow-up rounds).
// The JS wizard stores this per-item so the controller can link prediction to QuoteItem/JobItem on save.
public int? AiPredictionId { get; set; }
// Pricing breakdown — helps users understand and sanity-check the estimate
public AiPricingBreakdown? Breakdown { get; set; }
}
/// <summary>
/// Display-only cost breakdown returned to the client.
/// Contains computed dollar amounts only — rates, percentages, and coverage constants are intentionally excluded.
/// </summary>
public class AiPricingBreakdown
{
public decimal SurfaceAreaSqFt { get; set; }
public decimal PowderLbsPerCoat { get; set; }
public int CoatCount { get; set; }
public decimal MaterialCost { get; set; }
public decimal ConsumablesCost { get; set; }
public int EstimatedMinutes { get; set; }
/// <summary>Non-zero when a material-type minimum floor exists for this material.</summary>
public int MaterialMinMinutes { get; set; }
/// <summary>True when the floor was actually applied (AI returned less than the floor).</summary>
public bool MinFloorApplied { get; set; }
public decimal LaborCost { get; set; }
public int OvenCycleMinutes { get; set; }
public decimal OvenCost { get; set; }
public bool RequiresPreheat { get; set; }
public int PreheatMinutes { get; set; }
public decimal PreheatCost { get; set; }
public decimal SubtotalBeforeComplexity { get; set; }
public string Complexity { get; set; } = string.Empty;
public decimal ComplexityCharge { get; set; }
public decimal SubtotalBeforeMarkup { get; set; }
public decimal MarkupAmount { get; set; }
public decimal UnitPrice { get; set; }
}
/// <summary>Request to recalculate AI item price when the user overrides sq ft, minutes, or complexity.</summary>
public class AiRecalcPriceRequest
{
public decimal SurfaceAreaSqFt { get; set; }
public int EstimatedMinutes { get; set; }
public string Complexity { get; set; } = "Moderate";
public int CoatCount { get; set; } = 1;
}
/// <summary>Result of a price recalculation — returns the new unit price and a display-safe breakdown.</summary>
public class AiRecalcPriceResult
{
public bool Success { get; set; }
public decimal UnitPrice { get; set; }
public AiPricingBreakdown? Breakdown { get; set; }
}
/// <summary>Historical price benchmark from completed jobs with similar complexity and size.</summary>
public class AiBenchmarkResult
{
public int MatchCount { get; set; }
public decimal MinPrice { get; set; }
public decimal MaxPrice { get; set; }
public decimal AvgPrice { get; set; }
public string ComplexityLevel { get; set; } = string.Empty;
public decimal SqFtRangeMin { get; set; }
public decimal SqFtRangeMax { get; set; }
}
/// <summary>
/// Company-specific AI context passed to the quote service on every analysis call.
/// Contains optional free-text profile and recent accepted predictions as few-shot calibration examples.
/// </summary>
public class CompanyAiContext
{
/// <summary>Free-text description of the shop's specialties and pricing style (from Company Settings).</summary>
public string? ProfileText { get; set; }
/// <summary>Recent predictions the user accepted without override — used as few-shot calibration examples.</summary>
public List<AiFewShotExample> AcceptedExamples { get; set; } = new();
}
/// <summary>A single accepted-prediction record formatted for AI few-shot context.</summary>
public class AiFewShotExample
{
public string Description { get; set; } = string.Empty;
public decimal SurfaceAreaSqFt { get; set; }
public string Complexity { get; set; } = string.Empty;
public int EstimatedMinutes { get; set; }
public decimal FinalUnitPrice { get; set; }
public string? Tags { get; set; }
}
/// <summary>Internal structured response that Claude returns as JSON.</summary>
public class ClaudeQuoteResponse
{
public string Description { get; set; } = string.Empty;
public decimal SurfaceAreaSqFt { get; set; }
public string Complexity { get; set; } = "Moderate";
public int EstimatedMinutes { get; set; }
public bool RequiresPreheat { get; set; }
public int PreheatMinutes { get; set; }
public string Confidence { get; set; } = "Medium";
public bool NeedsFollowUp { get; set; }
public string? FollowUpQuestion { get; set; }
public string Reasoning { get; set; } = string.Empty;
public List<string> Tags { get; set; } = new();
}