Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7020797a25 | |||
| 3b5511a703 | |||
| 8df37ca760 | |||
| 7239f55308 | |||
| 09e077897b | |||
| 051c86810e | |||
| 6721de91e4 | |||
| 226a6237a6 | |||
| cf6acc125f | |||
| f467862877 | |||
| 7ad7d84016 | |||
| 75b0a8afe2 | |||
| 38748c2152 | |||
| 4ec55e7290 | |||
| 3eda91f170 | |||
| cefdf3e35c | |||
| f34ee749be | |||
| 357ef84001 | |||
| 7a1a697dc2 | |||
| 539c6c2559 | |||
| a947494cbd | |||
| 7e79a13cb1 | |||
| 2ad6df1195 | |||
| dc3cd75ea4 | |||
| a73f14fa7f | |||
| 0af31c39b3 | |||
| e1256503be | |||
| b69ff6db3a | |||
| 66231822af | |||
| d5ad9fa073 | |||
| d134dd51e5 | |||
| 1df7c13abd | |||
| 4a8778504f | |||
| f1d7054b3e | |||
| 46b950baf2 | |||
| 4e9c9d321a | |||
| 0c8723ef84 | |||
| 377bb1ce38 | |||
| 2acf54e1a9 | |||
| 0b24c320cd | |||
| 350f2d7658 | |||
| 856d202b78 | |||
| 8caaa84eac | |||
| e70f7ee9f1 | |||
| 6a918c2afc | |||
| 27bfd4db4d | |||
| 787d1504ef | |||
| 726bebdce9 | |||
| 786b78e502 | |||
| cb1b6dceb6 | |||
| fb31fa7eb3 | |||
| 637be701ea | |||
| e9cd67f5d9 | |||
| 433090effd | |||
| 4ca90f561e | |||
| f95397204c | |||
| 31d305b66a | |||
| 42a8c089d5 | |||
| 2c353f2e7f | |||
| c02a5584b4 | |||
| 17da692dce | |||
| 656f830898 | |||
| dde66c807f | |||
| feff0fa73d | |||
| 59beba2e15 | |||
| 959e323f3a | |||
| e2f9e9ae4f | |||
| 328b195127 | |||
| f6d457fe0e | |||
| c65445b94e | |||
| ccb094e57a | |||
| 0204430fa5 | |||
| 4fd9c52aaf | |||
| fde24b09c9 | |||
| a255893ada | |||
| d94612cc9c | |||
| 14026818e2 | |||
| 42eff3357e | |||
| d3a5d827f9 | |||
| 1229081436 | |||
| cf9dcfb4c1 | |||
| a33687f7bd | |||
| 0afb474c3e | |||
| 7e1676cfd7 | |||
| 379b0de885 | |||
| edd7389d7d | |||
| 61866e1d1e | |||
| bc9de38da3 | |||
| 2694863d07 | |||
| 8646fa83c8 | |||
| 796d084ea6 | |||
| 6d23c63912 | |||
| 3803d16731 | |||
| 29fd7163dc |
@@ -478,6 +478,27 @@ All modules below are fully implemented with controllers, views, and migrations
|
|||||||
- In-stock inventory powder: charge for calculated usage only (surface area × lbs/sqft × unit cost)
|
- In-stock inventory powder: charge for calculated usage only (surface area × lbs/sqft × unit cost)
|
||||||
- Tax exempt customers (`Customer.IsTaxExempt`): `TaxPercent` defaults to 0 on quote and invoice create; customer dropdown marks exempt customers with ★
|
- Tax exempt customers (`Customer.IsTaxExempt`): `TaxPercent` defaults to 0 on quote and invoice create; customer dropdown marks exempt customers with ★
|
||||||
|
|
||||||
|
### Pricing Routing Flags — Must Stay In Sync Across All Three Layers
|
||||||
|
|
||||||
|
`PricingCalculationService.CalculateQuoteItemPriceAsync` routes each item to the correct pricing path using boolean flags. **These flags MUST exist identically on `QuoteItem`, `JobItem`, and `CreateQuoteItemDto`, AND be mapped in all three `JobItemAssemblyService.CreateJobItem` overloads.**
|
||||||
|
|
||||||
|
| Flag | Effect if missing on JobItem |
|
||||||
|
|------|------------------------------|
|
||||||
|
| `IsAiItem` | Job repriced as calculated item; oven cost double-charged on every save |
|
||||||
|
| `IsGenericItem` | ManualUnitPrice ignored; price recalculated from surface area |
|
||||||
|
| `IsLaborItem` | Item repriced at surface-area rate instead of hours × labor rate |
|
||||||
|
| `IsSalesItem` | ManualUnitPrice ignored; item repriced using coat/surface math |
|
||||||
|
|
||||||
|
**Checklist when adding a new pricing routing flag:**
|
||||||
|
1. Add the property to `QuoteItem` (Core/Entities)
|
||||||
|
2. Add the property to `JobItem` (Core/Entities)
|
||||||
|
3. Add it to `CreateQuoteItemDto` (Application/DTOs)
|
||||||
|
4. Add it to `JobItemSeed` (private class in JobItemAssemblyService)
|
||||||
|
5. Map it in all three `JobItemAssemblyService.CreateJobItem` overloads
|
||||||
|
6. Include it in every `existingItemsData` JSON block in job views (`Edit.cshtml`, `EditItems.cshtml`) and in all job controller actions that build `CreateQuoteItemDto` from a `JobItem`
|
||||||
|
7. Add a migration if the field is new on a persisted entity
|
||||||
|
8. The structural test `PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem` in `JobItemAssemblyServiceTests` will fail until steps 1–3 are done — this is intentional
|
||||||
|
|
||||||
### Branding
|
### Branding
|
||||||
- Application name: **Powder Coating Logix**
|
- Application name: **Powder Coating Logix**
|
||||||
- PCL logo: `wwwroot/images/pcl-logo.png` — used in sidebar header (when no tenant logo), login/register pages, sidebar footer
|
- PCL logo: `wwwroot/images/pcl-logo.png` — used in sidebar header (when no tenant logo), login/register pages, sidebar footer
|
||||||
|
|||||||
Vendored
+49
-1
@@ -24,6 +24,17 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage('Test') {
|
||||||
|
steps {
|
||||||
|
bat 'dotnet test tests\\PowderCoating.UnitTests --no-build -c Release --logger "trx;LogFileName=results.trx" --results-directory TestResults'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
junit testResults: 'TestResults/*.trx', allowEmptyResults: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stage('Run Migrations') {
|
stage('Run Migrations') {
|
||||||
steps {
|
steps {
|
||||||
bat 'dotnet tool install --global dotnet-ef 2>nul || dotnet tool update --global dotnet-ef 2>nul'
|
bat 'dotnet tool install --global dotnet-ef 2>nul || dotnet tool update --global dotnet-ef 2>nul'
|
||||||
@@ -42,7 +53,18 @@ pipeline {
|
|||||||
|
|
||||||
stage('Deploy to Azure') {
|
stage('Deploy to Azure') {
|
||||||
steps {
|
steps {
|
||||||
bat 'powershell -Command "Add-Type -Assembly System.IO.Compression.FileSystem; if (Test-Path deploy.zip) { Remove-Item deploy.zip }; [System.IO.Compression.ZipFile]::CreateFromDirectory(\'publish\', \'deploy.zip\')"'
|
powershell '''
|
||||||
|
Add-Type -Assembly System.IO.Compression.FileSystem
|
||||||
|
if (Test-Path deploy.zip) { Remove-Item deploy.zip }
|
||||||
|
$publishDir = (Resolve-Path "publish").Path
|
||||||
|
$zip = [System.IO.Compression.ZipFile]::Open("deploy.zip", "Create")
|
||||||
|
Get-ChildItem -Path $publishDir -Recurse -File | ForEach-Object {
|
||||||
|
$entryName = $_.FullName.Substring($publishDir.Length + 1).Replace("\\", "/")
|
||||||
|
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $_.FullName, $entryName, "Optimal") | Out-Null
|
||||||
|
}
|
||||||
|
$zip.Dispose()
|
||||||
|
Write-Host "deploy.zip created with forward-slash entry paths"
|
||||||
|
'''
|
||||||
withCredentials([azureServicePrincipal(
|
withCredentials([azureServicePrincipal(
|
||||||
credentialsId: 'azure-pcl',
|
credentialsId: 'azure-pcl',
|
||||||
subscriptionIdVariable: 'AZ_SUB_ID',
|
subscriptionIdVariable: 'AZ_SUB_ID',
|
||||||
@@ -57,6 +79,32 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage('Health Check') {
|
||||||
|
steps {
|
||||||
|
powershell '''
|
||||||
|
$url = "https://app.powdercoatinglogix.com/"
|
||||||
|
$timeout = 180
|
||||||
|
$elapsed = 0
|
||||||
|
Write-Host "Polling $url for up to $timeout seconds..."
|
||||||
|
do {
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
$elapsed += 10
|
||||||
|
try {
|
||||||
|
$r = Invoke-WebRequest $url -UseBasicParsing -TimeoutSec 10
|
||||||
|
if ($r.StatusCode -lt 400) {
|
||||||
|
Write-Host "App responded HTTP $($r.StatusCode) after ${elapsed}s"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "[${elapsed}s] Not yet responding: $_"
|
||||||
|
}
|
||||||
|
} while ($elapsed -lt $timeout)
|
||||||
|
Write-Error "App did not come healthy within $timeout seconds"
|
||||||
|
exit 1
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|||||||
@@ -322,3 +322,214 @@ public class ClaudeAnomalyFlag
|
|||||||
public string? RecommendedAction { get; set; }
|
public string? RecommendedAction { get; set; }
|
||||||
public string? BillNumber { get; set; }
|
public string? BillNumber { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Feature 7: Bank Rec Auto-Match ───────────────────────────────────────────
|
||||||
|
|
||||||
|
public class BankRecMatchItem
|
||||||
|
{
|
||||||
|
public string EntityType { get; set; } = string.Empty; // "Payment", "BillPayment", "Expense"
|
||||||
|
public int EntityId { get; set; }
|
||||||
|
public string Date { get; set; } = string.Empty; // ISO 8601
|
||||||
|
public string Reference { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public string Direction { get; set; } = string.Empty; // "deposit" or "payment"
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AutoMatchRequest
|
||||||
|
{
|
||||||
|
public List<BankRecMatchItem> UnclearedItems { get; set; } = new();
|
||||||
|
public decimal BeginningBalance { get; set; }
|
||||||
|
public decimal StatementEndingBalance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AutoMatchSuggestion
|
||||||
|
{
|
||||||
|
public string EntityType { get; set; } = string.Empty;
|
||||||
|
public int EntityId { get; set; }
|
||||||
|
public double Confidence { get; set; } // 0.0–1.0
|
||||||
|
public string Reason { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AutoMatchResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
public List<AutoMatchSuggestion> SuggestedCleared { get; set; } = new();
|
||||||
|
public List<string> Insights { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Internal JSON schema that Claude returns for bank rec auto-match.</summary>
|
||||||
|
public class ClaudeAutoMatchResponse
|
||||||
|
{
|
||||||
|
public List<ClaudeAutoMatchSuggestion> SuggestedCleared { get; set; } = new();
|
||||||
|
public List<string> Insights { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClaudeAutoMatchSuggestion
|
||||||
|
{
|
||||||
|
public string EntityType { get; set; } = string.Empty;
|
||||||
|
public int EntityId { get; set; }
|
||||||
|
public double Confidence { get; set; }
|
||||||
|
public string Reason { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Feature 8: Late Payment Prediction ───────────────────────────────────────
|
||||||
|
|
||||||
|
public class OpenInvoiceSummary
|
||||||
|
{
|
||||||
|
public string InvoiceNumber { get; set; } = string.Empty;
|
||||||
|
public decimal BalanceDue { get; set; }
|
||||||
|
public string? DueDateIso { get; set; }
|
||||||
|
public int DaysOverdue { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LatePaymentCustomerData
|
||||||
|
{
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
public decimal TotalOwed { get; set; }
|
||||||
|
public double AvgDaysToPay { get; set; } // historical average
|
||||||
|
public int TotalInvoicesAllTime { get; set; }
|
||||||
|
public int LateInvoicesAllTime { get; set; }
|
||||||
|
public List<OpenInvoiceSummary> OpenInvoices { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LatePaymentPredictionRequest
|
||||||
|
{
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public List<LatePaymentCustomerData> Customers { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LatePaymentPrediction
|
||||||
|
{
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
/// <summary>"high", "medium", or "low"</summary>
|
||||||
|
public string RiskLevel { get; set; } = "medium";
|
||||||
|
public int EstimatedDaysToPayment { get; set; }
|
||||||
|
public string Reasoning { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LatePaymentPredictionResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
public List<LatePaymentPrediction> Predictions { get; set; } = new();
|
||||||
|
public List<string> Insights { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Internal JSON schema that Claude returns for late payment predictions.</summary>
|
||||||
|
public class ClaudeLatePaymentResponse
|
||||||
|
{
|
||||||
|
public List<ClaudeLatePaymentPrediction> Predictions { get; set; } = new();
|
||||||
|
public List<string> Insights { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClaudeLatePaymentPrediction
|
||||||
|
{
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
public string RiskLevel { get; set; } = "medium";
|
||||||
|
public int EstimatedDaysToPayment { get; set; }
|
||||||
|
public string Reasoning { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Feature 9: Natural Language Financial Queries ─────────────────────────────
|
||||||
|
|
||||||
|
public class MonthlyFinancialSummary
|
||||||
|
{
|
||||||
|
public string Month { get; set; } = string.Empty; // "YYYY-MM"
|
||||||
|
public decimal Revenue { get; set; }
|
||||||
|
public decimal Expenses { get; set; }
|
||||||
|
public decimal NetIncome { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FinancialQueryContext
|
||||||
|
{
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public string AsOfDate { get; set; } = string.Empty;
|
||||||
|
public decimal TotalRevenueYtd { get; set; }
|
||||||
|
public decimal TotalExpensesYtd { get; set; }
|
||||||
|
public decimal NetIncomeYtd { get; set; }
|
||||||
|
public decimal ArOutstanding { get; set; }
|
||||||
|
public decimal ApOutstanding { get; set; }
|
||||||
|
public List<MonthlyFinancialSummary> Last12Months { get; set; } = new();
|
||||||
|
public List<ExpenseByCategory> ExpensesByCategory { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FinancialQueryRequest
|
||||||
|
{
|
||||||
|
public string Question { get; set; } = string.Empty;
|
||||||
|
public FinancialQueryContext Context { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FinancialQueryResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
public string Answer { get; set; } = string.Empty;
|
||||||
|
public string? FollowUpSuggestion { get; set; }
|
||||||
|
public List<string> RelevantFacts { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Internal JSON schema that Claude returns for financial queries.</summary>
|
||||||
|
public class ClaudeFinancialQueryResponse
|
||||||
|
{
|
||||||
|
public string Answer { get; set; } = string.Empty;
|
||||||
|
public string? FollowUpSuggestion { get; set; }
|
||||||
|
public List<string> RelevantFacts { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Feature 10: Recurring Bill Detection ─────────────────────────────────────
|
||||||
|
|
||||||
|
public class RecurringBillHistoryItem
|
||||||
|
{
|
||||||
|
public string VendorName { get; set; } = string.Empty;
|
||||||
|
public string BillNumber { get; set; } = string.Empty;
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public string DateIso { get; set; } = string.Empty;
|
||||||
|
public string? Memo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RecurringBillDetectionRequest
|
||||||
|
{
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public List<RecurringBillHistoryItem> Bills { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RecurringBillPattern
|
||||||
|
{
|
||||||
|
public string VendorName { get; set; } = string.Empty;
|
||||||
|
/// <summary>"monthly", "quarterly", "biannual", "annual"</summary>
|
||||||
|
public string Frequency { get; set; } = string.Empty;
|
||||||
|
public decimal TypicalAmount { get; set; }
|
||||||
|
public string? NextExpectedDateIso { get; set; }
|
||||||
|
/// <summary>"high", "medium", or "low"</summary>
|
||||||
|
public string Confidence { get; set; } = "medium";
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string? SuggestedAction { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RecurringBillDetectionResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
public List<RecurringBillPattern> Patterns { get; set; } = new();
|
||||||
|
public List<string> Insights { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Internal JSON schema that Claude returns for recurring bill detection.</summary>
|
||||||
|
public class ClaudeRecurringBillResponse
|
||||||
|
{
|
||||||
|
public List<ClaudeRecurringPattern> Patterns { get; set; } = new();
|
||||||
|
public List<string> Insights { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClaudeRecurringPattern
|
||||||
|
{
|
||||||
|
public string VendorName { get; set; } = string.Empty;
|
||||||
|
public string Frequency { get; set; } = string.Empty;
|
||||||
|
public decimal TypicalAmount { get; set; }
|
||||||
|
public string? NextExpectedDateIso { get; set; }
|
||||||
|
public string Confidence { get; set; } = "medium";
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string? SuggestedAction { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,158 @@ using PowderCoating.Core.Enums;
|
|||||||
|
|
||||||
namespace PowderCoating.Application.DTOs.Accounting;
|
namespace PowderCoating.Application.DTOs.Accounting;
|
||||||
|
|
||||||
|
// Accounting method badge — set on report DTOs so views can show "Cash Basis" / "Accrual Basis"
|
||||||
|
// without needing a separate round-trip to the company settings.
|
||||||
|
|
||||||
|
|
||||||
|
// ── Cash Flow Statement ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cash Flow Statement using the direct (cash-basis) method for operating activities.
|
||||||
|
/// Investing and Financing sections contain line items derived from account-level changes.
|
||||||
|
/// BeginningCash + NetChangeInCash should equal EndingCash (within rounding tolerances).
|
||||||
|
/// </summary>
|
||||||
|
public class CashFlowStatementDto
|
||||||
|
{
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public DateTime From { get; set; }
|
||||||
|
public DateTime To { get; set; }
|
||||||
|
public AccountingMethod Method { get; set; }
|
||||||
|
|
||||||
|
// ── Operating (direct / cash method) ───────────────────────────────────
|
||||||
|
/// <summary>Customer invoice payments received in the period.</summary>
|
||||||
|
public decimal CashFromCustomers { get; set; }
|
||||||
|
/// <summary>Vendor bill payments made in the period.</summary>
|
||||||
|
public decimal CashToVendors { get; set; }
|
||||||
|
/// <summary>Direct expense payments made in the period (not via bills).</summary>
|
||||||
|
public decimal CashForExpenses { get; set; }
|
||||||
|
public decimal NetOperating => CashFromCustomers - CashToVendors - CashForExpenses;
|
||||||
|
|
||||||
|
// ── Investing ──────────────────────────────────────────────────────────
|
||||||
|
public List<CashFlowLineDto> InvestingLines { get; set; } = new();
|
||||||
|
public decimal NetInvesting => InvestingLines.Sum(l => l.Amount);
|
||||||
|
|
||||||
|
// ── Financing ──────────────────────────────────────────────────────────
|
||||||
|
public List<CashFlowLineDto> FinancingLines { get; set; } = new();
|
||||||
|
public decimal NetFinancing => FinancingLines.Sum(l => l.Amount);
|
||||||
|
|
||||||
|
// ── Summary ────────────────────────────────────────────────────────────
|
||||||
|
public decimal BeginningCash { get; set; }
|
||||||
|
public decimal NetChangeInCash => NetOperating + NetInvesting + NetFinancing;
|
||||||
|
public decimal EndingCash => BeginningCash + NetChangeInCash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A single line in the Investing or Financing section of the Cash Flow Statement.</summary>
|
||||||
|
public class CashFlowLineDto
|
||||||
|
{
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
/// <summary>Positive = cash inflow, negative = cash outflow.</summary>
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Customer / Vendor Statements ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class CustomerStatementDto
|
||||||
|
{
|
||||||
|
public int CustomerId { get; set; }
|
||||||
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public string? CustomerAddress { get; set; }
|
||||||
|
public DateTime From { get; set; }
|
||||||
|
public DateTime To { get; set; }
|
||||||
|
public decimal OpeningBalance { get; set; }
|
||||||
|
public List<StatementLineDto> Lines { get; set; } = new();
|
||||||
|
public decimal ClosingBalance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VendorStatementDto
|
||||||
|
{
|
||||||
|
public int VendorId { get; set; }
|
||||||
|
public string VendorName { get; set; } = string.Empty;
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public DateTime From { get; set; }
|
||||||
|
public DateTime To { get; set; }
|
||||||
|
public decimal OpeningBalance { get; set; }
|
||||||
|
public List<StatementLineDto> Lines { get; set; } = new();
|
||||||
|
public decimal ClosingBalance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StatementLineDto
|
||||||
|
{
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
/// <summary>E.g., "Invoice", "Payment", "Credit Applied", "Deposit Applied".</summary>
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
public string Reference { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
/// <summary>Amount added to the balance (invoice for customer, bill for vendor).</summary>
|
||||||
|
public decimal? Debit { get; set; }
|
||||||
|
/// <summary>Amount reducing the balance (payment, credit).</summary>
|
||||||
|
public decimal? Credit { get; set; }
|
||||||
|
public decimal RunningBalance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── AP Aging ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class ApAgingReportDto
|
||||||
|
{
|
||||||
|
public DateTime AsOf { get; set; }
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<ApAgingVendorDto> Vendors { get; set; } = new();
|
||||||
|
|
||||||
|
public decimal TotalCurrent { get; set; }
|
||||||
|
public decimal Total1to30 { get; set; }
|
||||||
|
public decimal Total31to60 { get; set; }
|
||||||
|
public decimal Total61to90 { get; set; }
|
||||||
|
public decimal TotalOver90 { get; set; }
|
||||||
|
public decimal TotalOutstanding => TotalCurrent + Total1to30 + Total31to60 + Total61to90 + TotalOver90;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApAgingVendorDto
|
||||||
|
{
|
||||||
|
public int VendorId { get; set; }
|
||||||
|
public string VendorName { get; set; } = string.Empty;
|
||||||
|
public List<ApAgingBillDto> Bills { get; set; } = new();
|
||||||
|
public decimal TotalCurrent { get; set; }
|
||||||
|
public decimal Total1to30 { get; set; }
|
||||||
|
public decimal Total31to60 { get; set; }
|
||||||
|
public decimal Total61to90 { get; set; }
|
||||||
|
public decimal TotalOver90 { get; set; }
|
||||||
|
public decimal TotalBalance => TotalCurrent + Total1to30 + Total31to60 + Total61to90 + TotalOver90;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApAgingBillDto
|
||||||
|
{
|
||||||
|
public int BillId { get; set; }
|
||||||
|
public string BillNumber { get; set; } = string.Empty;
|
||||||
|
public DateTime BillDate { get; set; }
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
|
public decimal BalanceDue { get; set; }
|
||||||
|
public int DaysOverdue { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Trial Balance ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class TrialBalanceDto
|
||||||
|
{
|
||||||
|
public DateTime AsOf { get; set; }
|
||||||
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public List<TrialBalanceLine> Lines { get; set; } = new();
|
||||||
|
public decimal TotalDebits { get; set; }
|
||||||
|
public decimal TotalCredits { get; set; }
|
||||||
|
public bool IsBalanced => Math.Abs(TotalDebits - TotalCredits) < 0.01m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TrialBalanceLine
|
||||||
|
{
|
||||||
|
public int AccountId { get; set; }
|
||||||
|
public string AccountNumber { get; set; } = string.Empty;
|
||||||
|
public string AccountName { get; set; } = string.Empty;
|
||||||
|
public AccountType AccountType { get; set; }
|
||||||
|
public decimal DebitBalance { get; set; }
|
||||||
|
public decimal CreditBalance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Profit & Loss ─────────────────────────────────────────────────────────────
|
// ── Profit & Loss ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public class ProfitAndLossDto
|
public class ProfitAndLossDto
|
||||||
@@ -9,6 +161,7 @@ public class ProfitAndLossDto
|
|||||||
public DateTime From { get; set; }
|
public DateTime From { get; set; }
|
||||||
public DateTime To { get; set; }
|
public DateTime To { get; set; }
|
||||||
public string CompanyName { get; set; } = string.Empty;
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public AccountingMethod AccountingMethod { get; set; } = AccountingMethod.Accrual;
|
||||||
|
|
||||||
public List<FinancialReportLine> RevenueLines { get; set; } = new();
|
public List<FinancialReportLine> RevenueLines { get; set; } = new();
|
||||||
public decimal TotalRevenue { get; set; }
|
public decimal TotalRevenue { get; set; }
|
||||||
@@ -40,6 +193,7 @@ public class BalanceSheetDto
|
|||||||
{
|
{
|
||||||
public DateTime AsOf { get; set; }
|
public DateTime AsOf { get; set; }
|
||||||
public string CompanyName { get; set; } = string.Empty;
|
public string CompanyName { get; set; } = string.Empty;
|
||||||
|
public AccountingMethod AccountingMethod { get; set; } = AccountingMethod.Accrual;
|
||||||
|
|
||||||
// Assets
|
// Assets
|
||||||
public List<FinancialReportLine> CurrentAssets { get; set; } = new();
|
public List<FinancialReportLine> CurrentAssets { get; set; } = new();
|
||||||
|
|||||||
@@ -3,6 +3,22 @@ namespace PowderCoating.Application.DTOs.Common;
|
|||||||
public class PagedResult<T>
|
public class PagedResult<T>
|
||||||
{
|
{
|
||||||
public IEnumerable<T> Items { get; set; } = new List<T>();
|
public IEnumerable<T> Items { get; set; } = new List<T>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a PagedResult populated from a GridRequest, avoiding repetitive property
|
||||||
|
/// assignments across every Index action. SortColumn, SortDirection, and SearchTerm
|
||||||
|
/// are copied from the grid so the model carries full state for view binding.
|
||||||
|
/// </summary>
|
||||||
|
public static PagedResult<T> From(GridRequest grid, IEnumerable<T> items, int totalCount) => new()
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
PageNumber = grid.PageNumber,
|
||||||
|
PageSize = grid.PageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
SortColumn = grid.SortColumn,
|
||||||
|
SortDirection = grid.SortDirection,
|
||||||
|
SearchTerm = grid.SearchTerm
|
||||||
|
};
|
||||||
public int PageNumber { get; set; }
|
public int PageNumber { get; set; }
|
||||||
public int PageSize { get; set; }
|
public int PageSize { get; set; }
|
||||||
public int TotalCount { get; set; }
|
public int TotalCount { get; set; }
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ public class CompanyListDto
|
|||||||
public bool WizardCompleted { get; set; }
|
public bool WizardCompleted { get; set; }
|
||||||
public DateTime? WizardCompletedAt { get; set; }
|
public DateTime? WizardCompletedAt { get; set; }
|
||||||
public string? WizardCompletedByName { get; set; }
|
public string? WizardCompletedByName { get; set; }
|
||||||
|
|
||||||
|
// Health signals — populated by CompaniesController.Index after the count summary query
|
||||||
|
public int HealthScore { get; set; }
|
||||||
|
public string HealthRisk { get; set; } = "Healthy";
|
||||||
|
public DateTime? LastLoginDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ public class CompanyPreferencesDto
|
|||||||
// Blank Work Order PDF Template
|
// Blank Work Order PDF Template
|
||||||
public string WoAccentColor { get; set; } = "#374151";
|
public string WoAccentColor { get; set; } = "#374151";
|
||||||
public string? WoTerms { get; set; }
|
public string? WoTerms { get; set; }
|
||||||
|
|
||||||
|
// Kiosk settings
|
||||||
|
public string KioskIntakeOutput { get; set; } = "Quote";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UpdateAppDefaultsDto
|
public class UpdateAppDefaultsDto
|
||||||
@@ -136,3 +139,11 @@ public class UpdateWorkOrderTemplateDto
|
|||||||
public string WoAccentColor { get; set; } = "#374151";
|
public string WoAccentColor { get; set; } = "#374151";
|
||||||
[StringLength(2000)] public string? WoTerms { get; set; }
|
[StringLength(2000)] public string? WoTerms { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class UpdateKioskSettingsDto
|
||||||
|
{
|
||||||
|
/// <summary>"Quote" (default) or "Job" — what the kiosk creates on submission.</summary>
|
||||||
|
[Required]
|
||||||
|
public string KioskIntakeOutput { get; set; } = "Quote";
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace PowderCoating.Application.DTOs.Company
|
|||||||
public string? State { get; set; }
|
public string? State { get; set; }
|
||||||
public string? ZipCode { get; set; }
|
public string? ZipCode { get; set; }
|
||||||
public string? TimeZone { get; set; }
|
public string? TimeZone { get; set; }
|
||||||
|
public AccountingMethod AccountingMethod { get; set; } = AccountingMethod.Accrual;
|
||||||
public bool HasLogo { get; set; }
|
public bool HasLogo { get; set; }
|
||||||
|
|
||||||
public CompanyOperatingCostsDto? OperatingCosts { get; set; }
|
public CompanyOperatingCostsDto? OperatingCosts { get; set; }
|
||||||
@@ -96,6 +97,9 @@ namespace PowderCoating.Application.DTOs.Company
|
|||||||
|
|
||||||
[StringLength(50, ErrorMessage = "Time zone cannot exceed 50 characters")]
|
[StringLength(50, ErrorMessage = "Time zone cannot exceed 50 characters")]
|
||||||
public string? TimeZone { get; set; }
|
public string? TimeZone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Cash or Accrual accounting method preference for financial reports.</summary>
|
||||||
|
public AccountingMethod AccountingMethod { get; set; } = AccountingMethod.Accrual;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -140,12 +140,12 @@ public class CreateCustomerDto : IValidatableObject
|
|||||||
new[] { nameof(CompanyName), nameof(ContactFirstName), nameof(ContactLastName) });
|
new[] { nameof(CompanyName), nameof(ContactFirstName), nameof(ContactLastName) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// At least one contact method is required (Email OR Phone)
|
// At least one contact method is required (Email, Phone, or Mobile Phone)
|
||||||
if (string.IsNullOrWhiteSpace(Email) && string.IsNullOrWhiteSpace(Phone))
|
if (string.IsNullOrWhiteSpace(Email) && string.IsNullOrWhiteSpace(Phone) && string.IsNullOrWhiteSpace(MobilePhone))
|
||||||
{
|
{
|
||||||
yield return new ValidationResult(
|
yield return new ValidationResult(
|
||||||
"Please provide at least one contact method (Email or Phone)",
|
"Please provide at least one contact method (Email, Phone, or Mobile Phone)",
|
||||||
new[] { nameof(Email), nameof(Phone) });
|
new[] { nameof(Email), nameof(Phone), nameof(MobilePhone) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate each address in comma-separated email fields
|
// Validate each address in comma-separated email fields
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class GiftCertificateListDto
|
|||||||
public GiftCertificateStatus Status { get; set; }
|
public GiftCertificateStatus Status { get; set; }
|
||||||
public DateTime IssueDate { get; set; }
|
public DateTime IssueDate { get; set; }
|
||||||
public DateTime? ExpiryDate { get; set; }
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
public Guid? BatchId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GiftCertificateDto : GiftCertificateListDto
|
public class GiftCertificateDto : GiftCertificateListDto
|
||||||
@@ -87,3 +88,27 @@ public class RedeemGiftCertificateDto
|
|||||||
[Range(0.01, 9999.99)]
|
[Range(0.01, 9999.99)]
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class BulkCreateGiftCertificateDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[Range(1, 500, ErrorMessage = "Quantity must be between 1 and 500.")]
|
||||||
|
[Display(Name = "Number of Certificates")]
|
||||||
|
public int Quantity { get; set; } = 25;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Range(1.00, 9999.99, ErrorMessage = "Amount must be between $1.00 and $9,999.99.")]
|
||||||
|
[Display(Name = "Face Value (each)")]
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Display(Name = "Issued Reason")]
|
||||||
|
public GiftCertificateIssuedReason IssuedReason { get; set; } = GiftCertificateIssuedReason.Promotional;
|
||||||
|
|
||||||
|
[Display(Name = "Expiry Date (optional)")]
|
||||||
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
[StringLength(1000)]
|
||||||
|
[Display(Name = "Event / Notes (applied to all certificates)")]
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ public class InvoiceDto
|
|||||||
public string CustomerName { get; set; } = string.Empty;
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
public string? CustomerEmail { get; set; }
|
public string? CustomerEmail { get; set; }
|
||||||
public string? CustomerPhone { get; set; }
|
public string? CustomerPhone { get; set; }
|
||||||
|
public string? CustomerMobilePhone { get; set; }
|
||||||
public bool CustomerNotifyByEmail { get; set; }
|
public bool CustomerNotifyByEmail { get; set; }
|
||||||
|
public bool CustomerNotifyBySms { get; set; }
|
||||||
public string? PreparedById { get; set; }
|
public string? PreparedById { get; set; }
|
||||||
public string? PreparedByName { get; set; }
|
public string? PreparedByName { get; set; }
|
||||||
public InvoiceStatus Status { get; set; }
|
public InvoiceStatus Status { get; set; }
|
||||||
@@ -82,6 +84,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ public class IssueRefundDto
|
|||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public DateTime RefundDate { get; set; } = DateTime.Today;
|
public DateTime RefundDate { get; set; } = DateTime.Today;
|
||||||
public PaymentMethod RefundMethod { get; set; }
|
public PaymentMethod RefundMethod { get; set; }
|
||||||
|
/// <summary>Bank/cash account money leaves when issuing a cash/card refund. Null for store credit.</summary>
|
||||||
|
public int? DepositAccountId { get; set; }
|
||||||
public string Reason { get; set; } = string.Empty;
|
public string Reason { get; set; } = string.Empty;
|
||||||
public string? Reference { get; set; }
|
public string? Reference { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
|
|||||||
@@ -515,6 +515,9 @@ public class JobEditItemsViewModel
|
|||||||
public string JobNumber { get; set; } = string.Empty;
|
public string JobNumber { get; set; } = string.Empty;
|
||||||
public int? CustomerId { get; set; }
|
public int? CustomerId { get; set; }
|
||||||
public decimal TaxPercent { get; set; }
|
public decimal TaxPercent { get; set; }
|
||||||
|
public int? OvenCostId { get; set; }
|
||||||
|
public int OvenBatches { get; set; } = 1;
|
||||||
|
public int? OvenCycleMinutes { get; set; }
|
||||||
public List<CreateQuoteItemDto> JobItems { get; set; } = new();
|
public List<CreateQuoteItemDto> JobItems { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using PowderCoating.Core.Enums;
|
||||||
|
|
||||||
|
namespace PowderCoating.Application.DTOs.Kiosk;
|
||||||
|
|
||||||
|
// ── Staff-facing ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Input for sending a remote intake link to a customer by email.</summary>
|
||||||
|
public class SendRemoteLinkDto
|
||||||
|
{
|
||||||
|
[Required, EmailAddress]
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Optional — used to personalise the email greeting.</summary>
|
||||||
|
public string? CustomerName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Customer-facing step DTOs ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Step 1 — Contact information submitted by the customer.</summary>
|
||||||
|
public class SubmitKioskContactDto
|
||||||
|
{
|
||||||
|
[Required, MaxLength(100)]
|
||||||
|
public string FirstName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, MaxLength(100)]
|
||||||
|
public string LastName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, Phone]
|
||||||
|
public string Phone { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required, EmailAddress]
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool IsReturningCustomer { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Step 2 — Job description submitted by the customer.</summary>
|
||||||
|
public class SubmitKioskJobDto
|
||||||
|
{
|
||||||
|
[Required, MaxLength(2000)]
|
||||||
|
public string JobDescription { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? HowDidYouHearAboutUs { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Step 3 — Terms agreement (+ optional drawn signature for in-person sessions).</summary>
|
||||||
|
public class SubmitKioskTermsDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[Range(typeof(bool), "true", "true", ErrorMessage = "You must agree to the terms to continue.")]
|
||||||
|
public bool AgreedToTerms { get; set; }
|
||||||
|
|
||||||
|
public bool SmsOptIn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Base-64 PNG from signature_pad; required for InPerson sessions, null for Remote.</summary>
|
||||||
|
public string? SignatureDataBase64 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Staff review list ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>One row in the Kiosk Intakes staff review list.</summary>
|
||||||
|
public class KioskSessionListDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public Guid SessionToken { get; set; }
|
||||||
|
public KioskSessionType SessionType { get; set; }
|
||||||
|
public KioskSessionStatus Status { get; set; }
|
||||||
|
public string CustomerFirstName { get; set; } = string.Empty;
|
||||||
|
public string CustomerLastName { get; set; } = string.Empty;
|
||||||
|
public string CustomerEmail { get; set; } = string.Empty;
|
||||||
|
public string CustomerPhone { get; set; } = string.Empty;
|
||||||
|
public string JobDescription { get; set; } = string.Empty;
|
||||||
|
public bool SmsOptIn { get; set; }
|
||||||
|
public DateTime? SubmittedAt { get; set; }
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
public int? LinkedCustomerId { get; set; }
|
||||||
|
public int? LinkedJobId { get; set; }
|
||||||
|
public int? LinkedQuoteId { get; set; }
|
||||||
|
public string? RemoteLinkEmail { get; set; }
|
||||||
|
|
||||||
|
public string CustomerFullName => $"{CustomerFirstName} {CustomerLastName}".Trim();
|
||||||
|
public string JobDescriptionSnippet =>
|
||||||
|
JobDescription.Length > 80 ? JobDescription[..80] + "…" : JobDescription;
|
||||||
|
public bool IsConverted => LinkedJobId.HasValue || LinkedQuoteId.HasValue;
|
||||||
|
public bool IsExpired => Status == KioskSessionStatus.Expired ||
|
||||||
|
(Status == KioskSessionStatus.Active && DateTime.UtcNow > ExpiresAt);
|
||||||
|
}
|
||||||
@@ -604,6 +604,11 @@ public class QuotePricingBreakdownDto
|
|||||||
|
|
||||||
public decimal SubtotalBeforeDiscount { get; set; }
|
public decimal SubtotalBeforeDiscount { get; set; }
|
||||||
|
|
||||||
|
public decimal PricingTierDiscountAmount { get; set; }
|
||||||
|
public decimal PricingTierDiscountPercent { get; set; }
|
||||||
|
public decimal QuoteDiscountAmount { get; set; }
|
||||||
|
public decimal QuoteDiscountPercent { get; set; }
|
||||||
|
|
||||||
public decimal DiscountAmount { get; set; }
|
public decimal DiscountAmount { get; set; }
|
||||||
public decimal DiscountPercent { get; set; }
|
public decimal DiscountPercent { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ public class CompanyUserDto
|
|||||||
public bool CanManageMaintenance { get; set; }
|
public bool CanManageMaintenance { get; set; }
|
||||||
public bool CanManageInvoices { get; set; }
|
public bool CanManageInvoices { get; set; }
|
||||||
public bool CanViewReports { get; set; }
|
public bool CanViewReports { get; set; }
|
||||||
|
public bool CanManageBills { get; set; }
|
||||||
|
public bool CanManageAccounting { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -156,6 +158,12 @@ public class CreateCompanyUserDto
|
|||||||
[Display(Name = "Can View Reports")]
|
[Display(Name = "Can View Reports")]
|
||||||
public bool CanViewReports { get; set; }
|
public bool CanViewReports { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Can Manage Bills & AP")]
|
||||||
|
public bool CanManageBills { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Can Manage Accounting")]
|
||||||
|
public bool CanManageAccounting { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Send Welcome Email")]
|
[Display(Name = "Send Welcome Email")]
|
||||||
public bool SendWelcomeEmail { get; set; } = true;
|
public bool SendWelcomeEmail { get; set; } = true;
|
||||||
}
|
}
|
||||||
@@ -258,4 +266,10 @@ public class UpdateCompanyUserDto
|
|||||||
|
|
||||||
[Display(Name = "Can View Reports")]
|
[Display(Name = "Can View Reports")]
|
||||||
public bool CanViewReports { get; set; }
|
public bool CanViewReports { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Can Manage Bills & AP")]
|
||||||
|
public bool CanManageBills { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Can Manage Accounting")]
|
||||||
|
public bool CanManageAccounting { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ public class CreateVendorDto
|
|||||||
[Display(Name = "Preferred Vendor")]
|
[Display(Name = "Preferred Vendor")]
|
||||||
public bool IsPreferred { get; set; } = false;
|
public bool IsPreferred { get; set; } = false;
|
||||||
|
|
||||||
|
[Display(Name = "1099 Vendor")]
|
||||||
|
public bool Is1099Vendor { get; set; } = false;
|
||||||
|
|
||||||
[Display(Name = "Default Expense Account")]
|
[Display(Name = "Default Expense Account")]
|
||||||
public int? DefaultExpenseAccountId { get; set; }
|
public int? DefaultExpenseAccountId { get; set; }
|
||||||
}
|
}
|
||||||
@@ -201,6 +204,9 @@ public class UpdateVendorDto
|
|||||||
[Display(Name = "Preferred Vendor")]
|
[Display(Name = "Preferred Vendor")]
|
||||||
public bool IsPreferred { get; set; }
|
public bool IsPreferred { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "1099 Vendor")]
|
||||||
|
public bool Is1099Vendor { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Default Expense Account")]
|
[Display(Name = "Default Expense Account")]
|
||||||
public int? DefaultExpenseAccountId { get; set; }
|
public int? DefaultExpenseAccountId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,4 +43,33 @@ public interface IAccountingAiService
|
|||||||
/// Returns a ranked list of flagged items with recommended actions.
|
/// Returns a ranked list of flagged items with recommended actions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<AnomalyDetectionResult> DetectAnomaliesAsync(AnomalyDetectionRequest request);
|
Task<AnomalyDetectionResult> DetectAnomaliesAsync(AnomalyDetectionRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suggests which uncleared bank rec items should be marked as cleared to reconcile
|
||||||
|
/// a statement. Returns a ranked list of suggestions with confidence scores based on
|
||||||
|
/// amount/date patterns and the gap between the current cleared balance and the
|
||||||
|
/// statement ending balance.
|
||||||
|
/// </summary>
|
||||||
|
Task<AutoMatchResult> AutoMatchReconciliationAsync(AutoMatchRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Predicts likelihood of late payment for each open AR customer using their historical
|
||||||
|
/// payment behavior (avg days to pay, late rate) combined with current overdue status.
|
||||||
|
/// Returns risk levels (high/medium/low) and estimated days to collection.
|
||||||
|
/// </summary>
|
||||||
|
Task<LatePaymentPredictionResult> PredictLatePaymentsAsync(LatePaymentPredictionRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Answers a plain-English financial question (e.g. "What did we spend on powder last quarter?")
|
||||||
|
/// using pre-loaded company financial context. Returns a direct answer, supporting facts,
|
||||||
|
/// and an optional follow-up question suggestion.
|
||||||
|
/// </summary>
|
||||||
|
Task<FinancialQueryResult> AnswerFinancialQueryAsync(FinancialQueryRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analyzes 6–12 months of bill history to detect recurring payment patterns per vendor.
|
||||||
|
/// Returns detected patterns with frequency, typical amount, next expected date, and
|
||||||
|
/// suggested actions (e.g. set a reminder, create a template).
|
||||||
|
/// </summary>
|
||||||
|
Task<RecurringBillDetectionResult> DetectRecurringBillsAsync(RecurringBillDetectionRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using PowderCoating.Application.DTOs.Accounting;
|
using PowderCoating.Application.DTOs.Accounting;
|
||||||
|
using PowderCoating.Core.Enums;
|
||||||
|
|
||||||
namespace PowderCoating.Application.Interfaces;
|
namespace PowderCoating.Application.Interfaces;
|
||||||
|
|
||||||
@@ -6,14 +7,16 @@ namespace PowderCoating.Application.Interfaces;
|
|||||||
/// Read-only service for financial aggregate reports. All methods query the database
|
/// Read-only service for financial aggregate reports. All methods query the database
|
||||||
/// with AsNoTracking and return pre-shaped DTOs — no tracked entities are returned.
|
/// with AsNoTracking and return pre-shaped DTOs — no tracked entities are returned.
|
||||||
/// Implemented in Infrastructure; uses ApplicationDbContext directly.
|
/// Implemented in Infrastructure; uses ApplicationDbContext directly.
|
||||||
|
/// The <paramref name="method"/> parameter overrides the company's stored preference when
|
||||||
|
/// supplied; pass <c>null</c> to fall back to the company's configured accounting method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IFinancialReportService
|
public interface IFinancialReportService
|
||||||
{
|
{
|
||||||
/// <summary>Returns a Profit & Loss report for the given company and date range.</summary>
|
/// <summary>Returns a Profit & Loss report for the given company and date range.</summary>
|
||||||
Task<ProfitAndLossDto> GetProfitAndLossAsync(int companyId, DateTime from, DateTime to);
|
Task<ProfitAndLossDto> GetProfitAndLossAsync(int companyId, DateTime from, DateTime to, AccountingMethod? method = null);
|
||||||
|
|
||||||
/// <summary>Returns a Balance Sheet snapshot as of the given date.</summary>
|
/// <summary>Returns a Balance Sheet snapshot as of the given date.</summary>
|
||||||
Task<BalanceSheetDto> GetBalanceSheetAsync(int companyId, DateTime asOf);
|
Task<BalanceSheetDto> GetBalanceSheetAsync(int companyId, DateTime asOf, AccountingMethod? method = null);
|
||||||
|
|
||||||
/// <summary>Returns an AR Aging report bucketed at 0-30, 31-60, 61-90, and 90+ days.</summary>
|
/// <summary>Returns an AR Aging report bucketed at 0-30, 31-60, 61-90, and 90+ days.</summary>
|
||||||
Task<ArAgingReportDto> GetArAgingAsync(int companyId, DateTime asOf);
|
Task<ArAgingReportDto> GetArAgingAsync(int companyId, DateTime asOf);
|
||||||
@@ -23,4 +26,27 @@ public interface IFinancialReportService
|
|||||||
|
|
||||||
/// <summary>Returns an invoice-basis Sales Tax Liability report for the given company and date range.</summary>
|
/// <summary>Returns an invoice-basis Sales Tax Liability report for the given company and date range.</summary>
|
||||||
Task<SalesTaxReportDto> GetSalesTaxReportAsync(int companyId, DateTime from, DateTime to);
|
Task<SalesTaxReportDto> GetSalesTaxReportAsync(int companyId, DateTime from, DateTime to);
|
||||||
|
|
||||||
|
/// <summary>Returns an AP Aging report bucketed at 0-30, 31-60, 61-90, and 90+ days past the bill due date.</summary>
|
||||||
|
Task<ApAgingReportDto> GetApAgingAsync(int companyId, DateTime asOf);
|
||||||
|
|
||||||
|
/// <summary>Returns a Trial Balance using current account balances as of the given date.</summary>
|
||||||
|
Task<TrialBalanceDto> GetTrialBalanceAsync(int companyId, DateTime asOf);
|
||||||
|
|
||||||
|
/// <summary>Looks up the accounting method configured for the given company. Returns Accrual if not found.</summary>
|
||||||
|
Task<AccountingMethod> GetCompanyAccountingMethodAsync(int companyId);
|
||||||
|
|
||||||
|
/// <summary>Returns a dated activity statement for a customer showing opening balance, all transactions in the period, and closing balance.</summary>
|
||||||
|
Task<CustomerStatementDto> GetCustomerStatementAsync(int companyId, int customerId, DateTime from, DateTime to);
|
||||||
|
|
||||||
|
/// <summary>Returns a dated activity statement for a vendor showing opening balance, all transactions in the period, and closing balance.</summary>
|
||||||
|
Task<VendorStatementDto> GetVendorStatementAsync(int companyId, int vendorId, DateTime from, DateTime to);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a Cash Flow Statement for the period using the direct (cash-basis) method for
|
||||||
|
/// operating activities. Investing and Financing sections are derived from account-level data.
|
||||||
|
/// BeginningCash is computed from all cash/bank account credits and debits prior to
|
||||||
|
/// <paramref name="from"/>; EndingCash adds the net change during the period.
|
||||||
|
/// </summary>
|
||||||
|
Task<CashFlowStatementDto> GetCashFlowStatementAsync(int companyId, DateTime from, DateTime to);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using PowderCoating.Application.DTOs.Quote;
|
||||||
|
using PowderCoating.Core.Entities;
|
||||||
|
|
||||||
|
namespace PowderCoating.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface IJobItemAssemblyService
|
||||||
|
{
|
||||||
|
JobItem CreateJobItem(CreateQuoteItemDto source, int jobId, int companyId, QuoteItemPricingResult pricing, DateTime createdAtUtc);
|
||||||
|
IReadOnlyList<JobItemCoat> CreateJobItemCoats(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc);
|
||||||
|
IReadOnlyList<JobItemPrepService> CreateJobItemPrepServices(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc);
|
||||||
|
|
||||||
|
JobItem CreateJobItem(QuoteItem source, int jobId, int companyId, DateTime createdAtUtc);
|
||||||
|
IReadOnlyList<JobItemCoat> CreateJobItemCoats(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc);
|
||||||
|
IReadOnlyList<JobItemPrepService> CreateJobItemPrepServices(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc);
|
||||||
|
|
||||||
|
JobItem CreateJobItem(JobItem source, int jobId, int companyId, DateTime createdAtUtc);
|
||||||
|
IReadOnlyList<JobItemCoat> CreateJobItemCoats(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc);
|
||||||
|
IReadOnlyList<JobItemPrepService> CreateJobItemPrepServices(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc);
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ public interface INotificationService
|
|||||||
/// Notify customer when an invoice has been sent.
|
/// Notify customer when an invoice has been sent.
|
||||||
/// Optionally includes an online payment link in the email body.
|
/// Optionally includes an online payment link in the email body.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task NotifyInvoiceSentAsync(Invoice invoice, byte[]? pdfAttachment = null, string? pdfFilename = null, string? paymentUrl = null, string? overrideEmail = null);
|
Task NotifyInvoiceSentAsync(Invoice invoice, byte[]? pdfAttachment = null, string? pdfFilename = null, string? paymentUrl = null, string? overrideEmail = null, bool sendSms = false, string? viewUrl = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Notify customer (internal) when a payment has been recorded on an invoice.
|
/// Notify customer (internal) when a payment has been recorded on an invoice.
|
||||||
|
|||||||
@@ -42,10 +42,19 @@ public interface IPdfService
|
|||||||
Task<byte[]> GenerateArAgingPdfAsync(ArAgingReportDto dto);
|
Task<byte[]> GenerateArAgingPdfAsync(ArAgingReportDto dto);
|
||||||
Task<byte[]> GenerateSalesAndIncomePdfAsync(SalesIncomeReportDto dto);
|
Task<byte[]> GenerateSalesAndIncomePdfAsync(SalesIncomeReportDto dto);
|
||||||
Task<byte[]> GenerateSalesTaxReportPdfAsync(SalesTaxReportDto dto);
|
Task<byte[]> GenerateSalesTaxReportPdfAsync(SalesTaxReportDto dto);
|
||||||
|
Task<byte[]> GenerateApAgingPdfAsync(ApAgingReportDto dto);
|
||||||
|
Task<byte[]> GenerateTrialBalancePdfAsync(TrialBalanceDto dto);
|
||||||
|
Task<byte[]> GenerateCashFlowStatementPdfAsync(CashFlowStatementDto dto);
|
||||||
|
|
||||||
Task<byte[]> GenerateGiftCertificatePdfAsync(
|
Task<byte[]> GenerateGiftCertificatePdfAsync(
|
||||||
GiftCertificateDto cert,
|
GiftCertificateDto cert,
|
||||||
byte[]? companyLogo,
|
byte[]? companyLogo,
|
||||||
string? companyLogoContentType,
|
string? companyLogoContentType,
|
||||||
CompanyInfoDto companyInfo);
|
CompanyInfoDto companyInfo);
|
||||||
|
|
||||||
|
Task<byte[]> GenerateBulkGiftCertificatePdfAsync(
|
||||||
|
IList<GiftCertificateDto> certs,
|
||||||
|
byte[]? companyLogo,
|
||||||
|
string? companyLogoContentType,
|
||||||
|
CompanyInfoDto companyInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using PowderCoating.Application.DTOs.Quote;
|
||||||
|
using PowderCoating.Core.Entities;
|
||||||
|
|
||||||
|
namespace PowderCoating.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface IQuotePricingAssemblyService
|
||||||
|
{
|
||||||
|
void ApplyPricingSnapshot(Quote quote, QuotePricingResult pricingResult);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<QuoteItem>> CreateQuoteItemsAsync(
|
||||||
|
IEnumerable<CreateQuoteItemDto> itemDtos,
|
||||||
|
int quoteId,
|
||||||
|
int companyId,
|
||||||
|
decimal? ovenRateOverride,
|
||||||
|
DateTime createdAtUtc);
|
||||||
|
}
|
||||||
@@ -54,5 +54,6 @@ public class CompanyProfile : Profile
|
|||||||
CreateMap<UpdateQuoteTemplateDto, CompanyPreferences>();
|
CreateMap<UpdateQuoteTemplateDto, CompanyPreferences>();
|
||||||
CreateMap<UpdateInvoiceTemplateDto, CompanyPreferences>();
|
CreateMap<UpdateInvoiceTemplateDto, CompanyPreferences>();
|
||||||
CreateMap<UpdateWorkOrderTemplateDto, CompanyPreferences>();
|
CreateMap<UpdateWorkOrderTemplateDto, CompanyPreferences>();
|
||||||
|
CreateMap<UpdateKioskSettingsDto, CompanyPreferences>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ public class InvoiceProfile : Profile
|
|||||||
? (s.Customer.BillingEmail ?? s.Customer.Email)
|
? (s.Customer.BillingEmail ?? s.Customer.Email)
|
||||||
: null))
|
: null))
|
||||||
.ForMember(d => d.CustomerPhone, o => o.MapFrom(s => s.Customer != null ? s.Customer.Phone : null))
|
.ForMember(d => d.CustomerPhone, o => o.MapFrom(s => s.Customer != null ? s.Customer.Phone : null))
|
||||||
|
.ForMember(d => d.CustomerMobilePhone, o => o.MapFrom(s => s.Customer != null ? s.Customer.MobilePhone : null))
|
||||||
.ForMember(d => d.CustomerNotifyByEmail, o => o.MapFrom(s => s.Customer == null || s.Customer.NotifyByEmail))
|
.ForMember(d => d.CustomerNotifyByEmail, o => o.MapFrom(s => s.Customer == null || s.Customer.NotifyByEmail))
|
||||||
|
.ForMember(d => d.CustomerNotifyBySms, o => o.MapFrom(s => s.Customer != null && s.Customer.NotifyBySms))
|
||||||
.ForMember(d => d.PreparedByName, o => o.MapFrom(s => s.PreparedBy != null
|
.ForMember(d => d.PreparedByName, o => o.MapFrom(s => s.PreparedBy != null
|
||||||
? $"{s.PreparedBy.FirstName} {s.PreparedBy.LastName}".Trim()
|
? $"{s.PreparedBy.FirstName} {s.PreparedBy.LastName}".Trim()
|
||||||
: null))
|
: null))
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace PowderCoating.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared file validation and content-type resolution used across all blob storage services.
|
||||||
|
/// </summary>
|
||||||
|
public static class BlobFileHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates an uploaded file against an extension allowlist and a maximum size.
|
||||||
|
/// Returns the normalized (lowercase) extension on success so callers do not re-derive it.
|
||||||
|
/// </summary>
|
||||||
|
public static (bool IsValid, string Extension, string Error) ValidateUpload(
|
||||||
|
IFormFile? file,
|
||||||
|
string[] allowedExtensions,
|
||||||
|
long maxBytes)
|
||||||
|
{
|
||||||
|
if (file == null || file.Length == 0)
|
||||||
|
return (false, string.Empty, "No file provided.");
|
||||||
|
|
||||||
|
if (file.Length > maxBytes)
|
||||||
|
return (false, string.Empty, $"File exceeds the {maxBytes / 1024 / 1024} MB limit.");
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||||
|
if (string.IsNullOrEmpty(extension) || !allowedExtensions.Contains(extension))
|
||||||
|
return (false, string.Empty, $"File type not allowed. Allowed: {string.Join(", ", allowedExtensions)}.");
|
||||||
|
|
||||||
|
return (true, extension, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a file extension to its MIME content type, covering common image formats and
|
||||||
|
/// document types. Falls back to <c>application/octet-stream</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetContentType(string extension) => extension switch
|
||||||
|
{
|
||||||
|
".jpg" or ".jpeg" => "image/jpeg",
|
||||||
|
".png" => "image/png",
|
||||||
|
".gif" => "image/gif",
|
||||||
|
".webp" => "image/webp",
|
||||||
|
".svg" => "image/svg+xml",
|
||||||
|
".pdf" => "application/pdf",
|
||||||
|
".doc" => "application/msword",
|
||||||
|
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
".txt" => "text/plain",
|
||||||
|
_ => "application/octet-stream"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strips OS-invalid filename characters from a base filename (no extension), replacing
|
||||||
|
/// them with underscores to produce a safe blob path segment.
|
||||||
|
/// </summary>
|
||||||
|
public static string SanitizeFileName(string fileName)
|
||||||
|
{
|
||||||
|
var sanitized = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars()));
|
||||||
|
return string.IsNullOrWhiteSpace(sanitized) ? "file" : sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,15 +47,9 @@ public class CatalogImageService : ICatalogImageService
|
|||||||
string? existingImagePath,
|
string? existingImagePath,
|
||||||
string? existingThumbnailPath)
|
string? existingThumbnailPath)
|
||||||
{
|
{
|
||||||
if (file == null || file.Length == 0)
|
var (isValid, ext, validationError) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSizeBytes);
|
||||||
return (false, string.Empty, string.Empty, "No file provided.");
|
if (!isValid)
|
||||||
|
return (false, string.Empty, string.Empty, validationError);
|
||||||
if (file.Length > MaxFileSizeBytes)
|
|
||||||
return (false, string.Empty, string.Empty, "File exceeds the 10 MB limit.");
|
|
||||||
|
|
||||||
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
|
|
||||||
if (!AllowedExtensions.Contains(ext))
|
|
||||||
return (false, string.Empty, string.Empty, $"File type '{ext}' is not allowed. Accepted types: jpg, jpeg, png, gif, webp.");
|
|
||||||
|
|
||||||
var container = _settings.Containers.CatalogImages;
|
var container = _settings.Containers.CatalogImages;
|
||||||
var blobId = Guid.NewGuid().ToString("N");
|
var blobId = Guid.NewGuid().ToString("N");
|
||||||
|
|||||||
@@ -67,21 +67,15 @@ public class CompanyLogoService : ICompanyLogoService
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveCompanyLogoAsync(IFormFile file, int companyId)
|
public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveCompanyLogoAsync(IFormFile file, int companyId)
|
||||||
{
|
{
|
||||||
if (file == null || file.Length == 0)
|
var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSize);
|
||||||
return (false, string.Empty, "No file provided");
|
if (!isValid)
|
||||||
|
return (false, string.Empty, error);
|
||||||
if (file.Length > MaxFileSize)
|
|
||||||
return (false, string.Empty, $"File size exceeds maximum allowed size of {MaxFileSize / 1024 / 1024} MB");
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
|
||||||
if (!AllowedExtensions.Contains(extension))
|
|
||||||
return (false, string.Empty, $"File type not allowed. Allowed types: {string.Join(", ", AllowedExtensions)}");
|
|
||||||
|
|
||||||
// Delete old logo (any extension) before saving new one
|
// Delete old logo (any extension) before saving new one
|
||||||
await DeleteOldLogosAsync(companyId, extension);
|
await DeleteOldLogosAsync(companyId, extension);
|
||||||
|
|
||||||
var blobName = GetCompanyLogoPath(companyId, extension);
|
var blobName = GetCompanyLogoPath(companyId, extension);
|
||||||
var contentType = GetContentType(extension);
|
var contentType = BlobFileHelper.GetContentType(extension);
|
||||||
|
|
||||||
using var stream = file.OpenReadStream();
|
using var stream = file.OpenReadStream();
|
||||||
var result = await _blobService.UploadAsync(_settings.Containers.CompanyLogos, blobName, stream, contentType);
|
var result = await _blobService.UploadAsync(_settings.Containers.CompanyLogos, blobName, stream, contentType);
|
||||||
@@ -158,20 +152,4 @@ public class CompanyLogoService : ICompanyLogoService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maps a lowercase file extension to its canonical MIME content type.
|
|
||||||
/// The correct content type is required so that browsers render the image
|
|
||||||
/// inline rather than triggering a download.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="extension">Lowercase file extension including the leading dot.</param>
|
|
||||||
/// <returns>MIME type string, or <c>application/octet-stream</c> as a safe fallback.</returns>
|
|
||||||
private static string GetContentType(string extension) => extension switch
|
|
||||||
{
|
|
||||||
".jpg" or ".jpeg" => "image/jpeg",
|
|
||||||
".png" => "image/png",
|
|
||||||
".gif" => "image/gif",
|
|
||||||
".webp" => "image/webp",
|
|
||||||
".svg" => "image/svg+xml",
|
|
||||||
_ => "application/octet-stream"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,25 +56,16 @@ public class EquipmentManualService : IEquipmentManualService
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveEquipmentManualAsync(IFormFile file, int companyId, int equipmentId)
|
public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveEquipmentManualAsync(IFormFile file, int companyId, int equipmentId)
|
||||||
{
|
{
|
||||||
if (file == null || file.Length == 0)
|
var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSize);
|
||||||
return (false, string.Empty, "No file provided");
|
if (!isValid)
|
||||||
|
return (false, string.Empty, error);
|
||||||
if (file.Length > MaxFileSize)
|
|
||||||
return (false, string.Empty, $"File size exceeds maximum allowed size of {MaxFileSize / 1024 / 1024} MB");
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
|
||||||
if (!AllowedExtensions.Contains(extension))
|
|
||||||
return (false, string.Empty, $"File type not allowed. Allowed types: {string.Join(", ", AllowedExtensions)}");
|
|
||||||
|
|
||||||
// Sanitize filename — replace OS-invalid characters with underscores to
|
// Sanitize filename — replace OS-invalid characters with underscores to
|
||||||
// prevent path traversal and blob naming errors in Azure.
|
// prevent path traversal and blob naming errors in Azure.
|
||||||
var fileName = Path.GetFileNameWithoutExtension(file.FileName);
|
var fileName = BlobFileHelper.SanitizeFileName(Path.GetFileNameWithoutExtension(file.FileName));
|
||||||
fileName = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars()));
|
|
||||||
if (string.IsNullOrWhiteSpace(fileName))
|
|
||||||
fileName = "manual";
|
|
||||||
|
|
||||||
var blobName = $"{companyId}/equipment-manuals/{equipmentId}/{fileName}{extension}";
|
var blobName = $"{companyId}/equipment-manuals/{equipmentId}/{fileName}{extension}";
|
||||||
var contentType = GetContentType(extension);
|
var contentType = BlobFileHelper.GetContentType(extension);
|
||||||
|
|
||||||
using var stream = file.OpenReadStream();
|
using var stream = file.OpenReadStream();
|
||||||
var result = await _blobService.UploadAsync(_settings.Containers.Manuals, blobName, stream, contentType);
|
var result = await _blobService.UploadAsync(_settings.Containers.Manuals, blobName, stream, contentType);
|
||||||
@@ -130,19 +121,4 @@ public class EquipmentManualService : IEquipmentManualService
|
|||||||
return await _blobService.ExistsAsync(_settings.Containers.Manuals, filePath);
|
return await _blobService.ExistsAsync(_settings.Containers.Manuals, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maps a lowercase file extension to its canonical MIME content type.
|
|
||||||
/// Correct MIME types are required so browsers open PDFs inline and
|
|
||||||
/// Word documents prompt a compatible application rather than a raw download.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="extension">Lowercase file extension including the leading dot.</param>
|
|
||||||
/// <returns>MIME type string, or <c>application/octet-stream</c> as a safe fallback.</returns>
|
|
||||||
private static string GetContentType(string extension) => extension switch
|
|
||||||
{
|
|
||||||
".pdf" => "application/pdf",
|
|
||||||
".doc" => "application/msword",
|
|
||||||
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
||||||
".txt" => "text/plain",
|
|
||||||
_ => "application/octet-stream"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
using PowderCoating.Application.DTOs.Quote;
|
||||||
|
using PowderCoating.Application.Interfaces;
|
||||||
|
using PowderCoating.Core.Entities;
|
||||||
|
|
||||||
|
namespace PowderCoating.Application.Services;
|
||||||
|
|
||||||
|
public class JobItemAssemblyService : IJobItemAssemblyService
|
||||||
|
{
|
||||||
|
public JobItem CreateJobItem(CreateQuoteItemDto source, int jobId, int companyId, QuoteItemPricingResult pricing, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ArgumentNullException.ThrowIfNull(pricing);
|
||||||
|
|
||||||
|
return BuildJobItem(
|
||||||
|
new JobItemSeed
|
||||||
|
{
|
||||||
|
Description = source.Description,
|
||||||
|
Quantity = source.Quantity,
|
||||||
|
SurfaceAreaSqFt = source.SurfaceAreaSqFt,
|
||||||
|
CatalogItemId = source.CatalogItemId,
|
||||||
|
IsGenericItem = source.IsGenericItem,
|
||||||
|
IsLaborItem = source.IsLaborItem,
|
||||||
|
IsSalesItem = source.IsSalesItem,
|
||||||
|
IsAiItem = source.IsAiItem,
|
||||||
|
Sku = source.Sku,
|
||||||
|
ManualUnitPrice = source.ManualUnitPrice,
|
||||||
|
PowderCostOverride = source.PowderCostOverride,
|
||||||
|
UnitPrice = pricing.UnitPrice,
|
||||||
|
TotalPrice = pricing.TotalPrice,
|
||||||
|
LaborCost = pricing.LaborCost,
|
||||||
|
RequiresSandblasting = source.RequiresSandblasting,
|
||||||
|
RequiresMasking = source.RequiresMasking,
|
||||||
|
EstimatedMinutes = source.EstimatedMinutes,
|
||||||
|
Notes = source.Notes,
|
||||||
|
IncludePrepCost = source.IncludePrepCost,
|
||||||
|
Complexity = source.Complexity,
|
||||||
|
AiTags = source.AiTags,
|
||||||
|
AiPredictionId = source.AiPredictionId
|
||||||
|
},
|
||||||
|
jobId,
|
||||||
|
companyId,
|
||||||
|
createdAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<JobItemCoat> CreateJobItemCoats(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
return source.Coats?
|
||||||
|
.OrderBy(c => c.Sequence)
|
||||||
|
.Select(c => BuildJobItemCoat(
|
||||||
|
new JobItemCoatSeed
|
||||||
|
{
|
||||||
|
CoatName = c.CoatName,
|
||||||
|
Sequence = c.Sequence,
|
||||||
|
InventoryItemId = c.InventoryItemId,
|
||||||
|
ColorName = c.ColorName,
|
||||||
|
VendorId = c.VendorId,
|
||||||
|
ColorCode = c.ColorCode,
|
||||||
|
Finish = c.Finish,
|
||||||
|
CoverageSqFtPerLb = c.CoverageSqFtPerLb,
|
||||||
|
TransferEfficiency = c.TransferEfficiency,
|
||||||
|
PowderCostPerLb = c.PowderCostPerLb,
|
||||||
|
PowderToOrder = CalculatePowderToOrder(c.PowderToOrder, source.SurfaceAreaSqFt, source.Quantity, c.CoverageSqFtPerLb, c.TransferEfficiency),
|
||||||
|
Notes = c.Notes
|
||||||
|
},
|
||||||
|
jobItemId,
|
||||||
|
companyId,
|
||||||
|
createdAtUtc))
|
||||||
|
.ToList() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<JobItemPrepService> CreateJobItemPrepServices(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
return BuildJobItemPrepServices(
|
||||||
|
source.PrepServices?.Select(p => new JobItemPrepServiceSeed
|
||||||
|
{
|
||||||
|
PrepServiceId = p.PrepServiceId,
|
||||||
|
EstimatedMinutes = p.EstimatedMinutes,
|
||||||
|
BlastSetupId = p.BlastSetupId
|
||||||
|
}),
|
||||||
|
jobItemId,
|
||||||
|
companyId,
|
||||||
|
createdAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JobItem CreateJobItem(QuoteItem source, int jobId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
var firstCoat = source.Coats?
|
||||||
|
.OrderBy(c => c.Sequence)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
return BuildJobItem(
|
||||||
|
new JobItemSeed
|
||||||
|
{
|
||||||
|
Description = source.Description,
|
||||||
|
Quantity = source.Quantity,
|
||||||
|
ColorName = firstCoat?.ColorName,
|
||||||
|
ColorCode = firstCoat?.ColorCode,
|
||||||
|
Finish = firstCoat?.Finish,
|
||||||
|
SurfaceArea = source.SurfaceAreaSqFt,
|
||||||
|
SurfaceAreaSqFt = source.SurfaceAreaSqFt,
|
||||||
|
CatalogItemId = source.CatalogItemId,
|
||||||
|
IsGenericItem = source.IsGenericItem,
|
||||||
|
IsLaborItem = source.IsLaborItem,
|
||||||
|
IsSalesItem = source.IsSalesItem,
|
||||||
|
IsAiItem = source.IsAiItem,
|
||||||
|
Sku = source.Sku,
|
||||||
|
ManualUnitPrice = source.ManualUnitPrice,
|
||||||
|
PowderCostOverride = source.PowderCostOverride,
|
||||||
|
UnitPrice = source.UnitPrice,
|
||||||
|
TotalPrice = source.TotalPrice,
|
||||||
|
LaborCost = source.ItemLaborCost,
|
||||||
|
RequiresSandblasting = source.RequiresSandblasting,
|
||||||
|
RequiresMasking = source.RequiresMasking,
|
||||||
|
EstimatedMinutes = source.EstimatedMinutes,
|
||||||
|
Notes = source.Notes,
|
||||||
|
IncludePrepCost = source.IncludePrepCost,
|
||||||
|
Complexity = source.Complexity,
|
||||||
|
AiTags = source.AiTags,
|
||||||
|
AiPredictionId = source.AiPredictionId
|
||||||
|
},
|
||||||
|
jobId,
|
||||||
|
companyId,
|
||||||
|
createdAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<JobItemCoat> CreateJobItemCoats(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
return source.Coats?
|
||||||
|
.OrderBy(c => c.Sequence)
|
||||||
|
.Select(c =>
|
||||||
|
{
|
||||||
|
var appearance = ResolveCoatAppearance(c.ColorName, c.ColorCode, c.Finish, c.InventoryItem);
|
||||||
|
return BuildJobItemCoat(
|
||||||
|
new JobItemCoatSeed
|
||||||
|
{
|
||||||
|
CoatName = c.CoatName,
|
||||||
|
Sequence = c.Sequence,
|
||||||
|
InventoryItemId = c.InventoryItemId,
|
||||||
|
ColorName = appearance.ColorName,
|
||||||
|
VendorId = c.VendorId,
|
||||||
|
ColorCode = appearance.ColorCode,
|
||||||
|
Finish = appearance.Finish,
|
||||||
|
CoverageSqFtPerLb = c.CoverageSqFtPerLb,
|
||||||
|
TransferEfficiency = c.TransferEfficiency,
|
||||||
|
PowderCostPerLb = c.PowderCostPerLb,
|
||||||
|
PowderToOrder = CalculatePowderToOrder(c.PowderToOrder, source.SurfaceAreaSqFt, source.Quantity, c.CoverageSqFtPerLb, c.TransferEfficiency),
|
||||||
|
Notes = c.Notes
|
||||||
|
},
|
||||||
|
jobItemId,
|
||||||
|
companyId,
|
||||||
|
createdAtUtc);
|
||||||
|
})
|
||||||
|
.ToList() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<JobItemPrepService> CreateJobItemPrepServices(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
return BuildJobItemPrepServices(
|
||||||
|
source.PrepServices?.Select(p => new JobItemPrepServiceSeed
|
||||||
|
{
|
||||||
|
PrepServiceId = p.PrepServiceId,
|
||||||
|
EstimatedMinutes = p.EstimatedMinutes,
|
||||||
|
BlastSetupId = p.BlastSetupId
|
||||||
|
}),
|
||||||
|
jobItemId,
|
||||||
|
companyId,
|
||||||
|
createdAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JobItem CreateJobItem(JobItem source, int jobId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
return BuildJobItem(
|
||||||
|
new JobItemSeed
|
||||||
|
{
|
||||||
|
Description = source.Description,
|
||||||
|
Quantity = source.Quantity,
|
||||||
|
ColorName = source.ColorName,
|
||||||
|
ColorCode = source.ColorCode,
|
||||||
|
Finish = source.Finish,
|
||||||
|
SurfaceArea = source.SurfaceArea,
|
||||||
|
SurfaceAreaSqFt = source.SurfaceAreaSqFt,
|
||||||
|
CatalogItemId = source.CatalogItemId,
|
||||||
|
IsGenericItem = source.IsGenericItem,
|
||||||
|
IsLaborItem = source.IsLaborItem,
|
||||||
|
IsSalesItem = source.IsSalesItem,
|
||||||
|
IsAiItem = source.IsAiItem,
|
||||||
|
Sku = source.Sku,
|
||||||
|
ManualUnitPrice = source.ManualUnitPrice,
|
||||||
|
PowderCostOverride = source.PowderCostOverride,
|
||||||
|
UnitPrice = source.UnitPrice,
|
||||||
|
TotalPrice = source.TotalPrice,
|
||||||
|
LaborCost = source.LaborCost,
|
||||||
|
RequiresSandblasting = source.RequiresSandblasting,
|
||||||
|
RequiresMasking = source.RequiresMasking,
|
||||||
|
EstimatedMinutes = source.EstimatedMinutes,
|
||||||
|
Notes = source.Notes,
|
||||||
|
IncludePrepCost = source.IncludePrepCost,
|
||||||
|
Complexity = source.Complexity,
|
||||||
|
AiTags = source.AiTags,
|
||||||
|
AiPredictionId = source.AiPredictionId
|
||||||
|
},
|
||||||
|
jobId,
|
||||||
|
companyId,
|
||||||
|
createdAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<JobItemCoat> CreateJobItemCoats(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
return source.Coats?
|
||||||
|
.OrderBy(c => c.Sequence)
|
||||||
|
.Select(c => BuildJobItemCoat(
|
||||||
|
new JobItemCoatSeed
|
||||||
|
{
|
||||||
|
CoatName = c.CoatName,
|
||||||
|
Sequence = c.Sequence,
|
||||||
|
InventoryItemId = c.InventoryItemId,
|
||||||
|
ColorName = c.ColorName,
|
||||||
|
VendorId = c.VendorId,
|
||||||
|
ColorCode = c.ColorCode,
|
||||||
|
Finish = c.Finish,
|
||||||
|
CoverageSqFtPerLb = c.CoverageSqFtPerLb,
|
||||||
|
TransferEfficiency = c.TransferEfficiency,
|
||||||
|
PowderCostPerLb = c.PowderCostPerLb,
|
||||||
|
PowderToOrder = c.PowderToOrder,
|
||||||
|
Notes = c.Notes
|
||||||
|
},
|
||||||
|
jobItemId,
|
||||||
|
companyId,
|
||||||
|
createdAtUtc))
|
||||||
|
.ToList() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<JobItemPrepService> CreateJobItemPrepServices(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
return BuildJobItemPrepServices(
|
||||||
|
source.PrepServices?.Select(p => new JobItemPrepServiceSeed
|
||||||
|
{
|
||||||
|
PrepServiceId = p.PrepServiceId,
|
||||||
|
EstimatedMinutes = p.EstimatedMinutes,
|
||||||
|
BlastSetupId = p.BlastSetupId
|
||||||
|
}),
|
||||||
|
jobItemId,
|
||||||
|
companyId,
|
||||||
|
createdAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JobItem BuildJobItem(JobItemSeed seed, int jobId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
return new JobItem
|
||||||
|
{
|
||||||
|
JobId = jobId,
|
||||||
|
Description = seed.Description,
|
||||||
|
Quantity = seed.Quantity,
|
||||||
|
ColorName = seed.ColorName,
|
||||||
|
ColorCode = seed.ColorCode,
|
||||||
|
Finish = seed.Finish,
|
||||||
|
SurfaceArea = seed.SurfaceArea,
|
||||||
|
SurfaceAreaSqFt = seed.SurfaceAreaSqFt,
|
||||||
|
CatalogItemId = seed.CatalogItemId,
|
||||||
|
IsGenericItem = seed.IsGenericItem,
|
||||||
|
IsLaborItem = seed.IsLaborItem,
|
||||||
|
IsSalesItem = seed.IsSalesItem,
|
||||||
|
IsAiItem = seed.IsAiItem,
|
||||||
|
Sku = seed.Sku,
|
||||||
|
ManualUnitPrice = seed.ManualUnitPrice,
|
||||||
|
PowderCostOverride = seed.PowderCostOverride,
|
||||||
|
UnitPrice = seed.UnitPrice,
|
||||||
|
TotalPrice = seed.TotalPrice,
|
||||||
|
LaborCost = seed.LaborCost,
|
||||||
|
RequiresSandblasting = seed.RequiresSandblasting,
|
||||||
|
RequiresMasking = seed.RequiresMasking,
|
||||||
|
EstimatedMinutes = seed.EstimatedMinutes,
|
||||||
|
Notes = seed.Notes,
|
||||||
|
IncludePrepCost = seed.IncludePrepCost,
|
||||||
|
Complexity = seed.Complexity,
|
||||||
|
AiTags = seed.AiTags,
|
||||||
|
AiPredictionId = seed.AiPredictionId,
|
||||||
|
CompanyId = companyId,
|
||||||
|
CreatedAt = createdAtUtc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JobItemCoat BuildJobItemCoat(JobItemCoatSeed seed, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
return new JobItemCoat
|
||||||
|
{
|
||||||
|
JobItemId = jobItemId,
|
||||||
|
CoatName = seed.CoatName,
|
||||||
|
Sequence = seed.Sequence,
|
||||||
|
InventoryItemId = seed.InventoryItemId,
|
||||||
|
ColorName = seed.ColorName,
|
||||||
|
VendorId = seed.VendorId,
|
||||||
|
ColorCode = seed.ColorCode,
|
||||||
|
Finish = seed.Finish,
|
||||||
|
CoverageSqFtPerLb = seed.CoverageSqFtPerLb,
|
||||||
|
TransferEfficiency = seed.TransferEfficiency,
|
||||||
|
PowderCostPerLb = seed.PowderCostPerLb,
|
||||||
|
PowderToOrder = seed.PowderToOrder,
|
||||||
|
Notes = seed.Notes,
|
||||||
|
CompanyId = companyId,
|
||||||
|
CreatedAt = createdAtUtc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<JobItemPrepService> BuildJobItemPrepServices(IEnumerable<JobItemPrepServiceSeed>? seeds, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
return seeds?
|
||||||
|
.Select(seed => new JobItemPrepService
|
||||||
|
{
|
||||||
|
JobItemId = jobItemId,
|
||||||
|
PrepServiceId = seed.PrepServiceId,
|
||||||
|
EstimatedMinutes = seed.EstimatedMinutes,
|
||||||
|
BlastSetupId = seed.BlastSetupId,
|
||||||
|
CompanyId = companyId,
|
||||||
|
CreatedAt = createdAtUtc
|
||||||
|
})
|
||||||
|
.ToList() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal? CalculatePowderToOrder(decimal? storedPowderToOrder, decimal surfaceAreaSqFt, decimal quantity, decimal coverageSqFtPerLb, decimal transferEfficiency)
|
||||||
|
{
|
||||||
|
if (storedPowderToOrder.HasValue && storedPowderToOrder.Value > 0)
|
||||||
|
return storedPowderToOrder;
|
||||||
|
|
||||||
|
if (surfaceAreaSqFt <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var coverage = coverageSqFtPerLb > 0 ? coverageSqFtPerLb : 30m;
|
||||||
|
var efficiency = transferEfficiency > 0 ? transferEfficiency / 100m : 0.65m;
|
||||||
|
return Math.Round((surfaceAreaSqFt * quantity) / (coverage * efficiency), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string? ColorName, string? ColorCode, string? Finish) ResolveCoatAppearance(
|
||||||
|
string? colorName,
|
||||||
|
string? colorCode,
|
||||||
|
string? finish,
|
||||||
|
InventoryItem? inventoryItem)
|
||||||
|
{
|
||||||
|
if (inventoryItem == null)
|
||||||
|
return (colorName, colorCode, finish);
|
||||||
|
|
||||||
|
return (inventoryItem.Name, inventoryItem.ColorCode, inventoryItem.Finish);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class JobItemSeed
|
||||||
|
{
|
||||||
|
public string Description { get; init; } = string.Empty;
|
||||||
|
public decimal Quantity { get; init; }
|
||||||
|
public string? ColorName { get; init; }
|
||||||
|
public string? ColorCode { get; init; }
|
||||||
|
public string? Finish { get; init; }
|
||||||
|
public decimal? SurfaceArea { get; init; }
|
||||||
|
public decimal SurfaceAreaSqFt { get; init; }
|
||||||
|
public int? CatalogItemId { get; init; }
|
||||||
|
public bool IsGenericItem { get; init; }
|
||||||
|
public bool IsLaborItem { get; init; }
|
||||||
|
public bool IsSalesItem { get; init; }
|
||||||
|
public bool IsAiItem { get; init; }
|
||||||
|
public string? Sku { get; init; }
|
||||||
|
public decimal? ManualUnitPrice { get; init; }
|
||||||
|
public decimal? PowderCostOverride { get; init; }
|
||||||
|
public decimal UnitPrice { get; init; }
|
||||||
|
public decimal TotalPrice { get; init; }
|
||||||
|
public decimal LaborCost { get; init; }
|
||||||
|
public bool RequiresSandblasting { get; init; }
|
||||||
|
public bool RequiresMasking { get; init; }
|
||||||
|
public int EstimatedMinutes { get; init; }
|
||||||
|
public string? Notes { get; init; }
|
||||||
|
public bool IncludePrepCost { get; init; }
|
||||||
|
public string? Complexity { get; init; }
|
||||||
|
public string? AiTags { get; init; }
|
||||||
|
public int? AiPredictionId { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class JobItemCoatSeed
|
||||||
|
{
|
||||||
|
public string CoatName { get; init; } = string.Empty;
|
||||||
|
public int Sequence { get; init; }
|
||||||
|
public int? InventoryItemId { get; init; }
|
||||||
|
public string? ColorName { get; init; }
|
||||||
|
public int? VendorId { get; init; }
|
||||||
|
public string? ColorCode { get; init; }
|
||||||
|
public string? Finish { get; init; }
|
||||||
|
public decimal CoverageSqFtPerLb { get; init; }
|
||||||
|
public decimal TransferEfficiency { get; init; }
|
||||||
|
public decimal? PowderCostPerLb { get; init; }
|
||||||
|
public decimal? PowderToOrder { get; init; }
|
||||||
|
public string? Notes { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class JobItemPrepServiceSeed
|
||||||
|
{
|
||||||
|
public int PrepServiceId { get; init; }
|
||||||
|
public int EstimatedMinutes { get; init; }
|
||||||
|
public int? BlastSetupId { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,19 +69,13 @@ public class JobPhotoService : IJobPhotoService
|
|||||||
string? caption = null,
|
string? caption = null,
|
||||||
JobPhotoType photoType = JobPhotoType.Progress)
|
JobPhotoType photoType = JobPhotoType.Progress)
|
||||||
{
|
{
|
||||||
if (file == null || file.Length == 0)
|
var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedImageTypes, MaxPhotoSize);
|
||||||
return (false, string.Empty, "No file was uploaded.");
|
if (!isValid)
|
||||||
|
return (false, string.Empty, error);
|
||||||
if (file.Length > MaxPhotoSize)
|
|
||||||
return (false, string.Empty, "Photo must be smaller than 10 MB.");
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
|
||||||
if (string.IsNullOrEmpty(extension) || !AllowedImageTypes.Contains(extension))
|
|
||||||
return (false, string.Empty, "Only JPG, PNG, GIF, and WebP images are allowed.");
|
|
||||||
|
|
||||||
// SECURITY: Use GUID for blob name to prevent enumeration
|
// SECURITY: Use GUID for blob name to prevent enumeration
|
||||||
var blobName = $"{companyId}/job-photos/{jobId}/{Guid.NewGuid()}{extension}";
|
var blobName = $"{companyId}/job-photos/{jobId}/{Guid.NewGuid()}{extension}";
|
||||||
var contentType = GetContentType(extension);
|
var contentType = BlobFileHelper.GetContentType(extension);
|
||||||
|
|
||||||
using var stream = file.OpenReadStream();
|
using var stream = file.OpenReadStream();
|
||||||
var result = await _blobService.UploadAsync(_settings.Containers.JobImages, blobName, stream, contentType);
|
var result = await _blobService.UploadAsync(_settings.Containers.JobImages, blobName, stream, contentType);
|
||||||
@@ -137,19 +131,4 @@ public class JobPhotoService : IJobPhotoService
|
|||||||
return await _blobService.ExistsAsync(_settings.Containers.JobImages, filePath);
|
return await _blobService.ExistsAsync(_settings.Containers.JobImages, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maps a lowercase file extension to its canonical MIME content type.
|
|
||||||
/// Falls back to <c>image/jpeg</c> (rather than octet-stream) because all
|
|
||||||
/// allowed extensions are image types and browsers will render them correctly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="extension">Lowercase file extension including the leading dot.</param>
|
|
||||||
/// <returns>MIME type string.</returns>
|
|
||||||
private static string GetContentType(string extension) => extension switch
|
|
||||||
{
|
|
||||||
".jpg" or ".jpeg" => "image/jpeg",
|
|
||||||
".png" => "image/png",
|
|
||||||
".gif" => "image/gif",
|
|
||||||
".webp" => "image/webp",
|
|
||||||
_ => "image/jpeg"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1858,6 +1858,50 @@ public class PdfService : IPdfService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a multi-page PDF containing one gift certificate per page, all using the same
|
||||||
|
/// branded layout as the single-certificate download. Used for bulk print runs (car shows,
|
||||||
|
/// promotions) so staff can hand-cut and distribute a full batch from one print job.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]> GenerateBulkGiftCertificatePdfAsync(
|
||||||
|
IList<GiftCertificateDto> certs,
|
||||||
|
byte[]? companyLogo,
|
||||||
|
string? companyLogoContentType,
|
||||||
|
CompanyInfoDto companyInfo)
|
||||||
|
{
|
||||||
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
|
const string accent = "#7c3aed";
|
||||||
|
const string gold = "#b45309";
|
||||||
|
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var doc = Document.Create(container =>
|
||||||
|
{
|
||||||
|
foreach (var cert in certs)
|
||||||
|
{
|
||||||
|
container.Page(page =>
|
||||||
|
{
|
||||||
|
page.Size(PageSizes.Letter);
|
||||||
|
page.Margin(0.75f, Unit.Inch);
|
||||||
|
page.PageColor(Colors.White);
|
||||||
|
page.DefaultTextStyle(x => x.FontSize(10).FontFamily("Arial"));
|
||||||
|
|
||||||
|
page.Content().Element(c => ComposeGiftCertificateContent(c, cert, companyInfo, companyLogo, accent, gold));
|
||||||
|
|
||||||
|
page.Footer().AlignCenter().Text(text =>
|
||||||
|
{
|
||||||
|
text.Span(companyInfo.CompanyName).FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
if (!string.IsNullOrWhiteSpace(companyInfo.Phone))
|
||||||
|
text.Span($" · {FormatPhoneNumber(companyInfo.Phone)}").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.GeneratePdf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Composes the gift certificate body with a decorative double-border frame (outer purple 3pt,
|
/// Composes the gift certificate body with a decorative double-border frame (outer purple 3pt,
|
||||||
/// inner gold 1pt) that gives the document a premium printed-certificate appearance. Inside the
|
/// inner gold 1pt) that gives the document a premium printed-certificate appearance. Inside the
|
||||||
@@ -2357,4 +2401,356 @@ public class PdfService : IPdfService
|
|||||||
return document.GeneratePdf();
|
return document.GeneratePdf();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates an Accounts Payable Aging PDF. Layout mirrors GenerateArAgingPdfAsync:
|
||||||
|
/// a KPI summary band, a per-vendor summary table with aging columns, then a bill-detail
|
||||||
|
/// section grouped by vendor. Uses a red accent palette to visually distinguish AP from AR.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]> GenerateApAgingPdfAsync(ApAgingReportDto dto)
|
||||||
|
{
|
||||||
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
|
const string accent = "#b91c1c";
|
||||||
|
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var document = Document.Create(container =>
|
||||||
|
{
|
||||||
|
container.Page(page =>
|
||||||
|
{
|
||||||
|
page.Size(PageSizes.Letter);
|
||||||
|
page.Margin(0.6f, Unit.Inch);
|
||||||
|
page.PageColor(Colors.White);
|
||||||
|
page.DefaultTextStyle(x => x.FontSize(9).FontFamily("Arial"));
|
||||||
|
|
||||||
|
page.Header().Element(c => ComposeReportHeader(c, dto.CompanyName, "Accounts Payable Aging",
|
||||||
|
$"As of {dto.AsOf:MMMM d, yyyy}", accent));
|
||||||
|
|
||||||
|
page.Content().PaddingTop(12).Column(col =>
|
||||||
|
{
|
||||||
|
col.Item().Background("#f8fafc").Border(1).BorderColor("#e2e8f0").Padding(8).Row(row =>
|
||||||
|
{
|
||||||
|
KpiCell(row, "Current", dto.TotalCurrent.ToString("C0"), "#16a34a");
|
||||||
|
KpiCell(row, "1–30 Days", dto.Total1to30.ToString("C0"), "#ca8a04");
|
||||||
|
KpiCell(row, "31–60 Days", dto.Total31to60.ToString("C0"), "#ea580c");
|
||||||
|
KpiCell(row, "61–90 Days", dto.Total61to90.ToString("C0"), "#dc2626");
|
||||||
|
KpiCell(row, "Over 90", dto.TotalOver90.ToString("C0"), "#7f1d1d");
|
||||||
|
KpiCell(row, "Total Owed", dto.TotalOutstanding.ToString("C0"), accent);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dto.Vendors.Any())
|
||||||
|
{
|
||||||
|
col.Item().PaddingTop(20).AlignCenter()
|
||||||
|
.Text("All bills are paid — no outstanding balances.")
|
||||||
|
.FontSize(11).FontColor("#16a34a");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
col.Item().PaddingTop(14).Table(table =>
|
||||||
|
{
|
||||||
|
table.ColumnsDefinition(cols =>
|
||||||
|
{
|
||||||
|
cols.RelativeColumn(3);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.Header(h =>
|
||||||
|
{
|
||||||
|
foreach (var lbl in new[] { "Vendor", "Current", "1–30", "31–60", "61–90", "Over 90", "Total" })
|
||||||
|
h.Cell().Background(accent).Padding(4).Text(lbl).FontColor(Colors.White).Bold().FontSize(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
var alt = false;
|
||||||
|
foreach (var vend in dto.Vendors)
|
||||||
|
{
|
||||||
|
var bg = alt ? "#f8fafc" : "#ffffff";
|
||||||
|
table.Cell().Background(bg).Padding(4).Text(vend.VendorName).FontSize(9).Bold();
|
||||||
|
table.Cell().Background(bg).AlignRight().Padding(4).Text(vend.TotalCurrent > 0 ? vend.TotalCurrent.ToString("C") : "—").FontSize(9).FontColor("#16a34a");
|
||||||
|
table.Cell().Background(bg).AlignRight().Padding(4).Text(vend.Total1to30 > 0 ? vend.Total1to30.ToString("C") : "—").FontSize(9).FontColor("#ca8a04");
|
||||||
|
table.Cell().Background(bg).AlignRight().Padding(4).Text(vend.Total31to60 > 0 ? vend.Total31to60.ToString("C") : "—").FontSize(9).FontColor("#ea580c");
|
||||||
|
table.Cell().Background(bg).AlignRight().Padding(4).Text(vend.Total61to90 > 0 ? vend.Total61to90.ToString("C") : "—").FontSize(9).FontColor("#dc2626");
|
||||||
|
table.Cell().Background(bg).AlignRight().Padding(4).Text(vend.TotalOver90 > 0 ? vend.TotalOver90.ToString("C") : "—").FontSize(9).FontColor("#7f1d1d");
|
||||||
|
table.Cell().Background(bg).AlignRight().Padding(4).Text(vend.TotalBalance.ToString("C")).FontSize(9).Bold();
|
||||||
|
alt = !alt;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Cell().Background("#e2e8f0").Padding(4).Text("Total").FontSize(9).Bold();
|
||||||
|
table.Cell().Background("#e2e8f0").AlignRight().Padding(4).Text(dto.TotalCurrent.ToString("C")).FontSize(9).Bold().FontColor("#16a34a");
|
||||||
|
table.Cell().Background("#e2e8f0").AlignRight().Padding(4).Text(dto.Total1to30.ToString("C")).FontSize(9).Bold().FontColor("#ca8a04");
|
||||||
|
table.Cell().Background("#e2e8f0").AlignRight().Padding(4).Text(dto.Total31to60.ToString("C")).FontSize(9).Bold().FontColor("#ea580c");
|
||||||
|
table.Cell().Background("#e2e8f0").AlignRight().Padding(4).Text(dto.Total61to90.ToString("C")).FontSize(9).Bold().FontColor("#dc2626");
|
||||||
|
table.Cell().Background("#e2e8f0").AlignRight().Padding(4).Text(dto.TotalOver90.ToString("C")).FontSize(9).Bold().FontColor("#7f1d1d");
|
||||||
|
table.Cell().Background("#e2e8f0").AlignRight().Padding(4).Text(dto.TotalOutstanding.ToString("C")).FontSize(9).Bold();
|
||||||
|
});
|
||||||
|
|
||||||
|
col.Item().PaddingTop(16).Text("Bill Detail").FontSize(11).Bold();
|
||||||
|
|
||||||
|
foreach (var vend in dto.Vendors)
|
||||||
|
{
|
||||||
|
col.Item().PaddingTop(8).ShowEntire().Column(vendCol =>
|
||||||
|
{
|
||||||
|
vendCol.Item().Background("#f1f5f9").Padding(4).Text(vend.VendorName).Bold().FontSize(10);
|
||||||
|
|
||||||
|
vendCol.Item().Table(table =>
|
||||||
|
{
|
||||||
|
table.ColumnsDefinition(cols =>
|
||||||
|
{
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.Header(h =>
|
||||||
|
{
|
||||||
|
foreach (var lbl in new[] { "Bill #", "Bill Date", "Due Date", "Balance", "Age" })
|
||||||
|
h.Cell().Background("#e2e8f0").Padding(3).Text(lbl).Bold().FontSize(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var bill in vend.Bills.OrderBy(b => b.DaysOverdue))
|
||||||
|
{
|
||||||
|
var ageColor = bill.DaysOverdue <= 0 ? "#16a34a"
|
||||||
|
: bill.DaysOverdue <= 30 ? "#ca8a04"
|
||||||
|
: bill.DaysOverdue <= 60 ? "#ea580c"
|
||||||
|
: bill.DaysOverdue <= 90 ? "#dc2626"
|
||||||
|
: "#7f1d1d";
|
||||||
|
var ageLabel = bill.DaysOverdue <= 0 ? "Current" : $"{bill.DaysOverdue}d overdue";
|
||||||
|
|
||||||
|
table.Cell().Padding(3).Text(bill.BillNumber).FontSize(8);
|
||||||
|
table.Cell().Padding(3).Text(bill.BillDate.ToString("MM/dd/yyyy")).FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
table.Cell().Padding(3).Text(bill.DueDate?.ToString("MM/dd/yyyy") ?? "—").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
table.Cell().AlignRight().Padding(3).Text(bill.BalanceDue.ToString("C")).Bold().FontSize(8)
|
||||||
|
.FontColor(bill.DaysOverdue > 30 ? "#dc2626" : "#000000");
|
||||||
|
table.Cell().Padding(3).Text(ageLabel).FontSize(8).FontColor(ageColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Cell().ColumnSpan(3).Background("#f1f5f9").AlignRight().Padding(3)
|
||||||
|
.Text($"{vend.VendorName} subtotal").Bold().FontSize(8).FontColor(Colors.Grey.Darken2);
|
||||||
|
table.Cell().Background("#f1f5f9").AlignRight().Padding(3).Text(vend.TotalBalance.ToString("C")).Bold().FontSize(8);
|
||||||
|
table.Cell().Background("#f1f5f9");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page.Footer().AlignCenter().Text(text =>
|
||||||
|
{
|
||||||
|
text.CurrentPageNumber(); text.Span(" / "); text.TotalPages();
|
||||||
|
text.Span($" · {dto.CompanyName} · Generated {DateTime.Now:MMM d, yyyy}").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return document.GeneratePdf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a Trial Balance PDF. Each active account appears once with its balance in either
|
||||||
|
/// the Debit or Credit column based on AccountingRules sign conventions. A footer row shows
|
||||||
|
/// totals and a balanced/unbalanced indicator.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]> GenerateTrialBalancePdfAsync(TrialBalanceDto dto)
|
||||||
|
{
|
||||||
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
|
const string accent = "#1a56db";
|
||||||
|
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var document = Document.Create(container =>
|
||||||
|
{
|
||||||
|
container.Page(page =>
|
||||||
|
{
|
||||||
|
page.Size(PageSizes.Letter);
|
||||||
|
page.Margin(0.6f, Unit.Inch);
|
||||||
|
page.PageColor(Colors.White);
|
||||||
|
page.DefaultTextStyle(x => x.FontSize(9).FontFamily("Arial"));
|
||||||
|
|
||||||
|
page.Header().Element(c => ComposeReportHeader(c, dto.CompanyName, "Trial Balance",
|
||||||
|
$"As of {dto.AsOf:MMMM d, yyyy}", accent));
|
||||||
|
|
||||||
|
page.Content().PaddingTop(12).Column(col =>
|
||||||
|
{
|
||||||
|
col.Item().Background("#f8fafc").Border(1).BorderColor("#e2e8f0").Padding(8).Row(row =>
|
||||||
|
{
|
||||||
|
KpiCell(row, "Total Debits", dto.TotalDebits.ToString("C0"), "#1a56db");
|
||||||
|
KpiCell(row, "Total Credits", dto.TotalCredits.ToString("C0"), "#1a56db");
|
||||||
|
KpiCell(row, "Status", dto.IsBalanced ? "Balanced ✓" : "Out of Balance ✗",
|
||||||
|
dto.IsBalanced ? "#16a34a" : "#dc2626");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dto.Lines.Any())
|
||||||
|
{
|
||||||
|
col.Item().PaddingTop(20).AlignCenter()
|
||||||
|
.Text("No active accounts with balances found.")
|
||||||
|
.FontSize(11).FontColor(Colors.Grey.Darken1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
col.Item().PaddingTop(14).Table(table =>
|
||||||
|
{
|
||||||
|
table.ColumnsDefinition(cols =>
|
||||||
|
{
|
||||||
|
cols.ConstantColumn(70);
|
||||||
|
cols.RelativeColumn(4);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
cols.RelativeColumn(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.Header(h =>
|
||||||
|
{
|
||||||
|
foreach (var lbl in new[] { "Acct #", "Account Name", "Type", "Debit", "Credit" })
|
||||||
|
h.Cell().Background(accent).Padding(4).Text(lbl).FontColor(Colors.White).Bold().FontSize(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
var alt = false;
|
||||||
|
foreach (var line in dto.Lines)
|
||||||
|
{
|
||||||
|
var bg = alt ? "#f8fafc" : "#ffffff";
|
||||||
|
table.Cell().Background(bg).Padding(4).Text(line.AccountNumber).FontSize(8).FontColor(Colors.Grey.Darken2);
|
||||||
|
table.Cell().Background(bg).Padding(4).Text(line.AccountName).FontSize(9);
|
||||||
|
table.Cell().Background(bg).Padding(4).Text(line.AccountType.ToString()).FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
table.Cell().Background(bg).AlignRight().Padding(4).Text(line.DebitBalance > 0 ? line.DebitBalance.ToString("C") : "").FontSize(9);
|
||||||
|
table.Cell().Background(bg).AlignRight().Padding(4).Text(line.CreditBalance > 0 ? line.CreditBalance.ToString("C") : "").FontSize(9);
|
||||||
|
alt = !alt;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Cell().ColumnSpan(3).Background("#e2e8f0").Padding(4).Text("Total").FontSize(9).Bold();
|
||||||
|
table.Cell().Background("#e2e8f0").AlignRight().Padding(4).Text(dto.TotalDebits.ToString("C")).FontSize(9).Bold();
|
||||||
|
table.Cell().Background("#e2e8f0").AlignRight().Padding(4).Text(dto.TotalCredits.ToString("C")).FontSize(9).Bold();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
page.Footer().AlignCenter().Text(text =>
|
||||||
|
{
|
||||||
|
text.CurrentPageNumber(); text.Span(" / "); text.TotalPages();
|
||||||
|
text.Span($" · {dto.CompanyName} · Generated {DateTime.Now:MMM d, yyyy}").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return document.GeneratePdf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a Cash Flow Statement PDF with three sections (Operating, Investing, Financing)
|
||||||
|
/// plus a summary reconciling beginning → ending cash. Uses a teal accent palette to
|
||||||
|
/// visually distinguish it from the other financial statements.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]> GenerateCashFlowStatementPdfAsync(CashFlowStatementDto dto)
|
||||||
|
{
|
||||||
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
|
const string accent = "#0891b2";
|
||||||
|
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var document = Document.Create(container =>
|
||||||
|
{
|
||||||
|
container.Page(page =>
|
||||||
|
{
|
||||||
|
page.Size(PageSizes.Letter);
|
||||||
|
page.Margin(0.6f, Unit.Inch);
|
||||||
|
page.PageColor(Colors.White);
|
||||||
|
page.DefaultTextStyle(x => x.FontSize(9).FontFamily("Arial"));
|
||||||
|
|
||||||
|
page.Header().Element(c => ComposeReportHeader(c, dto.CompanyName, "Cash Flow Statement",
|
||||||
|
$"{dto.From:MMMM d, yyyy} – {dto.To:MMMM d, yyyy}", accent));
|
||||||
|
|
||||||
|
page.Content().PaddingTop(12).Column(col =>
|
||||||
|
{
|
||||||
|
col.Spacing(4);
|
||||||
|
|
||||||
|
// ── Operating Activities ──────────────────────────────────────
|
||||||
|
col.Item().Text("Operating Activities").Bold().FontSize(11).FontColor(accent);
|
||||||
|
col.Item().Table(t =>
|
||||||
|
{
|
||||||
|
t.ColumnsDefinition(c => { c.RelativeColumn(3); c.ConstantColumn(100); });
|
||||||
|
CfRow(t, "Cash Received from Customers", dto.CashFromCustomers, false);
|
||||||
|
CfRow(t, "Cash Paid to Vendors (Bills)", -dto.CashToVendors, false);
|
||||||
|
CfRow(t, "Cash Paid for Expenses", -dto.CashForExpenses, false);
|
||||||
|
CfTotalRow(t, "Net Cash from Operating Activities", dto.NetOperating);
|
||||||
|
});
|
||||||
|
|
||||||
|
col.Item().PaddingTop(10).Text("Investing Activities").Bold().FontSize(11).FontColor(accent);
|
||||||
|
col.Item().Table(t =>
|
||||||
|
{
|
||||||
|
t.ColumnsDefinition(c => { c.RelativeColumn(3); c.ConstantColumn(100); });
|
||||||
|
if (dto.InvestingLines.Count == 0)
|
||||||
|
CfRow(t, "No investing activities recorded", 0, true);
|
||||||
|
else
|
||||||
|
foreach (var line in dto.InvestingLines)
|
||||||
|
CfRow(t, line.Label, line.Amount, false);
|
||||||
|
CfTotalRow(t, "Net Cash from Investing Activities", dto.NetInvesting);
|
||||||
|
});
|
||||||
|
|
||||||
|
col.Item().PaddingTop(10).Text("Financing Activities").Bold().FontSize(11).FontColor(accent);
|
||||||
|
col.Item().Table(t =>
|
||||||
|
{
|
||||||
|
t.ColumnsDefinition(c => { c.RelativeColumn(3); c.ConstantColumn(100); });
|
||||||
|
if (dto.FinancingLines.Count == 0)
|
||||||
|
CfRow(t, "No financing activities recorded", 0, true);
|
||||||
|
else
|
||||||
|
foreach (var line in dto.FinancingLines)
|
||||||
|
CfRow(t, line.Label, line.Amount, false);
|
||||||
|
CfTotalRow(t, "Net Cash from Financing Activities", dto.NetFinancing);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Summary ───────────────────────────────────────────────────
|
||||||
|
col.Item().PaddingTop(12).Table(t =>
|
||||||
|
{
|
||||||
|
t.ColumnsDefinition(c => { c.RelativeColumn(3); c.ConstantColumn(100); });
|
||||||
|
|
||||||
|
void SumRow(string label, decimal amount, bool bold = false)
|
||||||
|
{
|
||||||
|
var bg = bold ? "#e0f2fe" : "#ffffff";
|
||||||
|
var lText = t.Cell().Background(bg).PaddingVertical(4).PaddingHorizontal(6).Text(label).FontSize(9);
|
||||||
|
if (bold) lText.Bold();
|
||||||
|
var vText = t.Cell().Background(bg).PaddingVertical(4).PaddingHorizontal(6).AlignRight()
|
||||||
|
.Text(amount.ToString("C")).FontSize(9)
|
||||||
|
.FontColor(amount < 0 ? Colors.Red.Darken2 : Colors.Black);
|
||||||
|
if (bold) vText.Bold();
|
||||||
|
}
|
||||||
|
|
||||||
|
SumRow("Beginning Cash Balance", dto.BeginningCash);
|
||||||
|
SumRow("Net Change in Cash", dto.NetChangeInCash);
|
||||||
|
SumRow("Ending Cash Balance", dto.EndingCash, bold: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
page.Footer().AlignCenter().Text(text =>
|
||||||
|
{
|
||||||
|
text.CurrentPageNumber(); text.Span(" / "); text.TotalPages();
|
||||||
|
text.Span($" · {dto.CompanyName} · Generated {DateTime.Now:MMM d, yyyy}").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return document.GeneratePdf();
|
||||||
|
});
|
||||||
|
|
||||||
|
static void CfRow(TableDescriptor t, string label, decimal amount, bool muted)
|
||||||
|
{
|
||||||
|
t.Cell().BorderBottom(0.5f).BorderColor("#e5e7eb")
|
||||||
|
.PaddingVertical(3).PaddingHorizontal(6)
|
||||||
|
.Text(label).FontSize(9).FontColor(muted ? Colors.Grey.Medium : Colors.Black);
|
||||||
|
t.Cell().BorderBottom(0.5f).BorderColor("#e5e7eb")
|
||||||
|
.PaddingVertical(3).PaddingHorizontal(6).AlignRight()
|
||||||
|
.Text(muted ? "" : amount.ToString("C")).FontSize(9)
|
||||||
|
.FontColor(amount < 0 ? Colors.Red.Darken2 : Colors.Black);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CfTotalRow(TableDescriptor t, string label, decimal amount)
|
||||||
|
{
|
||||||
|
t.Cell().Background("#f0f9ff").PaddingVertical(4).PaddingHorizontal(6)
|
||||||
|
.Text(label).Bold().FontSize(9);
|
||||||
|
t.Cell().Background("#f0f9ff").PaddingVertical(4).PaddingHorizontal(6).AlignRight()
|
||||||
|
.Text(amount.ToString("C")).Bold().FontSize(9)
|
||||||
|
.FontColor(amount < 0 ? Colors.Red.Darken2 : Colors.Black);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -590,53 +590,9 @@ public class PricingCalculationService : IPricingCalculationService
|
|||||||
{
|
{
|
||||||
QuoteItemPricingResult itemResult;
|
QuoteItemPricingResult itemResult;
|
||||||
|
|
||||||
// Catalog items - if they have coats, add coat costs to catalog base price
|
// All items (catalog and calculated) go through CalculateQuoteItemPriceAsync, which
|
||||||
if (item.CatalogItemId.HasValue)
|
// handles PowderCostOverride, prep cost inclusion, and all item type variants.
|
||||||
{
|
itemResult = await CalculateQuoteItemPriceAsync(item, companyId, ovenCostOverride);
|
||||||
var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(item.CatalogItemId.Value);
|
|
||||||
if (catalogItem != null)
|
|
||||||
{
|
|
||||||
// If the catalog item has coats, calculate using CalculateQuoteItemPriceAsync
|
|
||||||
// (which already includes the catalog base price + coat costs)
|
|
||||||
if (item.Coats != null && item.Coats.Any())
|
|
||||||
{
|
|
||||||
// CalculateQuoteItemPriceAsync already adds catalog base price to coat costs
|
|
||||||
itemResult = await CalculateQuoteItemPriceAsync(item, companyId, ovenCostOverride);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No coats - use simple catalog default price
|
|
||||||
var catalogItemTotal = catalogItem.DefaultPrice * item.Quantity;
|
|
||||||
itemResult = new QuoteItemPricingResult
|
|
||||||
{
|
|
||||||
MaterialCost = 0,
|
|
||||||
LaborCost = 0,
|
|
||||||
EquipmentCost = 0,
|
|
||||||
ItemSubtotal = catalogItemTotal,
|
|
||||||
UnitPrice = catalogItem.DefaultPrice,
|
|
||||||
TotalPrice = catalogItemTotal
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Catalog item not found, create zero result
|
|
||||||
itemResult = new QuoteItemPricingResult
|
|
||||||
{
|
|
||||||
MaterialCost = 0,
|
|
||||||
LaborCost = 0,
|
|
||||||
EquipmentCost = 0,
|
|
||||||
ItemSubtotal = 0,
|
|
||||||
UnitPrice = 0,
|
|
||||||
TotalPrice = 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Calculated items use the full pricing calculation
|
|
||||||
itemResult = await CalculateQuoteItemPriceAsync(item, companyId, ovenCostOverride);
|
|
||||||
}
|
|
||||||
|
|
||||||
itemResults.Add(itemResult);
|
itemResults.Add(itemResult);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,22 +66,16 @@ public class ProfilePhotoService : IProfilePhotoService
|
|||||||
string userId,
|
string userId,
|
||||||
int companyId)
|
int companyId)
|
||||||
{
|
{
|
||||||
if (file == null || file.Length == 0)
|
var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedImageTypes, MaxPhotoSize);
|
||||||
return (false, string.Empty, "No file was uploaded.");
|
if (!isValid)
|
||||||
|
return (false, string.Empty, error);
|
||||||
if (file.Length > MaxPhotoSize)
|
|
||||||
return (false, string.Empty, "Photo must be smaller than 10 MB.");
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
|
||||||
if (string.IsNullOrEmpty(extension) || !AllowedImageTypes.Contains(extension))
|
|
||||||
return (false, string.Empty, "Only JPG, PNG, GIF, and WebP images are allowed.");
|
|
||||||
|
|
||||||
// Delete old photos for this user with different extensions
|
// Delete old photos for this user with different extensions
|
||||||
await DeleteOldPhotosForUserAsync(companyId, userId, extension);
|
await DeleteOldPhotosForUserAsync(companyId, userId, extension);
|
||||||
|
|
||||||
// Blob path mirrors former filesystem path
|
// Blob path mirrors former filesystem path
|
||||||
var blobName = $"{companyId}/profile-photos/{userId}{extension}";
|
var blobName = $"{companyId}/profile-photos/{userId}{extension}";
|
||||||
var contentType = GetContentType(extension);
|
var contentType = BlobFileHelper.GetContentType(extension);
|
||||||
|
|
||||||
using var stream = file.OpenReadStream();
|
using var stream = file.OpenReadStream();
|
||||||
var result = await _blobService.UploadAsync(_settings.Containers.ProfileImages, blobName, stream, contentType);
|
var result = await _blobService.UploadAsync(_settings.Containers.ProfileImages, blobName, stream, contentType);
|
||||||
@@ -172,19 +166,4 @@ public class ProfilePhotoService : IProfilePhotoService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maps a lowercase file extension to its canonical MIME content type.
|
|
||||||
/// Falls back to <c>image/jpeg</c> (rather than octet-stream) because all
|
|
||||||
/// allowed extensions are image types and browsers will render them correctly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="extension">Lowercase file extension including the leading dot.</param>
|
|
||||||
/// <returns>MIME type string.</returns>
|
|
||||||
private static string GetContentType(string extension) => extension switch
|
|
||||||
{
|
|
||||||
".jpg" or ".jpeg" => "image/jpeg",
|
|
||||||
".png" => "image/png",
|
|
||||||
".gif" => "image/gif",
|
|
||||||
".webp" => "image/webp",
|
|
||||||
_ => "image/jpeg"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,19 +50,13 @@ public class QuotePhotoService : IQuotePhotoService
|
|||||||
public async Task<(bool Success, string TempId, string FilePath, string ErrorMessage)> SaveTempPhotoAsync(
|
public async Task<(bool Success, string TempId, string FilePath, string ErrorMessage)> SaveTempPhotoAsync(
|
||||||
IFormFile file, int companyId)
|
IFormFile file, int companyId)
|
||||||
{
|
{
|
||||||
if (file == null || file.Length == 0)
|
var (isValid, ext, validationError) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSizeBytes);
|
||||||
return (false, string.Empty, string.Empty, "No file provided.");
|
if (!isValid)
|
||||||
|
return (false, string.Empty, string.Empty, validationError);
|
||||||
if (file.Length > MaxFileSizeBytes)
|
|
||||||
return (false, string.Empty, string.Empty, "File exceeds the 10 MB limit.");
|
|
||||||
|
|
||||||
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
|
|
||||||
if (!AllowedExtensions.Contains(ext))
|
|
||||||
return (false, string.Empty, string.Empty, $"File type '{ext}' is not allowed.");
|
|
||||||
|
|
||||||
var tempId = Guid.NewGuid().ToString("N");
|
var tempId = Guid.NewGuid().ToString("N");
|
||||||
var blobName = $"temp/{tempId}/{Guid.NewGuid():N}{ext}";
|
var blobName = $"temp/{tempId}/{Guid.NewGuid():N}{ext}";
|
||||||
var contentType = GetContentType(ext);
|
var contentType = BlobFileHelper.GetContentType(ext);
|
||||||
|
|
||||||
using var stream = file.OpenReadStream();
|
using var stream = file.OpenReadStream();
|
||||||
var result = await _blobService.UploadAsync(_settings.Containers.QuoteImages, blobName, stream, contentType);
|
var result = await _blobService.UploadAsync(_settings.Containers.QuoteImages, blobName, stream, contentType);
|
||||||
@@ -100,7 +94,7 @@ public class QuotePhotoService : IQuotePhotoService
|
|||||||
return (false, string.Empty, "Failed to read temp photo.");
|
return (false, string.Empty, "Failed to read temp photo.");
|
||||||
|
|
||||||
using var ms = new MemoryStream(download.Content);
|
using var ms = new MemoryStream(download.Content);
|
||||||
var upload = await _blobService.UploadAsync(_settings.Containers.QuoteImages, destBlob, ms, GetContentType(ext));
|
var upload = await _blobService.UploadAsync(_settings.Containers.QuoteImages, destBlob, ms, BlobFileHelper.GetContentType(ext));
|
||||||
if (!upload.Success)
|
if (!upload.Success)
|
||||||
return (false, string.Empty, "Failed to save permanent photo.");
|
return (false, string.Empty, "Failed to save permanent photo.");
|
||||||
|
|
||||||
@@ -173,12 +167,4 @@ public class QuotePhotoService : IQuotePhotoService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetContentType(string ext) => ext switch
|
|
||||||
{
|
|
||||||
".jpg" or ".jpeg" => "image/jpeg",
|
|
||||||
".png" => "image/png",
|
|
||||||
".gif" => "image/gif",
|
|
||||||
".webp" => "image/webp",
|
|
||||||
_ => "image/jpeg"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,379 @@
|
|||||||
|
using PowderCoating.Application.DTOs.Quote;
|
||||||
|
using PowderCoating.Application.Interfaces;
|
||||||
|
using PowderCoating.Core.Entities;
|
||||||
|
using PowderCoating.Core.Interfaces;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace PowderCoating.Application.Services;
|
||||||
|
|
||||||
|
public class QuotePricingAssemblyService : IQuotePricingAssemblyService
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IPricingCalculationService _pricingService;
|
||||||
|
private readonly IInventoryAiLookupService _aiLookupService;
|
||||||
|
private readonly ILogger<QuotePricingAssemblyService> _logger;
|
||||||
|
|
||||||
|
public QuotePricingAssemblyService(
|
||||||
|
IUnitOfWork unitOfWork,
|
||||||
|
IPricingCalculationService pricingService,
|
||||||
|
IInventoryAiLookupService aiLookupService,
|
||||||
|
ILogger<QuotePricingAssemblyService> logger)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_pricingService = pricingService;
|
||||||
|
_aiLookupService = aiLookupService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyPricingSnapshot(Quote quote, QuotePricingResult pricingResult)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(quote);
|
||||||
|
ArgumentNullException.ThrowIfNull(pricingResult);
|
||||||
|
|
||||||
|
quote.MaterialCosts = pricingResult.MaterialCosts;
|
||||||
|
quote.LaborCosts = pricingResult.LaborCosts;
|
||||||
|
quote.EquipmentCosts = pricingResult.EquipmentCosts;
|
||||||
|
quote.ItemsSubtotal = pricingResult.ItemsSubtotal;
|
||||||
|
quote.OvenBatchCost = pricingResult.OvenBatchCost;
|
||||||
|
quote.FacilityOverheadCost = pricingResult.FacilityOverheadCost;
|
||||||
|
quote.FacilityOverheadRatePerHour = pricingResult.FacilityOverheadRatePerHour;
|
||||||
|
quote.ShopSuppliesAmount = pricingResult.ShopSuppliesAmount;
|
||||||
|
quote.ShopSuppliesPercent = pricingResult.ShopSuppliesPercent;
|
||||||
|
quote.OverheadAmount = pricingResult.OverheadCosts;
|
||||||
|
quote.OverheadPercent = pricingResult.OverheadPercent;
|
||||||
|
quote.ProfitMargin = pricingResult.ProfitMargin;
|
||||||
|
quote.ProfitPercent = pricingResult.ProfitPercent;
|
||||||
|
quote.SubTotal = pricingResult.SubtotalBeforeDiscount;
|
||||||
|
quote.PricingTierDiscountAmount = pricingResult.PricingTierDiscountAmount;
|
||||||
|
quote.PricingTierDiscountPercent = pricingResult.PricingTierDiscountPercent;
|
||||||
|
quote.QuoteDiscountAmount = pricingResult.QuoteDiscountAmount;
|
||||||
|
quote.QuoteDiscountPercent = pricingResult.QuoteDiscountPercent;
|
||||||
|
quote.DiscountPercent = pricingResult.DiscountPercent;
|
||||||
|
quote.DiscountAmount = pricingResult.DiscountAmount;
|
||||||
|
quote.SubtotalAfterDiscount = pricingResult.SubtotalAfterDiscount;
|
||||||
|
quote.RushFee = pricingResult.RushFee;
|
||||||
|
quote.TaxAmount = pricingResult.TaxAmount;
|
||||||
|
quote.Total = pricingResult.Total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<QuoteItem>> CreateQuoteItemsAsync(
|
||||||
|
IEnumerable<CreateQuoteItemDto> itemDtos,
|
||||||
|
int quoteId,
|
||||||
|
int companyId,
|
||||||
|
decimal? ovenRateOverride,
|
||||||
|
DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(itemDtos);
|
||||||
|
|
||||||
|
var items = new List<QuoteItem>();
|
||||||
|
foreach (var itemDto in itemDtos)
|
||||||
|
{
|
||||||
|
var item = BuildQuoteItem(itemDto, quoteId, companyId, createdAtUtc);
|
||||||
|
await ApplyPricingAsync(item, itemDto, companyId, ovenRateOverride);
|
||||||
|
await UpdateAiPredictionOverrideAsync(itemDto, item.UnitPrice);
|
||||||
|
|
||||||
|
item.Coats = await BuildQuoteItemCoatsAsync(itemDto, companyId, createdAtUtc);
|
||||||
|
item.PrepServices = BuildQuoteItemPrepServices(itemDto, companyId, createdAtUtc);
|
||||||
|
items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyPricingAsync(QuoteItem item, CreateQuoteItemDto itemDto, int companyId, decimal? ovenRateOverride)
|
||||||
|
{
|
||||||
|
if (itemDto.IsAiItem && itemDto.ManualUnitPrice.HasValue && itemDto.ManualUnitPrice.Value > 0)
|
||||||
|
{
|
||||||
|
item.UnitPrice = itemDto.ManualUnitPrice.Value;
|
||||||
|
item.TotalPrice = itemDto.ManualUnitPrice.Value * itemDto.Quantity;
|
||||||
|
_logger.LogInformation("AI item price: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemDto.IsSalesItem && itemDto.ManualUnitPrice.HasValue)
|
||||||
|
{
|
||||||
|
item.UnitPrice = itemDto.ManualUnitPrice.Value;
|
||||||
|
item.TotalPrice = itemDto.ManualUnitPrice.Value * itemDto.Quantity;
|
||||||
|
_logger.LogInformation("Sales item price: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemDto.CatalogItemId.HasValue)
|
||||||
|
{
|
||||||
|
if (itemDto.Coats != null && itemDto.Coats.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Calculating catalog item with {CoatCount} coats", itemDto.Coats.Count);
|
||||||
|
var itemPricing = await _pricingService.CalculateQuoteItemPriceAsync(itemDto, companyId, ovenRateOverride);
|
||||||
|
ApplyCalculatedPricing(item, itemPricing);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(itemDto.CatalogItemId.Value);
|
||||||
|
if (catalogItem != null)
|
||||||
|
{
|
||||||
|
var unitPrice = itemDto.PowderCostOverride is > 0
|
||||||
|
? itemDto.PowderCostOverride.Value
|
||||||
|
: catalogItem.DefaultPrice;
|
||||||
|
item.UnitPrice = unitPrice;
|
||||||
|
item.TotalPrice = unitPrice * itemDto.Quantity;
|
||||||
|
_logger.LogInformation("Catalog item no coats: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Calculating custom item with {CoatCount} coats", itemDto.Coats?.Count ?? 0);
|
||||||
|
var pricing = await _pricingService.CalculateQuoteItemPriceAsync(itemDto, companyId, ovenRateOverride);
|
||||||
|
ApplyCalculatedPricing(item, pricing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<QuoteItemCoat>> BuildQuoteItemCoatsAsync(CreateQuoteItemDto itemDto, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
if (itemDto.Coats == null || itemDto.Coats.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var coats = new List<QuoteItemCoat>();
|
||||||
|
for (var coatIndex = 0; coatIndex < itemDto.Coats.Count; coatIndex++)
|
||||||
|
{
|
||||||
|
var coatDto = itemDto.Coats[coatIndex];
|
||||||
|
|
||||||
|
if (coatDto.AddAsIncoming && coatDto.CatalogItemId.HasValue && !coatDto.InventoryItemId.HasValue)
|
||||||
|
coatDto.InventoryItemId = await CreateIncomingInventoryItemAsync(coatDto, companyId);
|
||||||
|
|
||||||
|
var coat = BuildQuoteItemCoat(coatDto, companyId, createdAtUtc);
|
||||||
|
var coatPricing = await _pricingService.CalculateCoatPriceAsync(
|
||||||
|
coatDto,
|
||||||
|
itemDto.SurfaceAreaSqFt,
|
||||||
|
itemDto.Quantity,
|
||||||
|
coatIndex,
|
||||||
|
itemDto.EstimatedMinutes,
|
||||||
|
companyId);
|
||||||
|
|
||||||
|
coat.CoatMaterialCost = coatPricing.CoatMaterialCost;
|
||||||
|
coat.CoatLaborCost = coatPricing.CoatLaborCost;
|
||||||
|
coat.CoatTotalCost = coatPricing.CoatTotalCost;
|
||||||
|
coats.Add(coat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return coats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<QuoteItemPrepService> BuildQuoteItemPrepServices(CreateQuoteItemDto itemDto, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
if (itemDto.PrepServices == null || itemDto.PrepServices.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return itemDto.PrepServices
|
||||||
|
.Select(ps => new QuoteItemPrepService
|
||||||
|
{
|
||||||
|
PrepServiceId = ps.PrepServiceId,
|
||||||
|
EstimatedMinutes = ps.EstimatedMinutes,
|
||||||
|
BlastSetupId = ps.BlastSetupId,
|
||||||
|
CompanyId = companyId,
|
||||||
|
CreatedAt = createdAtUtc
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static QuoteItem BuildQuoteItem(CreateQuoteItemDto itemDto, int quoteId, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
return new QuoteItem
|
||||||
|
{
|
||||||
|
QuoteId = quoteId,
|
||||||
|
Description = itemDto.Description,
|
||||||
|
Quantity = itemDto.Quantity,
|
||||||
|
SurfaceAreaSqFt = itemDto.SurfaceAreaSqFt,
|
||||||
|
CatalogItemId = itemDto.CatalogItemId,
|
||||||
|
IsGenericItem = itemDto.IsGenericItem,
|
||||||
|
ManualUnitPrice = itemDto.ManualUnitPrice,
|
||||||
|
PowderCostOverride = itemDto.PowderCostOverride,
|
||||||
|
IsLaborItem = itemDto.IsLaborItem,
|
||||||
|
IsSalesItem = itemDto.IsSalesItem,
|
||||||
|
Sku = itemDto.Sku,
|
||||||
|
RequiresSandblasting = itemDto.RequiresSandblasting,
|
||||||
|
RequiresMasking = itemDto.RequiresMasking,
|
||||||
|
EstimatedMinutes = itemDto.EstimatedMinutes,
|
||||||
|
IncludePrepCost = itemDto.IncludePrepCost,
|
||||||
|
Notes = itemDto.Notes,
|
||||||
|
Complexity = itemDto.Complexity,
|
||||||
|
IsAiItem = itemDto.IsAiItem,
|
||||||
|
AiTags = itemDto.AiTags,
|
||||||
|
AiPredictionId = itemDto.AiPredictionId,
|
||||||
|
CompanyId = companyId,
|
||||||
|
CreatedAt = createdAtUtc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static QuoteItemCoat BuildQuoteItemCoat(CreateQuoteItemCoatDto coatDto, int companyId, DateTime createdAtUtc)
|
||||||
|
{
|
||||||
|
return new QuoteItemCoat
|
||||||
|
{
|
||||||
|
CoatName = coatDto.CoatName,
|
||||||
|
Sequence = coatDto.Sequence,
|
||||||
|
InventoryItemId = coatDto.InventoryItemId,
|
||||||
|
ColorName = coatDto.ColorName,
|
||||||
|
VendorId = coatDto.VendorId,
|
||||||
|
ColorCode = coatDto.ColorCode,
|
||||||
|
Finish = coatDto.Finish,
|
||||||
|
CoverageSqFtPerLb = coatDto.CoverageSqFtPerLb,
|
||||||
|
TransferEfficiency = coatDto.TransferEfficiency,
|
||||||
|
PowderCostPerLb = coatDto.PowderCostPerLb,
|
||||||
|
PowderToOrder = coatDto.PowderToOrder,
|
||||||
|
Notes = coatDto.Notes,
|
||||||
|
CompanyId = companyId,
|
||||||
|
CreatedAt = createdAtUtc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyCalculatedPricing(QuoteItem item, QuoteItemPricingResult pricing)
|
||||||
|
{
|
||||||
|
item.UnitPrice = pricing.UnitPrice;
|
||||||
|
item.TotalPrice = pricing.TotalPrice;
|
||||||
|
item.ItemMaterialCost = pricing.MaterialCost;
|
||||||
|
item.ItemLaborCost = pricing.LaborCost;
|
||||||
|
item.ItemEquipmentCost = pricing.EquipmentCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateAiPredictionOverrideAsync(CreateQuoteItemDto itemDto, decimal finalUnitPrice)
|
||||||
|
{
|
||||||
|
if (!itemDto.AiPredictionId.HasValue) return;
|
||||||
|
|
||||||
|
var prediction = await _unitOfWork.AiItemPredictions.GetByIdAsync(itemDto.AiPredictionId.Value);
|
||||||
|
if (prediction == null) return;
|
||||||
|
|
||||||
|
var sqftDiff = Math.Abs(prediction.PredictedSurfaceAreaSqFt - itemDto.SurfaceAreaSqFt);
|
||||||
|
var priceDiff = Math.Abs(prediction.PredictedUnitPrice - finalUnitPrice);
|
||||||
|
prediction.UserOverrodeEstimate = sqftDiff > 0.01m || priceDiff > 0.01m;
|
||||||
|
prediction.UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int?> CreateIncomingInventoryItemAsync(CreateQuoteItemCoatDto coatDto, int companyId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var catalogItem = await _unitOfWork.PowderCatalog.GetByIdAsync(coatDto.CatalogItemId!.Value);
|
||||||
|
if (catalogItem == null) return null;
|
||||||
|
|
||||||
|
var categories = await _unitOfWork.InventoryCategoryLookups.GetAllAsync();
|
||||||
|
var coatingCategory = categories
|
||||||
|
.Where(c => c.IsActive && c.IsCoating)
|
||||||
|
.OrderBy(c => c.DisplayOrder)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
var vendors = await _unitOfWork.Vendors.GetAllAsync();
|
||||||
|
var vendorNameLower = catalogItem.VendorName.ToLower();
|
||||||
|
var matchedVendor = vendors.FirstOrDefault(v =>
|
||||||
|
v.CompanyName.ToLower().Contains(vendorNameLower) ||
|
||||||
|
vendorNameLower.Contains(v.CompanyName.ToLower()));
|
||||||
|
|
||||||
|
var code = coatingCategory != null
|
||||||
|
? (coatingCategory.CategoryCode.Length >= 4
|
||||||
|
? coatingCategory.CategoryCode[..4].ToUpperInvariant()
|
||||||
|
: coatingCategory.CategoryCode.ToUpperInvariant().PadRight(4, 'X'))
|
||||||
|
: "POWD";
|
||||||
|
var prefix = $"{code}-{DateTime.Now:yyMM}-";
|
||||||
|
var allItems = await _unitOfWork.InventoryItems.GetAllAsync(ignoreQueryFilters: true);
|
||||||
|
var maxSeq = allItems
|
||||||
|
.Where(i => i.SKU.StartsWith(prefix))
|
||||||
|
.Select(i => int.TryParse(i.SKU[prefix.Length..], out var n) ? n : 0)
|
||||||
|
.DefaultIfEmpty(0)
|
||||||
|
.Max();
|
||||||
|
var sku = $"{prefix}{(maxSeq + 1):D4}";
|
||||||
|
|
||||||
|
var name = System.Globalization.CultureInfo.CurrentCulture.TextInfo
|
||||||
|
.ToTitleCase(catalogItem.ColorName.Trim().ToLower());
|
||||||
|
|
||||||
|
var description = catalogItem.Description;
|
||||||
|
var finish = catalogItem.Finish;
|
||||||
|
var colorFamilies = catalogItem.ColorFamilies;
|
||||||
|
var cureTemp = catalogItem.CureTemperatureF;
|
||||||
|
var cureTime = catalogItem.CureTimeMinutes;
|
||||||
|
var coverage = catalogItem.CoverageSqFtPerLb;
|
||||||
|
var transferEff = catalogItem.TransferEfficiency;
|
||||||
|
var specificGravity = catalogItem.SpecificGravity;
|
||||||
|
var imageUrl = catalogItem.ImageUrl;
|
||||||
|
var sdsUrl = catalogItem.SdsUrl;
|
||||||
|
var tdsUrl = catalogItem.TdsUrl;
|
||||||
|
|
||||||
|
var needsAugment = !string.IsNullOrWhiteSpace(catalogItem.ProductUrl) &&
|
||||||
|
(string.IsNullOrWhiteSpace(description) ||
|
||||||
|
string.IsNullOrWhiteSpace(colorFamilies) ||
|
||||||
|
cureTemp == null || cureTime == null);
|
||||||
|
if (needsAugment)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var augmented = await _aiLookupService.LookupByUrlAsync(catalogItem.ProductUrl!, catalogItem.ColorName, catalogItem.TdsUrl);
|
||||||
|
if (augmented.Success)
|
||||||
|
{
|
||||||
|
description = string.IsNullOrWhiteSpace(description) ? augmented.Description : description;
|
||||||
|
finish = string.IsNullOrWhiteSpace(finish) ? augmented.Finish : finish;
|
||||||
|
colorFamilies = string.IsNullOrWhiteSpace(colorFamilies) ? augmented.ColorFamilies : colorFamilies;
|
||||||
|
cureTemp ??= augmented.CureTemperatureF;
|
||||||
|
cureTime ??= augmented.CureTimeMinutes;
|
||||||
|
coverage ??= augmented.CoverageSqFtPerLb;
|
||||||
|
transferEff ??= augmented.TransferEfficiency;
|
||||||
|
specificGravity ??= augmented.SpecificGravity;
|
||||||
|
imageUrl = string.IsNullOrWhiteSpace(imageUrl) ? augmented.ImageUrl : imageUrl;
|
||||||
|
sdsUrl = string.IsNullOrWhiteSpace(sdsUrl) ? augmented.SdsUrl : sdsUrl;
|
||||||
|
tdsUrl = string.IsNullOrWhiteSpace(tdsUrl) ? augmented.TdsUrl : tdsUrl;
|
||||||
|
_logger.LogInformation("AI-augmented incoming inventory item for catalog {CatalogId}", catalogItem.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "AI augment failed for catalog {CatalogId}, continuing with catalog data", catalogItem.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new InventoryItem
|
||||||
|
{
|
||||||
|
SKU = sku,
|
||||||
|
Name = name,
|
||||||
|
Description = description,
|
||||||
|
ColorName = catalogItem.ColorName,
|
||||||
|
Manufacturer = catalogItem.VendorName,
|
||||||
|
ManufacturerPartNumber = catalogItem.Sku,
|
||||||
|
Finish = finish,
|
||||||
|
ColorFamilies = colorFamilies,
|
||||||
|
RequiresClearCoat = catalogItem.RequiresClearCoat ?? false,
|
||||||
|
CoverageSqFtPerLb = coverage ?? 30m,
|
||||||
|
TransferEfficiency = transferEff ?? 65m,
|
||||||
|
CureTemperatureF = cureTemp,
|
||||||
|
CureTimeMinutes = cureTime,
|
||||||
|
SpecificGravity = specificGravity,
|
||||||
|
SpecPageUrl = catalogItem.ProductUrl,
|
||||||
|
ImageUrl = imageUrl,
|
||||||
|
SdsUrl = sdsUrl,
|
||||||
|
TdsUrl = tdsUrl,
|
||||||
|
UnitCost = catalogItem.UnitPrice,
|
||||||
|
AverageCost = catalogItem.UnitPrice,
|
||||||
|
LastPurchasePrice = catalogItem.UnitPrice,
|
||||||
|
QuantityOnHand = 0,
|
||||||
|
UnitOfMeasure = "lbs",
|
||||||
|
PrimaryVendorId = matchedVendor?.Id,
|
||||||
|
InventoryCategoryId = coatingCategory?.Id,
|
||||||
|
Category = coatingCategory?.DisplayName ?? "Powder Coating",
|
||||||
|
IsActive = true,
|
||||||
|
IsIncoming = true,
|
||||||
|
CompanyId = companyId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
await _unitOfWork.InventoryItems.AddAsync(item);
|
||||||
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
|
||||||
|
coatDto.PowderCostPerLb = null;
|
||||||
|
_logger.LogInformation("Created incoming inventory item {Id} ({Name}) from catalog {CatalogId} via quote coat",
|
||||||
|
item.Id, item.Name, coatDto.CatalogItemId);
|
||||||
|
|
||||||
|
return item.Id;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to create incoming inventory item from catalog {CatalogId}, continuing without inventory link",
|
||||||
|
coatDto.CatalogItemId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,6 +122,10 @@ public class BillPayment : BaseEntity
|
|||||||
public string? CheckNumber { get; set; }
|
public string? CheckNumber { get; set; }
|
||||||
public string? Memo { get; set; }
|
public string? Memo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>True once this payment has been matched against a bank statement during reconciliation.</summary>
|
||||||
|
public bool IsCleared { get; set; } = false;
|
||||||
|
public DateTime? ClearedDate { get; set; }
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
public virtual Bill Bill { get; set; } = null!;
|
public virtual Bill Bill { get; set; } = null!;
|
||||||
public virtual Vendor Vendor { get; set; } = null!;
|
public virtual Vendor Vendor { get; set; } = null!;
|
||||||
@@ -150,9 +154,305 @@ public class Expense : BaseEntity
|
|||||||
public string? Memo { get; set; }
|
public string? Memo { get; set; }
|
||||||
public string? ReceiptFilePath { get; set; }
|
public string? ReceiptFilePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>True once this expense has been matched against a bank statement during reconciliation.</summary>
|
||||||
|
public bool IsCleared { get; set; } = false;
|
||||||
|
public DateTime? ClearedDate { get; set; }
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
public virtual Vendor? Vendor { get; set; }
|
public virtual Vendor? Vendor { get; set; }
|
||||||
public virtual Account ExpenseAccount { get; set; } = null!;
|
public virtual Account ExpenseAccount { get; set; } = null!;
|
||||||
public virtual Account PaymentAccount { get; set; } = null!;
|
public virtual Account PaymentAccount { get; set; } = null!;
|
||||||
public virtual Job? Job { get; set; }
|
public virtual Job? Job { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manual double-entry journal entry. Lines must balance (sum of debits == sum of credits)
|
||||||
|
/// before posting. Once posted the entry is immutable — use Reverse to correct it.
|
||||||
|
/// Entry numbering follows the pattern JE-YYMM-#### scoped per company.
|
||||||
|
/// </summary>
|
||||||
|
public class JournalEntry : BaseEntity
|
||||||
|
{
|
||||||
|
public string EntryNumber { get; set; } = string.Empty;
|
||||||
|
public DateTime EntryDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public string? Reference { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public JournalEntryStatus Status { get; set; } = JournalEntryStatus.Draft;
|
||||||
|
|
||||||
|
/// <summary>True if this entry was machine-generated as a reversal of another entry.</summary>
|
||||||
|
public bool IsReversal { get; set; } = false;
|
||||||
|
/// <summary>FK to the original entry being reversed. Null for normal entries.</summary>
|
||||||
|
public int? ReversalOfId { get; set; }
|
||||||
|
|
||||||
|
public DateTime? PostedAt { get; set; }
|
||||||
|
public string? PostedBy { get; set; }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public virtual ICollection<JournalEntryLine> Lines { get; set; } = new List<JournalEntryLine>();
|
||||||
|
public virtual JournalEntry? ReversalOf { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One debit or credit line within a <see cref="JournalEntry"/>. Either DebitAmount or CreditAmount
|
||||||
|
/// should be non-zero per line (not both). LineOrder controls display sequence.
|
||||||
|
/// </summary>
|
||||||
|
public class JournalEntryLine : BaseEntity
|
||||||
|
{
|
||||||
|
public int JournalEntryId { get; set; }
|
||||||
|
public int AccountId { get; set; }
|
||||||
|
public decimal DebitAmount { get; set; }
|
||||||
|
public decimal CreditAmount { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public int LineOrder { get; set; }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public virtual JournalEntry JournalEntry { get; set; } = null!;
|
||||||
|
public virtual Account Account { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A bank reconciliation session for a single bank/cash account against a statement.
|
||||||
|
/// Cleared balance = BeginningBalance + cleared deposits - cleared payments.
|
||||||
|
/// The reconciliation is complete when Difference (EndingBalance - ClearedBalance) == 0.
|
||||||
|
/// </summary>
|
||||||
|
public class BankReconciliation : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>Must be a bank/cash subtype account.</summary>
|
||||||
|
public int AccountId { get; set; }
|
||||||
|
public DateTime StatementDate { get; set; }
|
||||||
|
public decimal BeginningBalance { get; set; }
|
||||||
|
public decimal EndingBalance { get; set; }
|
||||||
|
public BankReconciliationStatus Status { get; set; } = BankReconciliationStatus.InProgress;
|
||||||
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
public string? CompletedBy { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public virtual Account Account { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A credit note received from a vendor (returned goods, pricing dispute, short-ship).
|
||||||
|
/// Reduces Accounts Payable and reverses the original expense/COGS when posted.
|
||||||
|
/// Numbering: VC-YYMM-####
|
||||||
|
/// </summary>
|
||||||
|
public class VendorCredit : BaseEntity
|
||||||
|
{
|
||||||
|
public string CreditNumber { get; set; } = string.Empty;
|
||||||
|
public int VendorId { get; set; }
|
||||||
|
/// <summary>AP account this credit reduces (default: Accounts Payable 2000).</summary>
|
||||||
|
public int APAccountId { get; set; }
|
||||||
|
public DateTime CreditDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public VendorCreditStatus Status { get; set; } = VendorCreditStatus.Open;
|
||||||
|
public decimal Total { get; set; }
|
||||||
|
public decimal RemainingAmount { get; set; }
|
||||||
|
public string? Memo { get; set; }
|
||||||
|
/// <summary>Set by Post() when GL entries are made (DR AP / CR expense lines). Null = unposted.</summary>
|
||||||
|
public DateTime? PostedDate { get; set; }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public virtual Vendor Vendor { get; set; } = null!;
|
||||||
|
public virtual Account APAccount { get; set; } = null!;
|
||||||
|
public virtual ICollection<VendorCreditLineItem> LineItems { get; set; } = new List<VendorCreditLineItem>();
|
||||||
|
public virtual ICollection<VendorCreditApplication> Applications { get; set; } = new List<VendorCreditApplication>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A single line on a vendor credit, each reversing a specific expense/COGS account.
|
||||||
|
/// </summary>
|
||||||
|
public class VendorCreditLineItem : BaseEntity
|
||||||
|
{
|
||||||
|
public int VendorCreditId { get; set; }
|
||||||
|
/// <summary>Expense/COGS account being reversed by this line.</summary>
|
||||||
|
public int? AccountId { get; set; }
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public virtual VendorCredit VendorCredit { get; set; } = null!;
|
||||||
|
public virtual Account? Account { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records the application of a vendor credit against a specific vendor bill.
|
||||||
|
/// No additional GL posting is needed — AP was already adjusted when the credit was posted.
|
||||||
|
/// </summary>
|
||||||
|
public class VendorCreditApplication : BaseEntity
|
||||||
|
{
|
||||||
|
public int VendorCreditId { get; set; }
|
||||||
|
public int BillId { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public DateTime AppliedDate { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public virtual VendorCredit VendorCredit { get; set; } = null!;
|
||||||
|
public virtual Bill Bill { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A saved recipe for a document that should be automatically created on a recurring schedule.
|
||||||
|
/// The <see cref="TemplateData"/> column stores a JSON blob whose schema depends on
|
||||||
|
/// <see cref="TemplateType"/>: see <c>RecurringTransactionService</c> for the exact shape.
|
||||||
|
/// <para>
|
||||||
|
/// Bills are created as Draft so the user can review before posting.
|
||||||
|
/// Expenses are created immediately (already-paid transactions).
|
||||||
|
/// </para>
|
||||||
|
/// Numbering: REC-YYMM-####
|
||||||
|
/// </summary>
|
||||||
|
public class RecurringTemplate : BaseEntity
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public RecurringTemplateType TemplateType { get; set; }
|
||||||
|
public RecurringFrequency Frequency { get; set; }
|
||||||
|
/// <summary>Every N periods. E.g. Frequency=Monthly, IntervalCount=3 → quarterly.</summary>
|
||||||
|
public int IntervalCount { get; set; } = 1;
|
||||||
|
/// <summary>UTC date when the template will next fire. Set to the desired first occurrence date on creation.</summary>
|
||||||
|
public DateTime NextFireDate { get; set; }
|
||||||
|
/// <summary>Optional UTC date after which no further occurrences are generated.</summary>
|
||||||
|
public DateTime? EndDate { get; set; }
|
||||||
|
/// <summary>Optional hard cap on total occurrences. Null = unlimited.</summary>
|
||||||
|
public int? MaxOccurrences { get; set; }
|
||||||
|
/// <summary>How many documents have been generated so far.</summary>
|
||||||
|
public int OccurrenceCount { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
/// <summary>JSON payload whose schema matches the TemplateType. See RecurringTransactionService.</summary>
|
||||||
|
public string TemplateData { get; set; } = "{}";
|
||||||
|
/// <summary>Last error from the background service, cleared on next successful fire.</summary>
|
||||||
|
public string? LastError { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A named tax rate (e.g., "CA Sales Tax 8.25%") used to pre-fill the TaxPercent field on
|
||||||
|
/// invoices when a taxable customer is selected. Companies can define multiple rates for
|
||||||
|
/// different jurisdictions and mark one as default.
|
||||||
|
/// </summary>
|
||||||
|
public class TaxRate : BaseEntity
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
/// <summary>Rate as a percentage, e.g., 8.25 means 8.25%.</summary>
|
||||||
|
public decimal Rate { get; set; }
|
||||||
|
public string? State { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
/// <summary>When true, this rate is auto-applied to new invoices for taxable customers.</summary>
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A depreciable fixed asset (oven, blast cabinet, spray booth, vehicle, etc.).
|
||||||
|
/// Stores straight-line depreciation parameters and links to the three GL accounts needed
|
||||||
|
/// to auto-post monthly depreciation journal entries.
|
||||||
|
/// </summary>
|
||||||
|
public class FixedAsset : BaseEntity
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public DateTime PurchaseDate { get; set; }
|
||||||
|
public decimal PurchaseCost { get; set; }
|
||||||
|
/// <summary>Residual value at end of useful life (often $0 for shop equipment).</summary>
|
||||||
|
public decimal SalvageValue { get; set; } = 0;
|
||||||
|
/// <summary>Total depreciation period in months (e.g., 60 = 5 years).</summary>
|
||||||
|
public int UsefulLifeMonths { get; set; }
|
||||||
|
/// <summary>Running total of depreciation posted so far.</summary>
|
||||||
|
public decimal AccumulatedDepreciation { get; set; } = 0;
|
||||||
|
public bool IsDisposed { get; set; } = false;
|
||||||
|
public DateTime? DisposalDate { get; set; }
|
||||||
|
|
||||||
|
// Computed — not persisted
|
||||||
|
/// <summary>Current net book value: PurchaseCost minus AccumulatedDepreciation.</summary>
|
||||||
|
public decimal BookValue => PurchaseCost - AccumulatedDepreciation;
|
||||||
|
/// <summary>Straight-line monthly depreciation amount.</summary>
|
||||||
|
public decimal MonthlyDepreciation => UsefulLifeMonths > 0
|
||||||
|
? Math.Round((PurchaseCost - SalvageValue) / UsefulLifeMonths, 2) : 0;
|
||||||
|
|
||||||
|
// GL account links — all optional; assets without accounts can be tracked but not auto-posted
|
||||||
|
/// <summary>Balance Sheet FixedAsset account (debited when asset is purchased).</summary>
|
||||||
|
public int? AssetAccountId { get; set; }
|
||||||
|
/// <summary>P&L Depreciation Expense account (debited each period).</summary>
|
||||||
|
public int? DepreciationExpenseAccountId { get; set; }
|
||||||
|
/// <summary>Balance Sheet Accumulated Depreciation account (credited each period).</summary>
|
||||||
|
public int? AccumDepreciationAccountId { get; set; }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public virtual Account? AssetAccount { get; set; }
|
||||||
|
public virtual Account? DepreciationExpenseAccount { get; set; }
|
||||||
|
public virtual Account? AccumDepreciationAccount { get; set; }
|
||||||
|
public virtual ICollection<FixedAssetDepreciationEntry> DepreciationEntries { get; set; } = new List<FixedAssetDepreciationEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records each periodic depreciation posting for a fixed asset. One record per asset per
|
||||||
|
/// month/year combination; linked to the JournalEntry that was created so the posting
|
||||||
|
/// can be traced back through the GL.
|
||||||
|
/// </summary>
|
||||||
|
public class FixedAssetDepreciationEntry : BaseEntity
|
||||||
|
{
|
||||||
|
public int FixedAssetId { get; set; }
|
||||||
|
public int PeriodYear { get; set; }
|
||||||
|
public int PeriodMonth { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
/// <summary>The JE that was posted for this depreciation period (null if manually recorded).</summary>
|
||||||
|
public int? JournalEntryId { get; set; }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
public virtual FixedAsset FixedAsset { get; set; } = null!;
|
||||||
|
public virtual JournalEntry? JournalEntry { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A named annual budget. Contains one BudgetLine per account per month. Supports
|
||||||
|
/// multiple budgets per fiscal year (e.g. "Conservative" vs "Optimistic") but only
|
||||||
|
/// one is marked IsDefault for the Budget vs. Actual report.
|
||||||
|
/// </summary>
|
||||||
|
public class Budget : BaseEntity
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int FiscalYear { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public bool IsDefault { get; set; } = false;
|
||||||
|
|
||||||
|
public virtual ICollection<BudgetLine> Lines { get; set; } = new List<BudgetLine>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monthly budget amount for one account within a Budget. Jan–Dec stored as separate
|
||||||
|
/// columns so the grid editor can write them in a single POST without a line-item loop.
|
||||||
|
/// Annual is a computed property summing all twelve months.
|
||||||
|
/// </summary>
|
||||||
|
public class BudgetLine : BaseEntity
|
||||||
|
{
|
||||||
|
public int BudgetId { get; set; }
|
||||||
|
public int AccountId { get; set; }
|
||||||
|
|
||||||
|
public decimal Jan { get; set; }
|
||||||
|
public decimal Feb { get; set; }
|
||||||
|
public decimal Mar { get; set; }
|
||||||
|
public decimal Apr { get; set; }
|
||||||
|
public decimal May { get; set; }
|
||||||
|
public decimal Jun { get; set; }
|
||||||
|
public decimal Jul { get; set; }
|
||||||
|
public decimal Aug { get; set; }
|
||||||
|
public decimal Sep { get; set; }
|
||||||
|
public decimal Oct { get; set; }
|
||||||
|
public decimal Nov { get; set; }
|
||||||
|
public decimal Dec { get; set; }
|
||||||
|
|
||||||
|
public decimal Annual => Jan + Feb + Mar + Apr + May + Jun + Jul + Aug + Sep + Oct + Nov + Dec;
|
||||||
|
|
||||||
|
public virtual Budget Budget { get; set; } = null!;
|
||||||
|
public virtual Account Account { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records a completed year-end close. The close posts a JE that zeroes all
|
||||||
|
/// Revenue and Expense account balances into Retained Earnings, and marks
|
||||||
|
/// the year as closed so it cannot be closed again.
|
||||||
|
/// </summary>
|
||||||
|
public class YearEndClose : BaseEntity
|
||||||
|
{
|
||||||
|
public int ClosedYear { get; set; }
|
||||||
|
public DateTime ClosedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public string? ClosedBy { get; set; }
|
||||||
|
public int JournalEntryId { get; set; }
|
||||||
|
|
||||||
|
public virtual JournalEntry JournalEntry { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ public class ApplicationUser : IdentityUser
|
|||||||
public bool CanManageMaintenance { get; set; } = false;
|
public bool CanManageMaintenance { get; set; } = false;
|
||||||
public bool CanManageInvoices { get; set; } = false;
|
public bool CanManageInvoices { get; set; } = false;
|
||||||
public bool CanViewReports { get; set; } = false;
|
public bool CanViewReports { get; set; } = false;
|
||||||
|
public bool CanManageBills { get; set; } = false;
|
||||||
|
public bool CanManageAccounting { get; set; } = false;
|
||||||
|
|
||||||
// Profile Photo (filesystem storage)
|
// Profile Photo (filesystem storage)
|
||||||
public string? ProfilePictureFilePath { get; set; } // Relative path from ContentRoot/media/ (e.g., "123/profile-photos/user-abc.jpg")
|
public string? ProfilePictureFilePath { get; set; } // Relative path from ContentRoot/media/ (e.g., "123/profile-photos/user-abc.jpg")
|
||||||
|
|||||||
@@ -105,11 +105,34 @@ public class Company : BaseEntity
|
|||||||
public bool MarketingEmailOptOut { get; set; } = false;
|
public bool MarketingEmailOptOut { get; set; } = false;
|
||||||
public string MarketingUnsubscribeToken { get; set; } = Guid.NewGuid().ToString("N");
|
public string MarketingUnsubscribeToken { get; set; } = Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether financial reports (P&L, Balance Sheet, Cash Flow) use
|
||||||
|
/// cash-basis or accrual-basis presentation. Switchable at any time — no GL
|
||||||
|
/// re-posting occurs. Default is Accrual (standard for most businesses).
|
||||||
|
/// </summary>
|
||||||
|
public AccountingMethod AccountingMethod { get; set; } = AccountingMethod.Accrual;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When set, prevents creating or editing accounting entries (JEs, bills, expenses) with dates
|
||||||
|
/// on or before this date. Protects closed periods from accidental backdating. Null = no lock.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? BookLockedThrough { get; set; }
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
public string? TimeZone { get; set; } = "America/New_York";
|
public string? TimeZone { get; set; } = "America/New_York";
|
||||||
public byte[]? LogoData { get; set; } // Legacy - kept for backward compatibility
|
public byte[]? LogoData { get; set; } // Legacy - kept for backward compatibility
|
||||||
public string? LogoContentType { get; set; } // Legacy - kept for backward compatibility
|
public string? LogoContentType { get; set; } // Legacy - kept for backward compatibility
|
||||||
public string? LogoFilePath { get; set; } // Filesystem path: /media/{CompanyId}/company-logo.{ext}
|
public string? LogoFilePath { get; set; } // Filesystem path: /media/{CompanyId}/company-logo.{ext}
|
||||||
|
|
||||||
|
// Kiosk
|
||||||
|
/// <summary>
|
||||||
|
/// Random token written to a long-lived HttpOnly cookie on the front-desk tablet when the
|
||||||
|
/// owner activates the kiosk. Kiosk routes validate this token against the cookie so the
|
||||||
|
/// tablet can serve the intake form without requiring a logged-in user.
|
||||||
|
/// Null = kiosk not activated. Regenerate to revoke the current device.
|
||||||
|
/// </summary>
|
||||||
|
public string? KioskActivationToken { get; set; }
|
||||||
|
|
||||||
// Navigation Properties
|
// Navigation Properties
|
||||||
public virtual ICollection<ApplicationUser> Users { get; set; } = new List<ApplicationUser>();
|
public virtual ICollection<ApplicationUser> Users { get; set; } = new List<ApplicationUser>();
|
||||||
public virtual ICollection<Customer> Customers { get; set; } = new List<Customer>();
|
public virtual ICollection<Customer> Customers { get; set; } = new List<Customer>();
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ public class CompanyPreferences : BaseEntity
|
|||||||
/// <summary>JSON blob persisting QB Migration Wizard step state across sessions.</summary>
|
/// <summary>JSON blob persisting QB Migration Wizard step state across sessions.</summary>
|
||||||
public string? QbMigrationStateJson { get; set; }
|
public string? QbMigrationStateJson { get; set; }
|
||||||
|
|
||||||
|
// Kiosk settings
|
||||||
|
/// <summary>
|
||||||
|
/// Controls what the kiosk creates on submission: "Quote" (default) or "Job".
|
||||||
|
/// Quote aligns with the default Terms text ("subject to a formal quote").
|
||||||
|
/// Job is for shops that price on the spot and want the work order ready immediately.
|
||||||
|
/// </summary>
|
||||||
|
public string KioskIntakeOutput { get; set; } = "Quote";
|
||||||
|
|
||||||
// Guided activation / first-workflow onboarding
|
// Guided activation / first-workflow onboarding
|
||||||
/// <summary>Selected first-workflow path: quote_first or job_first. Null until chosen.</summary>
|
/// <summary>Selected first-workflow path: quote_first or job_first. Null until chosen.</summary>
|
||||||
public string? OnboardingPath { get; set; }
|
public string? OnboardingPath { get; set; }
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ public class Deposit : BaseEntity
|
|||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public string? RecordedById { get; set; }
|
public string? RecordedById { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Bank/checking account this deposit was deposited into. Set at recording time
|
||||||
|
/// so the Trial Balance can immediately debit the correct bank account.</summary>
|
||||||
|
public int? DepositAccountId { get; set; }
|
||||||
|
|
||||||
// Applied to invoice when invoice is created
|
// Applied to invoice when invoice is created
|
||||||
public int? AppliedToInvoiceId { get; set; }
|
public int? AppliedToInvoiceId { get; set; }
|
||||||
public DateTime? AppliedDate { get; set; }
|
public DateTime? AppliedDate { get; set; }
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ public class GiftCertificate : BaseEntity
|
|||||||
/// <summary>Set when this GC was sold via an invoice line item.</summary>
|
/// <summary>Set when this GC was sold via an invoice line item.</summary>
|
||||||
public int? SourceInvoiceItemId { get; set; }
|
public int? SourceInvoiceItemId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Groups all certificates created in a single bulk run. Null for individually issued certs.</summary>
|
||||||
|
public Guid? BatchId { get; set; }
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
public virtual Customer? RecipientCustomer { get; set; }
|
public virtual Customer? RecipientCustomer { get; set; }
|
||||||
public virtual Customer? PurchasingCustomer { get; set; }
|
public virtual Customer? PurchasingCustomer { get; set; }
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ public class Invoice : BaseEntity
|
|||||||
public decimal GiftCertificateRedeemed { get; set; } // Sum of gift certificate redemptions
|
public decimal GiftCertificateRedeemed { get; set; } // Sum of gift certificate redemptions
|
||||||
public decimal BalanceDue => Total - AmountPaid - CreditApplied - GiftCertificateRedeemed;
|
public decimal BalanceDue => Total - AmountPaid - CreditApplied - GiftCertificateRedeemed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Permanent public token for the customer-facing invoice view page (/invoice/{token}).
|
||||||
|
/// Generated when the invoice is first sent (regardless of Stripe status) and never expires.
|
||||||
|
/// Distinct from PaymentLinkToken which is Stripe-gated and expires in 5 days.
|
||||||
|
/// </summary>
|
||||||
|
public string? PublicViewToken { get; set; }
|
||||||
|
|
||||||
// Online payments (Stripe Connect)
|
// Online payments (Stripe Connect)
|
||||||
public OnlinePaymentStatus OnlinePaymentStatus { get; set; } = OnlinePaymentStatus.NotApplicable;
|
public OnlinePaymentStatus OnlinePaymentStatus { get; set; } = OnlinePaymentStatus.NotApplicable;
|
||||||
public string? PaymentLinkToken { get; set; } // Signed token for /pay/{token}
|
public string? PaymentLinkToken { get; set; } // Signed token for /pay/{token}
|
||||||
@@ -42,6 +49,19 @@ public class Invoice : BaseEntity
|
|||||||
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 (e.g., 2 means 2% discount).
|
||||||
|
/// Parsed from the customer's payment terms when the invoice is created (e.g., "2/10 Net 30").
|
||||||
|
/// Informational only — does not automatically reduce the amount due.
|
||||||
|
/// </summary>
|
||||||
|
public decimal EarlyPaymentDiscountPercent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of days after invoice date within which the early payment discount applies.
|
||||||
|
/// Parsed from the customer's payment terms (e.g., "2/10 Net 30" → 10 days).
|
||||||
|
/// </summary>
|
||||||
|
public int EarlyPaymentDiscountDays { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Original invoice number from an external system (e.g. QuickBooks invoice # "3048").
|
/// Original invoice number from an external system (e.g. QuickBooks invoice # "3048").
|
||||||
/// Stored for searchability and traceability after import. Searchable from the invoice list.
|
/// Stored for searchability and traceability after import. Searchable from the invoice list.
|
||||||
|
|||||||
@@ -25,9 +25,14 @@ public class Job : BaseEntity
|
|||||||
// Selected oven (carried over from quote; null = company default rate)
|
// Selected oven (carried over from quote; null = company default rate)
|
||||||
public int? OvenCostId { get; set; }
|
public int? OvenCostId { get; set; }
|
||||||
|
|
||||||
|
// Oven scheduling (carried over from quote)
|
||||||
|
public int OvenBatches { get; set; } = 1;
|
||||||
|
public int? OvenCycleMinutes { get; set; }
|
||||||
|
|
||||||
// Pricing
|
// Pricing
|
||||||
public decimal QuotedPrice { get; set; }
|
public decimal QuotedPrice { get; set; }
|
||||||
public decimal FinalPrice { get; set; }
|
public decimal FinalPrice { get; set; }
|
||||||
|
public decimal OvenBatchCost { get; set; }
|
||||||
public decimal ShopSuppliesAmount { get; set; }
|
public decimal ShopSuppliesAmount { get; set; }
|
||||||
public decimal ShopSuppliesPercent { get; set; }
|
public decimal ShopSuppliesPercent { get; set; }
|
||||||
|
|
||||||
@@ -61,6 +66,10 @@ public class Job : BaseEntity
|
|||||||
// Used to detect when the quote was subsequently edited so the job details page can warn the user.
|
// Used to detect when the quote was subsequently edited so the job details page can warn the user.
|
||||||
public DateTime? QuoteSnapshotUpdatedAt { get; set; }
|
public DateTime? QuoteSnapshotUpdatedAt { get; set; }
|
||||||
|
|
||||||
|
// Pricing snapshot — serialized QuotePricingBreakdownDto stored at save time so Details displays
|
||||||
|
// the breakdown that was actually calculated, not a re-run against current operating costs.
|
||||||
|
public string? PricingBreakdownJson { get; set; }
|
||||||
|
|
||||||
// Rework tracking
|
// Rework tracking
|
||||||
public bool IsReworkJob { get; set; }
|
public bool IsReworkJob { get; set; }
|
||||||
public int? OriginalJobId { get; set; } // Set when this job was created as a rework
|
public int? OriginalJobId { get; set; } // Set when this job was created as a rework
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ public class JobItem : BaseEntity
|
|||||||
// Values: "Simple" | "Moderate" | "Complex" | "Extreme"
|
// Values: "Simple" | "Moderate" | "Complex" | "Extreme"
|
||||||
public string? Complexity { get; set; }
|
public string? Complexity { get; set; }
|
||||||
|
|
||||||
|
// True when this item originated from an AI Photo Quote — ManualUnitPrice is used as-is
|
||||||
|
// and oven cost is not double-charged (it was excluded from the AI estimate at quote level).
|
||||||
|
public bool IsAiItem { get; set; }
|
||||||
|
|
||||||
// AI-generated standardized tags (comma-separated, e.g. "automotive,tubular")
|
// AI-generated standardized tags (comma-separated, e.g. "automotive,tubular")
|
||||||
public string? AiTags { get; set; }
|
public string? AiTags { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ public class JobTemplateItem : BaseEntity
|
|||||||
public int? CatalogItemId { get; set; }
|
public int? CatalogItemId { get; set; }
|
||||||
public bool IsGenericItem { get; set; }
|
public bool IsGenericItem { get; set; }
|
||||||
public bool IsLaborItem { get; set; }
|
public bool IsLaborItem { get; set; }
|
||||||
|
public bool IsSalesItem { get; set; }
|
||||||
|
public string? Sku { get; set; }
|
||||||
public decimal? ManualUnitPrice { get; set; }
|
public decimal? ManualUnitPrice { get; set; }
|
||||||
public bool RequiresSandblasting { get; set; }
|
public bool RequiresSandblasting { get; set; }
|
||||||
public bool RequiresMasking { get; set; }
|
public bool RequiresMasking { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using PowderCoating.Core.Enums;
|
||||||
|
|
||||||
|
namespace PowderCoating.Core.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents one customer self-service intake session — either completed on the front-desk tablet
|
||||||
|
/// (InPerson) or via an emailed link the customer fills out on their own device (Remote).
|
||||||
|
/// Sessions are tenant-scoped and soft-deletable. Load anonymous sessions with ignoreQueryFilters:true.
|
||||||
|
/// </summary>
|
||||||
|
public class KioskSession : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>URL-safe GUID used in all kiosk routes; unique across the table.</summary>
|
||||||
|
public Guid SessionToken { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public KioskSessionType SessionType { get; set; }
|
||||||
|
public KioskSessionStatus Status { get; set; } = KioskSessionStatus.Active;
|
||||||
|
|
||||||
|
// ── Step 1 — Contact ─────────────────────────────────────────────────────
|
||||||
|
public string CustomerFirstName { get; set; } = string.Empty;
|
||||||
|
public string CustomerLastName { get; set; } = string.Empty;
|
||||||
|
public string CustomerPhone { get; set; } = string.Empty;
|
||||||
|
public string CustomerEmail { get; set; } = string.Empty;
|
||||||
|
public bool IsReturningCustomer { get; set; }
|
||||||
|
|
||||||
|
// ── Step 2 — Job Description ──────────────────────────────────────────────
|
||||||
|
public string JobDescription { get; set; } = string.Empty;
|
||||||
|
public string? HowDidYouHearAboutUs { get; set; }
|
||||||
|
|
||||||
|
// ── Step 3 — Terms & Consent ──────────────────────────────────────────────
|
||||||
|
public bool AgreedToTerms { get; set; }
|
||||||
|
public DateTime? AgreedToTermsAt { get; set; }
|
||||||
|
/// <summary>Customer opted in to SMS order updates; sets Customer.NotifyBySms on submission.</summary>
|
||||||
|
public bool SmsOptIn { get; set; }
|
||||||
|
/// <summary>Base-64 PNG from signature_pad; null for Remote sessions (no drawn signature required).</summary>
|
||||||
|
public string? SignatureDataBase64 { get; set; }
|
||||||
|
|
||||||
|
// ── Outcome ───────────────────────────────────────────────────────────────
|
||||||
|
public int? LinkedCustomerId { get; set; }
|
||||||
|
/// <summary>Set when KioskIntakeOutput = "Job". Null when a Quote was created instead.</summary>
|
||||||
|
public int? LinkedJobId { get; set; }
|
||||||
|
/// <summary>Set when KioskIntakeOutput = "Quote". Null when a Job was created instead.</summary>
|
||||||
|
public int? LinkedQuoteId { get; set; }
|
||||||
|
public DateTime? SubmittedAt { get; set; }
|
||||||
|
/// <summary>Sessions auto-expire 2 h after creation (InPerson) or 48 h (Remote). ExpiresAt is set at creation.</summary>
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
|
||||||
|
// ── Remote-only ───────────────────────────────────────────────────────────
|
||||||
|
public string? RemoteLinkEmail { get; set; }
|
||||||
|
public DateTime? RemoteLinkSentAt { get; set; }
|
||||||
|
|
||||||
|
// ── Navigation ────────────────────────────────────────────────────────────
|
||||||
|
public virtual Customer? LinkedCustomer { get; set; }
|
||||||
|
public virtual Job? LinkedJob { get; set; }
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ public class Payment : BaseEntity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? DepositAccountId { get; set; }
|
public int? DepositAccountId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>True once this payment has been matched against a bank statement during reconciliation.</summary>
|
||||||
|
public bool IsCleared { get; set; } = false;
|
||||||
|
public DateTime? ClearedDate { get; set; }
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
public virtual Invoice Invoice { get; set; } = null!;
|
public virtual Invoice Invoice { get; set; } = null!;
|
||||||
public virtual ApplicationUser? RecordedBy { get; set; }
|
public virtual ApplicationUser? RecordedBy { get; set; }
|
||||||
|
|||||||
@@ -40,26 +40,33 @@ public class Quote : BaseEntity
|
|||||||
public DateTime? ApprovedDate { get; set; }
|
public DateTime? ApprovedDate { get; set; }
|
||||||
|
|
||||||
// Pricing — all values are snapshots captured at save time and must not be recalculated on load
|
// Pricing — all values are snapshots captured at save time and must not be recalculated on load
|
||||||
public decimal MaterialCosts { get; set; } // Sum of powder/material costs across all items
|
public decimal MaterialCosts { get; set; } // Sum of powder/material costs across all items
|
||||||
public decimal LaborCosts { get; set; } // Sum of labor costs across all items
|
public decimal LaborCosts { get; set; } // Sum of labor costs across all items
|
||||||
public decimal EquipmentCosts { get; set; } // Sum of equipment costs across all items
|
public decimal EquipmentCosts { get; set; } // Sum of equipment costs across all items
|
||||||
public decimal ItemsSubtotal { get; set; } // Sum of item prices before any quote-level costs
|
public decimal ItemsSubtotal { get; set; } // Sum of item prices before any quote-level costs
|
||||||
public decimal OvenBatchCost { get; set; } // Oven batch charge applied at quote level
|
public decimal OvenBatchCost { get; set; } // Oven batch charge applied at quote level
|
||||||
public decimal ShopSuppliesAmount { get; set; } // Shop supplies dollar amount
|
public decimal FacilityOverheadCost { get; set; } // Rent + utilities apportioned by estimated job hours
|
||||||
public decimal ShopSuppliesPercent { get; set; } // Shop supplies percentage used
|
public decimal FacilityOverheadRatePerHour { get; set; }// Rate used for facility overhead ($/hr)
|
||||||
public decimal OverheadAmount { get; set; } // Overhead dollar amount
|
public decimal ShopSuppliesAmount { get; set; } // Shop supplies dollar amount
|
||||||
public decimal OverheadPercent { get; set; } // Overhead percentage used
|
public decimal ShopSuppliesPercent { get; set; } // Shop supplies percentage used
|
||||||
public decimal ProfitMargin { get; set; } // Profit margin dollar amount
|
public decimal OverheadAmount { get; set; } // Legacy overhead (now always 0; kept for migration safety)
|
||||||
public decimal ProfitPercent { get; set; } // Profit margin percentage used
|
public decimal OverheadPercent { get; set; } // Legacy overhead percent
|
||||||
public decimal SubTotal { get; set; } // SubtotalBeforeDiscount (items + oven + overhead + profit + shop supplies)
|
public decimal ProfitMargin { get; set; } // Profit margin dollar amount (0 — baked into item prices)
|
||||||
|
public decimal ProfitPercent { get; set; } // Markup % used (for display reference)
|
||||||
|
public decimal SubTotal { get; set; } // SubtotalBeforeDiscount (items + oven + facility overhead + shop supplies)
|
||||||
|
|
||||||
// Discount Information
|
// Discount Information
|
||||||
public DiscountType DiscountType { get; set; } = DiscountType.None;
|
public DiscountType DiscountType { get; set; } = DiscountType.None;
|
||||||
public decimal DiscountValue { get; set; } = 0; // Value entered by user (percentage or fixed amount)
|
public decimal DiscountValue { get; set; } = 0; // Value entered by user (percentage or fixed amount)
|
||||||
public decimal DiscountPercent { get; set; } // Calculated: actual percentage applied
|
public decimal PricingTierDiscountAmount { get; set; } // Discount from customer's pricing tier
|
||||||
public decimal DiscountAmount { get; set; } // Calculated: actual dollar amount deducted
|
public decimal PricingTierDiscountPercent { get; set; } // Tier discount percentage
|
||||||
public string? DiscountReason { get; set; } // Why discount was applied
|
public decimal QuoteDiscountAmount { get; set; } // Manual quote-level discount amount
|
||||||
|
public decimal QuoteDiscountPercent { get; set; } // Manual quote-level discount percentage
|
||||||
|
public decimal DiscountPercent { get; set; } // Combined: actual percentage applied
|
||||||
|
public decimal DiscountAmount { get; set; } // Combined: actual dollar amount deducted
|
||||||
|
public string? DiscountReason { get; set; } // Why discount was applied
|
||||||
public bool HideDiscountFromCustomer { get; set; } = false; // Show only total on PDFs/portal
|
public bool HideDiscountFromCustomer { get; set; } = false; // Show only total on PDFs/portal
|
||||||
|
public decimal SubtotalAfterDiscount { get; set; } // SubTotal minus all discounts, before rush/tax
|
||||||
|
|
||||||
public decimal TaxPercent { get; set; }
|
public decimal TaxPercent { get; set; }
|
||||||
public decimal TaxAmount { get; set; }
|
public decimal TaxAmount { get; set; }
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ public class Refund : BaseEntity
|
|||||||
public DateTime? IssuedDate { get; set; }
|
public DateTime? IssuedDate { get; set; }
|
||||||
public string? IssuedById { get; set; }
|
public string? IssuedById { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Bank/checking account the refund was paid from. Mirrors Payment.DepositAccountId so
|
||||||
|
/// the Trial Balance can credit this account when computing bank balance.</summary>
|
||||||
|
public int? DepositAccountId { get; set; }
|
||||||
|
|
||||||
// For store-credit refunds: the CreditMemo created on their behalf
|
// For store-credit refunds: the CreditMemo created on their behalf
|
||||||
public int? CreditMemoId { get; set; }
|
public int? CreditMemoId { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ public class Vendor : BaseEntity
|
|||||||
/// <summary>Default expense account pre-filled on new bill line items for this vendor.</summary>
|
/// <summary>Default expense account pre-filled on new bill line items for this vendor.</summary>
|
||||||
public int? DefaultExpenseAccountId { get; set; }
|
public int? DefaultExpenseAccountId { get; set; }
|
||||||
|
|
||||||
|
// 1099 Contractor tracking
|
||||||
|
/// <summary>When true, this vendor is an independent contractor subject to 1099-NEC reporting.</summary>
|
||||||
|
public bool Is1099Vendor { get; set; } = false;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
public virtual ICollection<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
|
public virtual ICollection<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
|
||||||
public virtual ICollection<Bill> Bills { get; set; } = new List<Bill>();
|
public virtual ICollection<Bill> Bills { get; set; } = new List<Bill>();
|
||||||
|
|||||||
@@ -66,3 +66,61 @@ public enum BillStatus
|
|||||||
Paid = 3,
|
Paid = 3,
|
||||||
Voided = 4
|
Voided = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Company-level accounting method preference. Affects how financial reports
|
||||||
|
/// (P&L, Balance Sheet, Cash Flow) query and present data. Switching this
|
||||||
|
/// setting never re-posts historical GL entries — it is a report-time choice only.
|
||||||
|
/// </summary>
|
||||||
|
public enum AccountingMethod
|
||||||
|
{
|
||||||
|
/// <summary>Revenue and expenses recognised when cash changes hands.</summary>
|
||||||
|
Cash = 0,
|
||||||
|
/// <summary>Revenue and expenses recognised when earned/incurred (default).</summary>
|
||||||
|
Accrual = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BankReconciliationStatus
|
||||||
|
{
|
||||||
|
InProgress = 0,
|
||||||
|
Completed = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VendorCreditStatus
|
||||||
|
{
|
||||||
|
Open = 0,
|
||||||
|
PartiallyApplied = 1,
|
||||||
|
Applied = 2,
|
||||||
|
Voided = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Source document type for a recurring template — controls which entity is created on each fire.</summary>
|
||||||
|
public enum RecurringTemplateType
|
||||||
|
{
|
||||||
|
/// <summary>Creates a vendor Bill (Draft, pending user review).</summary>
|
||||||
|
Bill = 1,
|
||||||
|
/// <summary>Creates a direct Expense entry (immediately recorded).</summary>
|
||||||
|
Expense = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>How often a recurring template fires.</summary>
|
||||||
|
public enum RecurringFrequency
|
||||||
|
{
|
||||||
|
Daily = 1,
|
||||||
|
Weekly = 2,
|
||||||
|
BiWeekly = 3,
|
||||||
|
Monthly = 4,
|
||||||
|
Quarterly = 5,
|
||||||
|
Annually = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Lifecycle state of a Manual Journal Entry.</summary>
|
||||||
|
public enum JournalEntryStatus
|
||||||
|
{
|
||||||
|
/// <summary>Not yet posted — can still be edited or deleted.</summary>
|
||||||
|
Draft = 0,
|
||||||
|
/// <summary>Posted to the GL — immutable; can only be reversed.</summary>
|
||||||
|
Posted = 1,
|
||||||
|
/// <summary>A reversal JE has been created and posted for this entry.</summary>
|
||||||
|
Reversed = 2
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace PowderCoating.Core.Enums;
|
||||||
|
|
||||||
|
public enum KioskSessionType
|
||||||
|
{
|
||||||
|
InPerson = 0,
|
||||||
|
Remote = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum KioskSessionStatus
|
||||||
|
{
|
||||||
|
Active = 0,
|
||||||
|
Submitted = 1,
|
||||||
|
Expired = 2,
|
||||||
|
Cancelled = 3
|
||||||
|
}
|
||||||
@@ -91,6 +91,35 @@ public interface IUnitOfWork : IDisposable
|
|||||||
IRepository<BillPayment> BillPayments { get; }
|
IRepository<BillPayment> BillPayments { get; }
|
||||||
IRepository<Expense> Expenses { get; }
|
IRepository<Expense> Expenses { get; }
|
||||||
|
|
||||||
|
// Manual Journal Entries
|
||||||
|
IRepository<JournalEntry> JournalEntries { get; }
|
||||||
|
IRepository<JournalEntryLine> JournalEntryLines { get; }
|
||||||
|
|
||||||
|
// Vendor Credits
|
||||||
|
IRepository<VendorCredit> VendorCredits { get; }
|
||||||
|
IRepository<VendorCreditLineItem> VendorCreditLineItems { get; }
|
||||||
|
IRepository<VendorCreditApplication> VendorCreditApplications { get; }
|
||||||
|
|
||||||
|
// Bank Reconciliation
|
||||||
|
IRepository<BankReconciliation> BankReconciliations { get; }
|
||||||
|
|
||||||
|
// Tax Rates
|
||||||
|
IRepository<TaxRate> TaxRates { get; }
|
||||||
|
|
||||||
|
// Recurring Transactions
|
||||||
|
IRepository<RecurringTemplate> RecurringTemplates { get; }
|
||||||
|
|
||||||
|
// Fixed Assets
|
||||||
|
IRepository<FixedAsset> FixedAssets { get; }
|
||||||
|
IRepository<FixedAssetDepreciationEntry> FixedAssetDepreciationEntries { get; }
|
||||||
|
|
||||||
|
// Budgeting
|
||||||
|
IRepository<Budget> Budgets { get; }
|
||||||
|
IRepository<BudgetLine> BudgetLines { get; }
|
||||||
|
|
||||||
|
// Year-End Close
|
||||||
|
IRepository<YearEndClose> YearEndCloses { get; }
|
||||||
|
|
||||||
// Notifications — typed repository for IgnoreQueryFilters-based history lookups
|
// Notifications — typed repository for IgnoreQueryFilters-based history lookups
|
||||||
INotificationLogRepository NotificationLogs { get; }
|
INotificationLogRepository NotificationLogs { get; }
|
||||||
IRepository<NotificationTemplate> NotificationTemplates { get; }
|
IRepository<NotificationTemplate> NotificationTemplates { get; }
|
||||||
@@ -125,6 +154,9 @@ public interface IUnitOfWork : IDisposable
|
|||||||
IRepository<GiftCertificate> GiftCertificates { get; }
|
IRepository<GiftCertificate> GiftCertificates { get; }
|
||||||
IRepository<GiftCertificateRedemption> GiftCertificateRedemptions { get; }
|
IRepository<GiftCertificateRedemption> GiftCertificateRedemptions { get; }
|
||||||
|
|
||||||
|
// Customer Intake Kiosk
|
||||||
|
IRepository<KioskSession> KioskSessions { get; }
|
||||||
|
|
||||||
Task<int> SaveChangesAsync();
|
Task<int> SaveChangesAsync();
|
||||||
Task<int> CompleteAsync(); // Alias for SaveChangesAsync
|
Task<int> CompleteAsync(); // Alias for SaveChangesAsync
|
||||||
|
|
||||||
|
|||||||
@@ -85,4 +85,11 @@ public interface IJobRepository : IRepository<Job>
|
|||||||
/// Returns null if not found or soft-deleted.
|
/// Returns null if not found or soft-deleted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Job?> LoadForTemplateSnapshotAsync(int jobId);
|
Task<Job?> LoadForTemplateSnapshotAsync(int jobId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all non-terminal jobs whose <c>ScheduledDate</c> is before today and not null,
|
||||||
|
/// ordered by scheduled date then job number. Used by the Daily Board to surface jobs that
|
||||||
|
/// were never completed and rolled past their scheduled day.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<Job>> GetOverdueScheduledJobsAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,17 @@ public record CompanyWizardInfo(bool Completed, DateTime? CompletedAt, string? C
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-company entity count summary used to populate the Index list without N+1 round-trips.
|
/// Per-company entity count summary used to populate the Index list without N+1 round-trips.
|
||||||
|
/// Also carries health-signal data (jobs30, jobs90, last login) so callers can compute a
|
||||||
|
/// <c>ChurnRisk</c> badge without a separate round-trip.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CompanyCountSummary(
|
public record CompanyCountSummary(
|
||||||
IReadOnlyDictionary<int, int> JobCounts,
|
IReadOnlyDictionary<int, int> JobCounts,
|
||||||
IReadOnlyDictionary<int, int> QuoteCounts,
|
IReadOnlyDictionary<int, int> QuoteCounts,
|
||||||
IReadOnlyDictionary<int, int> CustomerCounts,
|
IReadOnlyDictionary<int, int> CustomerCounts,
|
||||||
IReadOnlyDictionary<int, CompanyWizardInfo> WizardInfo
|
IReadOnlyDictionary<int, CompanyWizardInfo> WizardInfo,
|
||||||
|
IReadOnlyDictionary<int, int> Jobs30Counts,
|
||||||
|
IReadOnlyDictionary<int, int> Jobs90Counts,
|
||||||
|
IReadOnlyDictionary<int, DateTime?> LastLoginDates
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -26,10 +31,13 @@ public interface ICompanyListService
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a paged, searched, and sorted slice of non-deleted companies together with the
|
/// Returns a paged, searched, and sorted slice of non-deleted companies together with the
|
||||||
/// total unfiltered count for pagination.
|
/// total count for pagination and the count of churned accounts that are currently hidden.
|
||||||
|
/// When <paramref name="hideChurned"/> is true, Expired/Canceled companies whose subscription
|
||||||
|
/// ended more than 14 days ago are excluded from results (but still counted for the banner).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<(List<Company> Companies, int TotalCount)> GetPagedAsync(
|
Task<(List<Company> Companies, int TotalCount, int ChurnedCount)> GetPagedAsync(
|
||||||
string? searchTerm, string sortColumn, string sortDirection, int page, int pageSize);
|
string? searchTerm, string sortColumn, string sortDirection, int page, int pageSize,
|
||||||
|
bool hideChurned = true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns job, quote, customer, and wizard completion counts for each of the supplied
|
/// Returns job, quote, customer, and wizard completion counts for each of the supplied
|
||||||
|
|||||||
@@ -324,6 +324,39 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
|||||||
/// <summary>Ad-hoc expense records (non-bill spending); tenant-filtered with soft delete.</summary>
|
/// <summary>Ad-hoc expense records (non-bill spending); tenant-filtered with soft delete.</summary>
|
||||||
public DbSet<Expense> Expenses { get; set; }
|
public DbSet<Expense> Expenses { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Manual double-entry journal entries (Draft/Posted/Reversed lifecycle); tenant-filtered with soft delete.</summary>
|
||||||
|
public DbSet<JournalEntry> JournalEntries { get; set; }
|
||||||
|
/// <summary>Individual debit/credit lines within a journal entry; soft-delete only (access controlled through parent JournalEntry).</summary>
|
||||||
|
public DbSet<JournalEntryLine> JournalEntryLines { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Bank reconciliation sessions matching GL transactions to bank statements; tenant-filtered with soft delete.</summary>
|
||||||
|
public DbSet<BankReconciliation> BankReconciliations { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Named tax rates used to pre-fill invoice tax percent by jurisdiction; tenant-filtered with soft delete.</summary>
|
||||||
|
public DbSet<TaxRate> TaxRates { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Recurring transaction templates that auto-generate bills or expenses on a schedule; tenant-filtered with soft delete.</summary>
|
||||||
|
public DbSet<RecurringTemplate> RecurringTemplates { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Fixed assets subject to straight-line depreciation; tenant-filtered with soft delete.</summary>
|
||||||
|
public DbSet<FixedAsset> FixedAssets { get; set; }
|
||||||
|
/// <summary>One record per asset per period for each depreciation posting; soft-delete only.</summary>
|
||||||
|
public DbSet<FixedAssetDepreciationEntry> FixedAssetDepreciationEntries { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Named annual budgets with monthly amounts per GL account; tenant-filtered with soft delete.</summary>
|
||||||
|
public DbSet<Budget> Budgets { get; set; }
|
||||||
|
/// <summary>One row per account per Budget; contains Jan–Dec decimal columns.</summary>
|
||||||
|
public DbSet<BudgetLine> BudgetLines { get; set; }
|
||||||
|
/// <summary>Audit trail of completed year-end closes; tenant-filtered with soft delete.</summary>
|
||||||
|
public DbSet<YearEndClose> YearEndCloses { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Credit notes received from vendors (returned goods, pricing disputes); tenant-filtered with soft delete.</summary>
|
||||||
|
public DbSet<VendorCredit> VendorCredits { get; set; }
|
||||||
|
/// <summary>Expense-reversal line items on a vendor credit; soft-delete only.</summary>
|
||||||
|
public DbSet<VendorCreditLineItem> VendorCreditLineItems { get; set; }
|
||||||
|
/// <summary>Application records linking a vendor credit to a specific bill; soft-delete only.</summary>
|
||||||
|
public DbSet<VendorCreditApplication> VendorCreditApplications { get; set; }
|
||||||
|
|
||||||
// Job Templates
|
// Job Templates
|
||||||
/// <summary>Reusable job templates that pre-populate job items, coats, and prep services on job creation.</summary>
|
/// <summary>Reusable job templates that pre-populate job items, coats, and prep services on job creation.</summary>
|
||||||
public DbSet<JobTemplate> JobTemplates { get; set; }
|
public DbSet<JobTemplate> JobTemplates { get; set; }
|
||||||
@@ -334,6 +367,10 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
|||||||
/// <summary>Prep-service definitions within a job template item.</summary>
|
/// <summary>Prep-service definitions within a job template item.</summary>
|
||||||
public DbSet<JobTemplateItemPrepService> JobTemplateItemPrepServices { get; set; }
|
public DbSet<JobTemplateItemPrepService> JobTemplateItemPrepServices { get; set; }
|
||||||
|
|
||||||
|
// Customer Intake Kiosk
|
||||||
|
/// <summary>Customer self-service intake sessions (walk-in tablet or remote email link); tenant-filtered with soft delete.</summary>
|
||||||
|
public DbSet<KioskSession> KioskSessions { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Platform-wide audit log capturing who changed what and when, across all tenants.
|
/// Platform-wide audit log capturing who changed what and when, across all tenants.
|
||||||
/// No global query filter — SuperAdmin controllers query this directly.
|
/// No global query filter — SuperAdmin controllers query this directly.
|
||||||
@@ -614,6 +651,93 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
|||||||
modelBuilder.Entity<Expense>().HasQueryFilter(e =>
|
modelBuilder.Entity<Expense>().HasQueryFilter(e =>
|
||||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
|
||||||
|
// Journal Entries: tenant-filtered; lines use soft-delete only (child rows)
|
||||||
|
modelBuilder.Entity<JournalEntry>().HasQueryFilter(e =>
|
||||||
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
modelBuilder.Entity<JournalEntryLine>().HasQueryFilter(e => !e.IsDeleted);
|
||||||
|
|
||||||
|
// Bank Reconciliation: tenant-filtered
|
||||||
|
modelBuilder.Entity<BankReconciliation>().HasQueryFilter(e =>
|
||||||
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
|
||||||
|
// Tax Rates: tenant-filtered
|
||||||
|
modelBuilder.Entity<TaxRate>().HasQueryFilter(e =>
|
||||||
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
|
||||||
|
// Recurring Templates: tenant-filtered
|
||||||
|
modelBuilder.Entity<RecurringTemplate>().HasQueryFilter(e =>
|
||||||
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
|
||||||
|
// Fixed Assets: tenant-filtered with soft delete; depreciation entries soft-delete only
|
||||||
|
modelBuilder.Entity<FixedAsset>().HasQueryFilter(e =>
|
||||||
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
modelBuilder.Entity<FixedAssetDepreciationEntry>().HasQueryFilter(e => !e.IsDeleted);
|
||||||
|
|
||||||
|
// FixedAsset → Account (three FKs): NoAction to avoid cascade conflicts; Account has no
|
||||||
|
// reverse collection for FixedAssets so WithMany() is anonymous for each.
|
||||||
|
modelBuilder.Entity<FixedAsset>()
|
||||||
|
.HasOne(fa => fa.AssetAccount)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(fa => fa.AssetAccountId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
modelBuilder.Entity<FixedAsset>()
|
||||||
|
.HasOne(fa => fa.DepreciationExpenseAccount)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(fa => fa.DepreciationExpenseAccountId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
modelBuilder.Entity<FixedAsset>()
|
||||||
|
.HasOne(fa => fa.AccumDepreciationAccount)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(fa => fa.AccumDepreciationAccountId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// FixedAssetDepreciationEntry → JournalEntry: NoAction (entries outlive their JE)
|
||||||
|
modelBuilder.Entity<FixedAssetDepreciationEntry>()
|
||||||
|
.HasOne(e => e.JournalEntry)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.JournalEntryId)
|
||||||
|
.OnDelete(DeleteBehavior.NoAction);
|
||||||
|
|
||||||
|
// Budgets: tenant-filtered; BudgetLines soft-delete only
|
||||||
|
modelBuilder.Entity<Budget>().HasQueryFilter(e =>
|
||||||
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
modelBuilder.Entity<BudgetLine>().HasQueryFilter(e => !e.IsDeleted);
|
||||||
|
|
||||||
|
// BudgetLine → Account: Restrict delete so removing an account doesn't cascade into budget data
|
||||||
|
modelBuilder.Entity<BudgetLine>()
|
||||||
|
.HasOne(bl => bl.Account)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(bl => bl.AccountId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// YearEndClose: tenant-filtered; links to a specific JE
|
||||||
|
modelBuilder.Entity<YearEndClose>().HasQueryFilter(e =>
|
||||||
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
modelBuilder.Entity<YearEndClose>()
|
||||||
|
.HasOne(y => y.JournalEntry)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(y => y.JournalEntryId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// Vendor Credits: tenant-filtered; child rows soft-delete only
|
||||||
|
modelBuilder.Entity<VendorCredit>().HasQueryFilter(e =>
|
||||||
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
modelBuilder.Entity<VendorCreditLineItem>().HasQueryFilter(e => !e.IsDeleted);
|
||||||
|
modelBuilder.Entity<VendorCreditApplication>().HasQueryFilter(e => !e.IsDeleted);
|
||||||
|
|
||||||
|
// VendorCreditApplication: NoAction on both FKs to avoid SQL Server multiple-cascade-path error 1785.
|
||||||
|
// Bills and VendorCredits both cascade-delete through Vendor, creating two paths to VendorCreditApplications.
|
||||||
|
modelBuilder.Entity<VendorCreditApplication>()
|
||||||
|
.HasOne(vca => vca.Bill)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(vca => vca.BillId)
|
||||||
|
.OnDelete(DeleteBehavior.NoAction);
|
||||||
|
modelBuilder.Entity<VendorCreditApplication>()
|
||||||
|
.HasOne(vca => vca.VendorCredit)
|
||||||
|
.WithMany(vc => vc.Applications)
|
||||||
|
.HasForeignKey(vca => vca.VendorCreditId)
|
||||||
|
.OnDelete(DeleteBehavior.NoAction);
|
||||||
|
|
||||||
// Purchase Orders
|
// Purchase Orders
|
||||||
modelBuilder.Entity<PurchaseOrder>().HasQueryFilter(e =>
|
modelBuilder.Entity<PurchaseOrder>().HasQueryFilter(e =>
|
||||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
@@ -626,6 +750,24 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
|||||||
modelBuilder.Entity<InAppNotification>().HasQueryFilter(e =>
|
modelBuilder.Entity<InAppNotification>().HasQueryFilter(e =>
|
||||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
|
||||||
|
// Customer intake kiosk sessions — tenant-filtered + soft delete.
|
||||||
|
// Anonymous intake routes must use ignoreQueryFilters:true when loading by SessionToken.
|
||||||
|
modelBuilder.Entity<KioskSession>().HasQueryFilter(e =>
|
||||||
|
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||||
|
modelBuilder.Entity<KioskSession>()
|
||||||
|
.HasIndex(e => e.SessionToken)
|
||||||
|
.IsUnique();
|
||||||
|
modelBuilder.Entity<KioskSession>()
|
||||||
|
.HasOne(k => k.LinkedCustomer)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(k => k.LinkedCustomerId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
modelBuilder.Entity<KioskSession>()
|
||||||
|
.HasOne(k => k.LinkedJob)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(k => k.LinkedJobId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
// Account self-referencing hierarchy
|
// Account self-referencing hierarchy
|
||||||
modelBuilder.Entity<Account>()
|
modelBuilder.Entity<Account>()
|
||||||
.HasOne(a => a.ParentAccount)
|
.HasOne(a => a.ParentAccount)
|
||||||
@@ -633,6 +775,34 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
|||||||
.HasForeignKey(a => a.ParentAccountId)
|
.HasForeignKey(a => a.ParentAccountId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// JournalEntry self-referencing reversal link
|
||||||
|
modelBuilder.Entity<JournalEntry>()
|
||||||
|
.HasOne(je => je.ReversalOf)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(je => je.ReversalOfId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// BankReconciliation → Account (no cascade)
|
||||||
|
modelBuilder.Entity<BankReconciliation>()
|
||||||
|
.HasOne(br => br.Account)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(br => br.AccountId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// VendorCredit → APAccount (no cascade)
|
||||||
|
modelBuilder.Entity<VendorCredit>()
|
||||||
|
.HasOne(vc => vc.APAccount)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(vc => vc.APAccountId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// VendorCreditLineItem → Account (nullable, no cascade)
|
||||||
|
modelBuilder.Entity<VendorCreditLineItem>()
|
||||||
|
.HasOne(li => li.Account)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(li => li.AccountId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
// Vendor → DefaultExpenseAccount (no cascade)
|
// Vendor → DefaultExpenseAccount (no cascade)
|
||||||
modelBuilder.Entity<Vendor>()
|
modelBuilder.Entity<Vendor>()
|
||||||
.HasOne(s => s.DefaultExpenseAccount)
|
.HasOne(s => s.DefaultExpenseAccount)
|
||||||
|
|||||||
@@ -967,6 +967,17 @@ New accounts walk through an 18-step setup wizard to configure company informati
|
|||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
},
|
},
|
||||||
new NotificationTemplate
|
new NotificationTemplate
|
||||||
|
{
|
||||||
|
NotificationType = NotificationType.InvoiceSent,
|
||||||
|
Channel = NotificationChannel.Sms,
|
||||||
|
DisplayName = "Invoice Sent (SMS)",
|
||||||
|
Subject = null,
|
||||||
|
Body = "{{companyName}}: Invoice {{invoiceNumber}} for {{invoiceTotal}} is ready. View your invoice: {{viewUrl}} Reply STOP to opt out.",
|
||||||
|
IsActive = true,
|
||||||
|
CompanyId = companyId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
},
|
||||||
|
new NotificationTemplate
|
||||||
{
|
{
|
||||||
NotificationType = NotificationType.PaymentReceived,
|
NotificationType = NotificationType.PaymentReceived,
|
||||||
Channel = NotificationChannel.Email,
|
Channel = NotificationChannel.Email,
|
||||||
|
|||||||
src/PowderCoating.Infrastructure/Migrations/20260510011252_AddJobTemplateItemSalesFields.Designer.cs
Generated
+9552
File diff suppressed because it is too large
Load Diff
+82
@@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddJobTemplateItemSalesFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsSalesItem",
|
||||||
|
table: "JobTemplateItems",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Sku",
|
||||||
|
table: "JobTemplateItems",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2249));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2260));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2261));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsSalesItem",
|
||||||
|
table: "JobTemplateItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Sku",
|
||||||
|
table: "JobTemplateItems");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 8, 14, 21, 51, 589, DateTimeKind.Utc).AddTicks(4358));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 8, 14, 21, 51, 589, DateTimeKind.Utc).AddTicks(4424));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 8, 14, 21, 51, 589, DateTimeKind.Utc).AddTicks(4426));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+9555
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAccountingMethod : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "AccountingMethod",
|
||||||
|
table: "Companies",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 1); // 1 = Accrual (default for new and existing companies)
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 25, 9, 644, DateTimeKind.Utc).AddTicks(9957));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 25, 9, 644, DateTimeKind.Utc).AddTicks(9963));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 25, 9, 644, DateTimeKind.Utc).AddTicks(9965));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AccountingMethod",
|
||||||
|
table: "Companies");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2249));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2260));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2261));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+9715
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddJournalEntries : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "JournalEntries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
EntryNumber = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
EntryDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
Reference = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsReversal = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
ReversalOfId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
PostedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
PostedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_JournalEntries", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_JournalEntries_JournalEntries_ReversalOfId",
|
||||||
|
column: x => x.ReversalOfId,
|
||||||
|
principalTable: "JournalEntries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "JournalEntryLines",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
JournalEntryId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AccountId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
DebitAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
CreditAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
LineOrder = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_JournalEntryLines", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_JournalEntryLines_Accounts_AccountId",
|
||||||
|
column: x => x.AccountId,
|
||||||
|
principalTable: "Accounts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_JournalEntryLines_JournalEntries_JournalEntryId",
|
||||||
|
column: x => x.JournalEntryId,
|
||||||
|
principalTable: "JournalEntries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 45, 31, 524, DateTimeKind.Utc).AddTicks(9350));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 45, 31, 524, DateTimeKind.Utc).AddTicks(9357));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 45, 31, 524, DateTimeKind.Utc).AddTicks(9359));
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_JournalEntries_ReversalOfId",
|
||||||
|
table: "JournalEntries",
|
||||||
|
column: "ReversalOfId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_JournalEntryLines_AccountId",
|
||||||
|
table: "JournalEntryLines",
|
||||||
|
column: "AccountId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_JournalEntryLines_JournalEntryId",
|
||||||
|
table: "JournalEntryLines",
|
||||||
|
column: "JournalEntryId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "JournalEntryLines");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "JournalEntries");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 25, 9, 644, DateTimeKind.Utc).AddTicks(9957));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 25, 9, 644, DateTimeKind.Utc).AddTicks(9963));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 25, 9, 644, DateTimeKind.Utc).AddTicks(9965));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+9951
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,212 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddVendorCredits : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "VendorCredits",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
CreditNumber = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
VendorId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
APAccountId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreditDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Total = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
RemainingAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Memo = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_VendorCredits", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_VendorCredits_Accounts_APAccountId",
|
||||||
|
column: x => x.APAccountId,
|
||||||
|
principalTable: "Accounts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_VendorCredits_Vendors_VendorId",
|
||||||
|
column: x => x.VendorId,
|
||||||
|
principalTable: "Vendors",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "VendorCreditApplications",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
VendorCreditId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
BillId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Amount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
AppliedDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_VendorCreditApplications", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_Bills_BillId",
|
||||||
|
column: x => x.BillId,
|
||||||
|
principalTable: "Bills",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.NoAction);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_VendorCredits_VendorCreditId",
|
||||||
|
column: x => x.VendorCreditId,
|
||||||
|
principalTable: "VendorCredits",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.NoAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "VendorCreditLineItems",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
VendorCreditId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AccountId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Amount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_VendorCreditLineItems", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_VendorCreditLineItems_Accounts_AccountId",
|
||||||
|
column: x => x.AccountId,
|
||||||
|
principalTable: "Accounts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_VendorCreditLineItems_VendorCredits_VendorCreditId",
|
||||||
|
column: x => x.VendorCreditId,
|
||||||
|
principalTable: "VendorCredits",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 58, 27, 360, DateTimeKind.Utc).AddTicks(6994));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 58, 27, 360, DateTimeKind.Utc).AddTicks(7001));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 58, 27, 360, DateTimeKind.Utc).AddTicks(7003));
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VendorCreditApplications_BillId",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
column: "BillId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VendorCreditApplications_VendorCreditId",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
column: "VendorCreditId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VendorCreditLineItems_AccountId",
|
||||||
|
table: "VendorCreditLineItems",
|
||||||
|
column: "AccountId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VendorCreditLineItems_VendorCreditId",
|
||||||
|
table: "VendorCreditLineItems",
|
||||||
|
column: "VendorCreditId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VendorCredits_APAccountId",
|
||||||
|
table: "VendorCredits",
|
||||||
|
column: "APAccountId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VendorCredits_VendorId",
|
||||||
|
table: "VendorCredits",
|
||||||
|
column: "VendorId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "VendorCreditApplications");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "VendorCreditLineItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "VendorCredits");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 45, 31, 524, DateTimeKind.Utc).AddTicks(9350));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 45, 31, 524, DateTimeKind.Utc).AddTicks(9357));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 45, 31, 524, DateTimeKind.Utc).AddTicks(9359));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10043
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddBankReconciliation : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ClearedDate",
|
||||||
|
table: "Payments",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsCleared",
|
||||||
|
table: "Payments",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ClearedDate",
|
||||||
|
table: "Expenses",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsCleared",
|
||||||
|
table: "Expenses",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ClearedDate",
|
||||||
|
table: "BillPayments",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsCleared",
|
||||||
|
table: "BillPayments",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "BankReconciliations",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
AccountId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
StatementDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
BeginningBalance = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
EndingBalance = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CompletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_BankReconciliations", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_BankReconciliations_Accounts_AccountId",
|
||||||
|
column: x => x.AccountId,
|
||||||
|
principalTable: "Accounts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 4, 6, 6, 200, DateTimeKind.Utc).AddTicks(8472));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 4, 6, 6, 200, DateTimeKind.Utc).AddTicks(8478));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 4, 6, 6, 200, DateTimeKind.Utc).AddTicks(8479));
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BankReconciliations_AccountId",
|
||||||
|
table: "BankReconciliations",
|
||||||
|
column: "AccountId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "BankReconciliations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ClearedDate",
|
||||||
|
table: "Payments");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsCleared",
|
||||||
|
table: "Payments");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ClearedDate",
|
||||||
|
table: "Expenses");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsCleared",
|
||||||
|
table: "Expenses");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ClearedDate",
|
||||||
|
table: "BillPayments");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsCleared",
|
||||||
|
table: "BillPayments");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 58, 27, 360, DateTimeKind.Utc).AddTicks(6994));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 58, 27, 360, DateTimeKind.Utc).AddTicks(7001));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 3, 58, 27, 360, DateTimeKind.Utc).AddTicks(7003));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10105
File diff suppressed because it is too large
Load Diff
+112
@@ -0,0 +1,112 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPaymentTermsAndTaxRates : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "EarlyPaymentDiscountDays",
|
||||||
|
table: "Invoices",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "EarlyPaymentDiscountPercent",
|
||||||
|
table: "Invoices",
|
||||||
|
type: "decimal(18,2)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TaxRates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Rate = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
State = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDefault = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TaxRates", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 14, 48, 51, 545, DateTimeKind.Utc).AddTicks(3903));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 14, 48, 51, 545, DateTimeKind.Utc).AddTicks(3909));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 14, 48, 51, 545, DateTimeKind.Utc).AddTicks(3910));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TaxRates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EarlyPaymentDiscountDays",
|
||||||
|
table: "Invoices");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EarlyPaymentDiscountPercent",
|
||||||
|
table: "Invoices");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 4, 6, 6, 200, DateTimeKind.Utc).AddTicks(8472));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 4, 6, 6, 200, DateTimeKind.Utc).AddTicks(8478));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 4, 6, 6, 200, DateTimeKind.Utc).AddTicks(8479));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10186
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,171 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddRecurringTemplates : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_Bills_BillId",
|
||||||
|
table: "VendorCreditApplications");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_VendorCredits_VendorCreditId",
|
||||||
|
table: "VendorCreditApplications");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "VendorCreditId1",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "RecurringTemplates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
TemplateType = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Frequency = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IntervalCount = table.Column<int>(type: "int", nullable: false),
|
||||||
|
NextFireDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
EndDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
MaxOccurrences = table.Column<int>(type: "int", nullable: true),
|
||||||
|
OccurrenceCount = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
TemplateData = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
LastError = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_RecurringTemplates", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
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_Bills_BillId",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
column: "BillId",
|
||||||
|
principalTable: "Bills",
|
||||||
|
principalColumn: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_VendorCredits_VendorCreditId",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
column: "VendorCreditId",
|
||||||
|
principalTable: "VendorCredits",
|
||||||
|
principalColumn: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_VendorCredits_VendorCreditId1",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
column: "VendorCreditId1",
|
||||||
|
principalTable: "VendorCredits",
|
||||||
|
principalColumn: "Id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_Bills_BillId",
|
||||||
|
table: "VendorCreditApplications");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_VendorCredits_VendorCreditId",
|
||||||
|
table: "VendorCreditApplications");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_VendorCredits_VendorCreditId1",
|
||||||
|
table: "VendorCreditApplications");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "RecurringTemplates");
|
||||||
|
|
||||||
|
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, 14, 48, 51, 545, DateTimeKind.Utc).AddTicks(3903));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 14, 48, 51, 545, DateTimeKind.Utc).AddTicks(3909));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 14, 48, 51, 545, DateTimeKind.Utc).AddTicks(3910));
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_Bills_BillId",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
column: "BillId",
|
||||||
|
principalTable: "Bills",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_VendorCreditApplications_VendorCredits_VendorCreditId",
|
||||||
|
table: "VendorCreditApplications",
|
||||||
|
column: "VendorCreditId",
|
||||||
|
principalTable: "VendorCredits",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10366
File diff suppressed because it is too large
Load Diff
+199
@@ -0,0 +1,199 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFixedAssetsLockAnd1099 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "Is1099Vendor",
|
||||||
|
table: "Vendors",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "BookLockedThrough",
|
||||||
|
table: "Companies",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FixedAssets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
PurchaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
PurchaseCost = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
SalvageValue = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
UsefulLifeMonths = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AccumulatedDepreciation = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
IsDisposed = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DisposalDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
AssetAccountId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
DepreciationExpenseAccountId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
AccumDepreciationAccountId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FixedAssets", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FixedAssets_Accounts_AccumDepreciationAccountId",
|
||||||
|
column: x => x.AccumDepreciationAccountId,
|
||||||
|
principalTable: "Accounts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FixedAssets_Accounts_AssetAccountId",
|
||||||
|
column: x => x.AssetAccountId,
|
||||||
|
principalTable: "Accounts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FixedAssets_Accounts_DepreciationExpenseAccountId",
|
||||||
|
column: x => x.DepreciationExpenseAccountId,
|
||||||
|
principalTable: "Accounts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FixedAssetDepreciationEntries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
FixedAssetId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PeriodYear = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PeriodMonth = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Amount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
JournalEntryId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FixedAssetDepreciationEntries", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FixedAssetDepreciationEntries_FixedAssets_FixedAssetId",
|
||||||
|
column: x => x.FixedAssetId,
|
||||||
|
principalTable: "FixedAssets",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FixedAssetDepreciationEntries_JournalEntries_JournalEntryId",
|
||||||
|
column: x => x.JournalEntryId,
|
||||||
|
principalTable: "JournalEntries",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 0, 49, 626, DateTimeKind.Utc).AddTicks(4004));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 0, 49, 626, DateTimeKind.Utc).AddTicks(4009));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 0, 49, 626, DateTimeKind.Utc).AddTicks(4011));
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FixedAssetDepreciationEntries_FixedAssetId",
|
||||||
|
table: "FixedAssetDepreciationEntries",
|
||||||
|
column: "FixedAssetId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FixedAssetDepreciationEntries_JournalEntryId",
|
||||||
|
table: "FixedAssetDepreciationEntries",
|
||||||
|
column: "JournalEntryId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FixedAssets_AccumDepreciationAccountId",
|
||||||
|
table: "FixedAssets",
|
||||||
|
column: "AccumDepreciationAccountId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FixedAssets_AssetAccountId",
|
||||||
|
table: "FixedAssets",
|
||||||
|
column: "AssetAccountId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FixedAssets_DepreciationExpenseAccountId",
|
||||||
|
table: "FixedAssets",
|
||||||
|
column: "DepreciationExpenseAccountId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FixedAssetDepreciationEntries");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FixedAssets");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Is1099Vendor",
|
||||||
|
table: "Vendors");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BookLockedThrough",
|
||||||
|
table: "Companies");
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10585
File diff suppressed because it is too large
Load Diff
+185
@@ -0,0 +1,185 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddBudgetsAndYearEndClose : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Budgets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
FiscalYear = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDefault = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Budgets", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "YearEndCloses",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
ClosedYear = table.Column<int>(type: "int", nullable: false),
|
||||||
|
ClosedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
ClosedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
JournalEntryId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_YearEndCloses", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_YearEndCloses_JournalEntries_JournalEntryId",
|
||||||
|
column: x => x.JournalEntryId,
|
||||||
|
principalTable: "JournalEntries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "BudgetLines",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
BudgetId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AccountId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Jan = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Feb = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Mar = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Apr = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
May = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Jun = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Jul = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Aug = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Sep = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Oct = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Nov = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
Dec = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_BudgetLines", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_BudgetLines_Accounts_AccountId",
|
||||||
|
column: x => x.AccountId,
|
||||||
|
principalTable: "Accounts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_BudgetLines_Budgets_BudgetId",
|
||||||
|
column: x => x.BudgetId,
|
||||||
|
principalTable: "Budgets",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 55, 8, 322, DateTimeKind.Utc).AddTicks(966));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 55, 8, 322, DateTimeKind.Utc).AddTicks(974));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 55, 8, 322, DateTimeKind.Utc).AddTicks(976));
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BudgetLines_AccountId",
|
||||||
|
table: "BudgetLines",
|
||||||
|
column: "AccountId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BudgetLines_BudgetId",
|
||||||
|
table: "BudgetLines",
|
||||||
|
column: "BudgetId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_YearEndCloses_JournalEntryId",
|
||||||
|
table: "YearEndCloses",
|
||||||
|
column: "JournalEntryId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "BudgetLines");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "YearEndCloses");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Budgets");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 0, 49, 626, DateTimeKind.Utc).AddTicks(4004));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 0, 49, 626, DateTimeKind.Utc).AddTicks(4009));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 0, 49, 626, DateTimeKind.Utc).AddTicks(4011));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10591
File diff suppressed because it is too large
Load Diff
+90
@@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAccountantRolePermissions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "CanManageAccounting",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "CanManageBills",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
// Grant both new permissions to all existing CompanyAdmin users so they don't lose access
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
UPDATE AspNetUsers
|
||||||
|
SET CanManageBills = 1, CanManageAccounting = 1
|
||||||
|
WHERE CompanyRole = 'CompanyAdmin'
|
||||||
|
");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 23, 40, 54, 100, DateTimeKind.Utc).AddTicks(8999));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 23, 40, 54, 100, DateTimeKind.Utc).AddTicks(9005));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 23, 40, 54, 100, DateTimeKind.Utc).AddTicks(9007));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CanManageAccounting",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CanManageBills",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 55, 8, 322, DateTimeKind.Utc).AddTicks(966));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 55, 8, 322, DateTimeKind.Utc).AddTicks(974));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 16, 55, 8, 322, DateTimeKind.Utc).AddTicks(976));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10594
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddJobOvenBatchCost : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "OvenBatchCost",
|
||||||
|
table: "Jobs",
|
||||||
|
type: "decimal(18,2)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5837));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5846));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5847));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "OvenBatchCost",
|
||||||
|
table: "Jobs");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 23, 40, 54, 100, DateTimeKind.Utc).AddTicks(8999));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 23, 40, 54, 100, DateTimeKind.Utc).AddTicks(9005));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 10, 23, 40, 54, 100, DateTimeKind.Utc).AddTicks(9007));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10594
File diff suppressed because it is too large
Load Diff
+88
@@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMissingPlatformSettings : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// Conditional inserts — safe to run against a DB that already has some of these keys set manually.
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'SmsEnabled')
|
||||||
|
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||||
|
VALUES ('SmsEnabled','false','SMS Enabled','Platform-level switch for outbound SMS. When off, no SMS messages are sent regardless of company settings.','Notifications');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'TrialsEnabled')
|
||||||
|
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||||
|
VALUES ('TrialsEnabled','true','Trials Enabled','Allow new companies to register with a free trial period. When off, registration requires a paid plan immediately.','Subscriptions');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'GracePeriodDays')
|
||||||
|
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||||
|
VALUES ('GracePeriodDays','14','Grace Period (days)','Days after subscription expiry before access is fully cut off. Gives companies time to renew without an abrupt lockout.','Subscriptions');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'GracePeriodAppliesToTrials')
|
||||||
|
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||||
|
VALUES ('GracePeriodAppliesToTrials','false','Grace Period Applies to Trials','When enabled, trial companies also receive the grace period after expiry rather than being cut off immediately.','Subscriptions');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'MaxTenants')
|
||||||
|
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||||
|
VALUES ('MaxTenants','-1','Max Tenants','Maximum number of active tenant companies allowed on the platform. Set to -1 for no limit.','Subscriptions');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'AiCatalogPriceCheckEnabled')
|
||||||
|
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||||
|
VALUES ('AiCatalogPriceCheckEnabled','true','AI Catalog Price Check','Platform-level switch for the AI catalog price review feature. When off, the feature is disabled for all companies regardless of their settings.','AI');
|
||||||
|
");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8377));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8383));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8385));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5837));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5846));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5847));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10594
File diff suppressed because it is too large
Load Diff
+95
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SeedSalesDiscountsAccount : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// Insert the 4950 Sales Discounts contra-revenue account for every company that does
|
||||||
|
// not already have it. The account is credit-normal (AccountType=4 Revenue,
|
||||||
|
// AccountSubType=32 OtherIncome) and is debited when invoice discounts are applied so
|
||||||
|
// the GL balances (DR Sales Discounts / gap between CR Revenue and DR AR).
|
||||||
|
// Idempotent: the WHERE NOT EXISTS guard means re-running the migration is safe.
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
INSERT INTO Accounts
|
||||||
|
(AccountNumber, Name, AccountType, AccountSubType,
|
||||||
|
IsSystem, IsActive, Description,
|
||||||
|
CompanyId, CreatedAt, IsDeleted,
|
||||||
|
CurrentBalance, OpeningBalance)
|
||||||
|
SELECT
|
||||||
|
'4950',
|
||||||
|
'Sales Discounts',
|
||||||
|
4, -- AccountType.Revenue
|
||||||
|
32, -- AccountSubType.OtherIncome
|
||||||
|
1, -- IsSystem = true
|
||||||
|
1, -- IsActive = true
|
||||||
|
'Contra-revenue for invoice discounts granted to customers',
|
||||||
|
c.Id,
|
||||||
|
GETUTCDATE(),
|
||||||
|
0, -- IsDeleted = false
|
||||||
|
0, -- CurrentBalance
|
||||||
|
0 -- OpeningBalance
|
||||||
|
FROM Companies c
|
||||||
|
WHERE c.IsDeleted = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM Accounts a
|
||||||
|
WHERE a.CompanyId = c.Id
|
||||||
|
AND a.AccountNumber = '4950'
|
||||||
|
AND a.IsDeleted = 0
|
||||||
|
);
|
||||||
|
");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 13, 39, 22, 61, DateTimeKind.Utc).AddTicks(8475));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 13, 39, 22, 61, DateTimeKind.Utc).AddTicks(8484));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 13, 39, 22, 61, DateTimeKind.Utc).AddTicks(8486));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8377));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8383));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8385));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10600
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AccountingGapsPhase2 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "PostedDate",
|
||||||
|
table: "VendorCredits",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "DepositAccountId",
|
||||||
|
table: "Refunds",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
// Seed the Gift Certificate Liability account (2500) for every company that doesn't
|
||||||
|
// already have it. Credit-normal OtherCurrentLiability account; credited when a GC is
|
||||||
|
// issued and debited when redeemed or voided. Idempotent guard prevents double-seeding.
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
INSERT INTO Accounts
|
||||||
|
(AccountNumber, Name, AccountType, AccountSubType,
|
||||||
|
IsSystem, IsActive, Description,
|
||||||
|
CompanyId, CreatedAt, IsDeleted,
|
||||||
|
CurrentBalance, OpeningBalance)
|
||||||
|
SELECT
|
||||||
|
'2500',
|
||||||
|
'Gift Certificate Liability',
|
||||||
|
2, -- AccountType.Liability
|
||||||
|
12, -- AccountSubType.OtherCurrentLiability
|
||||||
|
1, -- IsSystem = true
|
||||||
|
1, -- IsActive = true
|
||||||
|
'Outstanding gift certificate obligations owed to certificate holders',
|
||||||
|
c.Id,
|
||||||
|
GETUTCDATE(),
|
||||||
|
0, -- IsDeleted = false
|
||||||
|
0, -- CurrentBalance
|
||||||
|
0 -- OpeningBalance
|
||||||
|
FROM Companies c
|
||||||
|
WHERE c.IsDeleted = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM Accounts a
|
||||||
|
WHERE a.CompanyId = c.Id
|
||||||
|
AND a.AccountNumber = '2500'
|
||||||
|
AND a.IsDeleted = 0
|
||||||
|
);
|
||||||
|
");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 24, 44, 715, DateTimeKind.Utc).AddTicks(9166));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 24, 44, 715, DateTimeKind.Utc).AddTicks(9172));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 24, 44, 715, DateTimeKind.Utc).AddTicks(9174));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PostedDate",
|
||||||
|
table: "VendorCredits");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DepositAccountId",
|
||||||
|
table: "Refunds");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 13, 39, 22, 61, DateTimeKind.Utc).AddTicks(8475));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 13, 39, 22, 61, DateTimeKind.Utc).AddTicks(8484));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 13, 39, 22, 61, DateTimeKind.Utc).AddTicks(8486));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10603
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AccountingDepositsGL : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "DepositAccountId",
|
||||||
|
table: "Deposits",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
// Seed account 2300 "Customer Deposits" (Liability / OtherCurrentLiability) for every
|
||||||
|
// company that doesn't already have it. Credited when a deposit is taken; debited when
|
||||||
|
// the deposit is applied to an invoice. Idempotent guard prevents double-seeding.
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
INSERT INTO Accounts
|
||||||
|
(AccountNumber, Name, AccountType, AccountSubType,
|
||||||
|
IsSystem, IsActive, Description,
|
||||||
|
CompanyId, CreatedAt, IsDeleted,
|
||||||
|
CurrentBalance, OpeningBalance)
|
||||||
|
SELECT
|
||||||
|
'2300',
|
||||||
|
'Customer Deposits',
|
||||||
|
2, -- AccountType.Liability
|
||||||
|
12, -- AccountSubType.OtherCurrentLiability
|
||||||
|
1, -- IsSystem = true
|
||||||
|
1, -- IsActive = true
|
||||||
|
'Deposits received from customers before an invoice is created; cleared when deposit is applied to invoice',
|
||||||
|
c.Id,
|
||||||
|
GETUTCDATE(),
|
||||||
|
0, -- IsDeleted = false
|
||||||
|
0, -- CurrentBalance
|
||||||
|
0 -- OpeningBalance
|
||||||
|
FROM Companies c
|
||||||
|
WHERE c.IsDeleted = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM Accounts a
|
||||||
|
WHERE a.CompanyId = c.Id
|
||||||
|
AND a.AccountNumber = '2300'
|
||||||
|
AND a.IsDeleted = 0
|
||||||
|
);
|
||||||
|
");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 57, 30, 15, DateTimeKind.Utc).AddTicks(5641));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 57, 30, 15, DateTimeKind.Utc).AddTicks(5655));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 57, 30, 15, DateTimeKind.Utc).AddTicks(5656));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DepositAccountId",
|
||||||
|
table: "Deposits");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 24, 44, 715, DateTimeKind.Utc).AddTicks(9166));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 24, 44, 715, DateTimeKind.Utc).AddTicks(9172));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 24, 44, 715, DateTimeKind.Utc).AddTicks(9174));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10732
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddKioskIntakeSession : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "KioskActivationToken",
|
||||||
|
table: "Companies",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "KioskSessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
SessionToken = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
SessionType = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CustomerFirstName = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
CustomerLastName = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
CustomerPhone = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
CustomerEmail = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
IsReturningCustomer = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
JobDescription = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
HowDidYouHearAboutUs = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
AgreedToTerms = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
AgreedToTermsAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
SmsOptIn = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
SignatureDataBase64 = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
LinkedCustomerId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
LinkedJobId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
SubmittedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
RemoteLinkEmail = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
RemoteLinkSentAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_KioskSessions", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_KioskSessions_Customers_LinkedCustomerId",
|
||||||
|
column: x => x.LinkedCustomerId,
|
||||||
|
principalTable: "Customers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_KioskSessions_Jobs_LinkedJobId",
|
||||||
|
column: x => x.LinkedJobId,
|
||||||
|
principalTable: "Jobs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 18, 40, 15, 633, DateTimeKind.Utc).AddTicks(8207));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 18, 40, 15, 633, DateTimeKind.Utc).AddTicks(8213));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 18, 40, 15, 633, DateTimeKind.Utc).AddTicks(8215));
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KioskSessions_LinkedCustomerId",
|
||||||
|
table: "KioskSessions",
|
||||||
|
column: "LinkedCustomerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KioskSessions_LinkedJobId",
|
||||||
|
table: "KioskSessions",
|
||||||
|
column: "LinkedJobId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KioskSessions_SessionToken",
|
||||||
|
table: "KioskSessions",
|
||||||
|
column: "SessionToken",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "KioskSessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "KioskActivationToken",
|
||||||
|
table: "Companies");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 57, 30, 15, DateTimeKind.Utc).AddTicks(5641));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 57, 30, 15, DateTimeKind.Utc).AddTicks(5655));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 14, 57, 30, 15, DateTimeKind.Utc).AddTicks(5656));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10735
File diff suppressed because it is too large
Load Diff
+71
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddInvoicePublicViewToken : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PublicViewToken",
|
||||||
|
table: "Invoices",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 19, 28, 44, 26, DateTimeKind.Utc).AddTicks(4259));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 19, 28, 44, 26, DateTimeKind.Utc).AddTicks(4264));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 19, 28, 44, 26, DateTimeKind.Utc).AddTicks(4266));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PublicViewToken",
|
||||||
|
table: "Invoices");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 18, 40, 15, 633, DateTimeKind.Utc).AddTicks(8207));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 18, 40, 15, 633, DateTimeKind.Utc).AddTicks(8213));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 18, 40, 15, 633, DateTimeKind.Utc).AddTicks(8215));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10742
File diff suppressed because it is too large
Load Diff
+82
@@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddKioskIntakeOutputSetting : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "LinkedQuoteId",
|
||||||
|
table: "KioskSessions",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "KioskIntakeOutput",
|
||||||
|
table: "CompanyPreferences",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 2, 27, 27, 993, DateTimeKind.Utc).AddTicks(2349));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 2, 27, 27, 993, DateTimeKind.Utc).AddTicks(2366));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 2, 27, 27, 993, DateTimeKind.Utc).AddTicks(2367));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LinkedQuoteId",
|
||||||
|
table: "KioskSessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "KioskIntakeOutput",
|
||||||
|
table: "CompanyPreferences");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 19, 28, 44, 26, DateTimeKind.Utc).AddTicks(4259));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 19, 28, 44, 26, DateTimeKind.Utc).AddTicks(4264));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 13, 19, 28, 44, 26, DateTimeKind.Utc).AddTicks(4266));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10748
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddJobOvenBatchFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "OvenBatches",
|
||||||
|
table: "Jobs",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "OvenCycleMinutes",
|
||||||
|
table: "Jobs",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 5, 39, 939, DateTimeKind.Utc).AddTicks(6420));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 5, 39, 939, DateTimeKind.Utc).AddTicks(6425));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 5, 39, 939, DateTimeKind.Utc).AddTicks(6426));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "OvenBatches",
|
||||||
|
table: "Jobs");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "OvenCycleMinutes",
|
||||||
|
table: "Jobs");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 2, 27, 27, 993, DateTimeKind.Utc).AddTicks(2349));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 2, 27, 27, 993, DateTimeKind.Utc).AddTicks(2366));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 2, 27, 27, 993, DateTimeKind.Utc).AddTicks(2367));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10751
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddJobItemIsAiItem : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsAiItem",
|
||||||
|
table: "JobItems",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 43, 43, 116, DateTimeKind.Utc).AddTicks(7475));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 43, 43, 116, DateTimeKind.Utc).AddTicks(7481));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 43, 43, 116, DateTimeKind.Utc).AddTicks(7482));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsAiItem",
|
||||||
|
table: "JobItems");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 5, 39, 939, DateTimeKind.Utc).AddTicks(6420));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 5, 39, 939, DateTimeKind.Utc).AddTicks(6425));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 5, 39, 939, DateTimeKind.Utc).AddTicks(6426));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user