From 11a1b91be1b91c71cac6860be8cd755d11231f96 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 00:27:44 -0400 Subject: [PATCH 01/15] Add platform powder catalog management UI with full CRUD and AI lookup - PowderCatalogController: Create, Edit, ToggleDiscontinued actions; searchable/filterable/sortable Index with pagination; AiLookup and AiAugmentFromUrl endpoints backed by IInventoryAiLookupService - New views: Create, Edit, _Form partial (with AI-assisted field population), overhauled Index grid with completeness quality badges and responsive mobile cards - New ViewModels: PowderCatalogIndexViewModel, PowderCatalogFormViewModel, PowderCatalogListItemViewModel - AI lookup improvements: SpecificGravity field added to InventoryAiLookupResult; ApplyPowderFallbacks derives CoverageSqFtPerLb from specific gravity when docs omit it; DefaultTransferEfficiency (65%) applied everywhere transfer efficiency is null - powder-catalog-ai-lookup.js: client-side AI lookup and URL augment wiring for the catalog form Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.local.json | 3 +- TODO.txt | 6 + TODO.txt.bak | 12 +- scripts/migrate-prod.sql | 7122 +++++++++++++++++ .../Interfaces/IInventoryAiLookupService.cs | 1 + .../Services/InventoryAiLookupService.cs | 29 +- .../Controllers/InventoryController.cs | 17 +- .../Controllers/PowderCatalogController.cs | 509 +- .../PowderCatalogFormViewModel.cs | 85 + .../PowderCatalogIndexViewModel.cs | 37 + .../Views/PowderCatalog/Create.cshtml | 42 + .../Views/PowderCatalog/Edit.cshtml | 50 + .../Views/PowderCatalog/Index.cshtml | 464 +- .../Views/PowderCatalog/_Form.cshtml | 184 + .../wwwroot/js/powder-catalog-ai-lookup.js | 175 + 15 files changed, 8642 insertions(+), 94 deletions(-) create mode 100644 scripts/migrate-prod.sql create mode 100644 src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogFormViewModel.cs create mode 100644 src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogIndexViewModel.cs create mode 100644 src/PowderCoating.Web/Views/PowderCatalog/Create.cshtml create mode 100644 src/PowderCoating.Web/Views/PowderCatalog/Edit.cshtml create mode 100644 src/PowderCoating.Web/Views/PowderCatalog/_Form.cshtml create mode 100644 src/PowderCoating.Web/wwwroot/js/powder-catalog-ai-lookup.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c8a9ea0..b369143 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -171,7 +171,8 @@ "PowerShell(Select-String *)", "Bash(Select-Object -First 20)", "PowerShell(node -e \"require\\('fs'\\).existsSync\\(require\\('path'\\).join\\(process.cwd\\(\\), 'node_modules', 'sharp'\\)\\) ? console.log\\('sharp ok'\\) : console.log\\('no sharp'\\)\")", - "WebFetch(domain:www.powdercoatinglogix.com)" + "WebFetch(domain:www.powdercoatinglogix.com)", + "PowerShell($bytes = [System.IO.File]::ReadAllBytes\\('src/PowderCoating.Web/Views/Jobs/Details.cshtml'\\); $text = [System.Text.Encoding]::UTF8.GetString\\($bytes\\); $idx = $text.IndexOf\\('hasPowderData'\\); $snippet = $text.Substring\\($idx - 20, 250\\); [System.Text.Encoding]::Unicode.GetBytes\\($snippet\\) | Format-Hex | Select-Object -First 30)" ] } } diff --git a/TODO.txt b/TODO.txt index fd1a85d..0db5bd0 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,5 +1,11 @@ Shop Management App TO DO List ============================== +-Inventory Lookup not always finding price for Columbia Coatings +-Logging powder usage and choosing a job doesn't record properly in the activity section of the powder itself +-Need to allow deleting of powder usage entries, or at least editing in case of a goof up +-Still random weird characters on a bunch of pages. Intake button for example on the jobs screen shows: Intake ✓ + + -Google review request email after a job -Check my ChatGPT chat about surface area for a few solid ideas for the system -Fix up approve/decline messages between customer and user on quote approval feature diff --git a/TODO.txt.bak b/TODO.txt.bak index fd36605..da255c5 100644 --- a/TODO.txt.bak +++ b/TODO.txt.bak @@ -1,9 +1,9 @@ Shop Management App TO DO List ============================== --Lookup not working 100% correct. If I type columbia as the manufacturer and a color name....it's finding blackmamba from prismatic incorrectly. --Lookup Modal not showing ALL matches. Maybe make scrollable --Pickup cure information from TDS Sheet if not found by AI Search --ON AI Photo Quote page, when the AI info comes back we should scroll the modal window down so it's visible. It's not clear that new info has been added to the modal for all customers +-Inventory Lookup not always finding price for Columbia Coatings +-Logging powder usage and choosing a job doesn't record properly in the activity section of the powder itself +-Need to allow deleting of powder usage entries, or at least editing in case of a goof up + -Google review request email after a job -Check my ChatGPT chat about surface area for a few solid ideas for the system -Fix up approve/decline messages between customer and user on quote approval feature @@ -181,6 +181,10 @@ AI Agent item where we upload a picture and it will calculate the approximate sq -When scanning inventory QR Code, there is no cancel button -Bug: When scanning Inventory QR Code, if not logged in...it takes you to the dashboard after login, not our inventory scanning screen -Add SMS capabilities +-Lookup not working 100% correct. If I type columbia as the manufacturer and a color name....it's finding blackmamba from prismatic incorrectly. +-Lookup Modal not showing ALL matches. Maybe make scrollable +-Pickup cure information from TDS Sheet if not found by AI Search +-ON AI Photo Quote page, when the AI info comes back we should scroll the modal window down so it's visible. It's not clear that new info has been added to the modal for all customers Ideas Removed ======================= diff --git a/scripts/migrate-prod.sql b/scripts/migrate-prod.sql new file mode 100644 index 0000000..2a4a116 --- /dev/null +++ b/scripts/migrate-prod.sql @@ -0,0 +1,7122 @@ +IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL +BEGIN + CREATE TABLE [__EFMigrationsHistory] ( + [MigrationId] nvarchar(150) NOT NULL, + [ProductVersion] nvarchar(32) NOT NULL, + CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) + ); +END; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260316155002_Baseline' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-16T15:49:58.7377851Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260316155002_Baseline' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-16T15:49:58.7377856Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260316155002_Baseline' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-16T15:49:58.7377858Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260316155002_Baseline' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260316155002_Baseline', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317121938_AddAiContextProfile' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [AiContextProfile] nvarchar(2000) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317121938_AddAiContextProfile' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-17T12:19:34.4894690Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317121938_AddAiContextProfile' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-17T12:19:34.4894696Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317121938_AddAiContextProfile' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-17T12:19:34.4894698Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317121938_AddAiContextProfile' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260317121938_AddAiContextProfile', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317205927_FixLaborItemQuantityDecimal' +) +BEGIN + DECLARE @var0 sysname; + SELECT @var0 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[QuoteItems]') AND [c].[name] = N'Quantity'); + IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [QuoteItems] DROP CONSTRAINT [' + @var0 + '];'); + ALTER TABLE [QuoteItems] ALTER COLUMN [Quantity] decimal(18,2) NOT NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317205927_FixLaborItemQuantityDecimal' +) +BEGIN + DECLARE @var1 sysname; + SELECT @var1 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[JobItems]') AND [c].[name] = N'Quantity'); + IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [JobItems] DROP CONSTRAINT [' + @var1 + '];'); + ALTER TABLE [JobItems] ALTER COLUMN [Quantity] decimal(18,2) NOT NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317205927_FixLaborItemQuantityDecimal' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-17T20:59:24.2463737Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317205927_FixLaborItemQuantityDecimal' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-17T20:59:24.2463746Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317205927_FixLaborItemQuantityDecimal' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-17T20:59:24.2463748Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260317205927_FixLaborItemQuantityDecimal' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260317205927_FixLaborItemQuantityDecimal', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318124847_AddJobTimeEntries' +) +BEGIN + CREATE TABLE [JobTimeEntries] ( + [Id] int NOT NULL IDENTITY, + [JobId] int NOT NULL, + [ShopWorkerId] int NOT NULL, + [WorkDate] datetime2 NOT NULL, + [HoursWorked] decimal(18,2) NOT NULL, + [Stage] nvarchar(max) NULL, + [Notes] nvarchar(max) NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_JobTimeEntries] PRIMARY KEY ([Id]), + CONSTRAINT [FK_JobTimeEntries_Jobs_JobId] FOREIGN KEY ([JobId]) REFERENCES [Jobs] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_JobTimeEntries_ShopWorkers_ShopWorkerId] FOREIGN KEY ([ShopWorkerId]) REFERENCES [ShopWorkers] ([Id]) ON DELETE CASCADE + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318124847_AddJobTimeEntries' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T12:48:44.7462691Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318124847_AddJobTimeEntries' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T12:48:44.7462697Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318124847_AddJobTimeEntries' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T12:48:44.7462699Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318124847_AddJobTimeEntries' +) +BEGIN + CREATE INDEX [IX_JobTimeEntries_JobId] ON [JobTimeEntries] ([JobId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318124847_AddJobTimeEntries' +) +BEGIN + CREATE INDEX [IX_JobTimeEntries_ShopWorkerId] ON [JobTimeEntries] ([ShopWorkerId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318124847_AddJobTimeEntries' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260318124847_AddJobTimeEntries', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318131500_AddJobShopAccessCode' +) +BEGIN + ALTER TABLE [Jobs] ADD [ShopAccessCode] uniqueidentifier NOT NULL DEFAULT (NEWID()); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318131500_AddJobShopAccessCode' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T13:14:57.2203832Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318131500_AddJobShopAccessCode' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T13:14:57.2203838Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318131500_AddJobShopAccessCode' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T13:14:57.2203839Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318131500_AddJobShopAccessCode' +) +BEGIN + CREATE UNIQUE INDEX [IX_Jobs_ShopAccessCode] ON [Jobs] ([ShopAccessCode]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318131500_AddJobShopAccessCode' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260318131500_AddJobShopAccessCode', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318132857_AddShopWorkerRoleCosts' +) +BEGIN + CREATE TABLE [ShopWorkerRoleCosts] ( + [Id] int NOT NULL IDENTITY, + [Role] int NOT NULL, + [HourlyRate] decimal(18,2) NOT NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_ShopWorkerRoleCosts] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318132857_AddShopWorkerRoleCosts' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T13:28:54.6854802Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318132857_AddShopWorkerRoleCosts' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T13:28:54.6854849Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318132857_AddShopWorkerRoleCosts' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T13:28:54.6854851Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318132857_AddShopWorkerRoleCosts' +) +BEGIN + CREATE UNIQUE INDEX [IX_ShopWorkerRoleCosts_CompanyId_Role] ON [ShopWorkerRoleCosts] ([CompanyId], [Role]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318132857_AddShopWorkerRoleCosts' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260318132857_AddShopWorkerRoleCosts', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + ALTER TABLE [Jobs] ADD [IsReworkJob] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + ALTER TABLE [Jobs] ADD [OriginalJobId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + CREATE TABLE [ReworkRecords] ( + [Id] int NOT NULL IDENTITY, + [JobId] int NOT NULL, + [JobItemId] int NULL, + [ReworkJobId] int NULL, + [ReworkType] int NOT NULL, + [Reason] int NOT NULL, + [DefectDescription] nvarchar(max) NOT NULL, + [DiscoveredBy] int NOT NULL, + [DiscoveredDate] datetime2 NOT NULL, + [ReportedByName] nvarchar(max) NULL, + [EstimatedReworkCost] decimal(18,2) NOT NULL, + [ActualReworkCost] decimal(18,2) NOT NULL, + [IsBillableToCustomer] bit NOT NULL, + [BillingNotes] nvarchar(max) NULL, + [Status] int NOT NULL, + [Resolution] int NULL, + [ResolvedDate] datetime2 NULL, + [ResolutionNotes] nvarchar(max) NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_ReworkRecords] PRIMARY KEY ([Id]), + CONSTRAINT [FK_ReworkRecords_JobItems_JobItemId] FOREIGN KEY ([JobItemId]) REFERENCES [JobItems] ([Id]), + CONSTRAINT [FK_ReworkRecords_Jobs_JobId] FOREIGN KEY ([JobId]) REFERENCES [Jobs] ([Id]) ON DELETE NO ACTION, + CONSTRAINT [FK_ReworkRecords_Jobs_ReworkJobId] FOREIGN KEY ([ReworkJobId]) REFERENCES [Jobs] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T13:42:32.9092998Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T13:42:32.9093003Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T13:42:32.9093005Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + CREATE INDEX [IX_Jobs_OriginalJobId] ON [Jobs] ([OriginalJobId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + CREATE INDEX [IX_ReworkRecords_JobId] ON [ReworkRecords] ([JobId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + CREATE INDEX [IX_ReworkRecords_JobItemId] ON [ReworkRecords] ([JobItemId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + CREATE INDEX [IX_ReworkRecords_ReworkJobId] ON [ReworkRecords] ([ReworkJobId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + ALTER TABLE [Jobs] ADD CONSTRAINT [FK_Jobs_Jobs_OriginalJobId] FOREIGN KEY ([OriginalJobId]) REFERENCES [Jobs] ([Id]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318134236_AddReworkTracking' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260318134236_AddReworkTracking', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + ALTER TABLE [Invoices] ADD [CreditApplied] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + ALTER TABLE [Customers] ADD [CreditBalance] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE TABLE [CreditMemos] ( + [Id] int NOT NULL IDENTITY, + [MemoNumber] nvarchar(450) NOT NULL, + [CustomerId] int NOT NULL, + [OriginalInvoiceId] int NULL, + [ReworkRecordId] int NULL, + [Amount] decimal(18,2) NOT NULL, + [AmountApplied] decimal(18,2) NOT NULL, + [IssueDate] datetime2 NOT NULL, + [ExpiryDate] datetime2 NULL, + [Reason] nvarchar(max) NOT NULL, + [Notes] nvarchar(max) NULL, + [Status] int NOT NULL, + [IssuedById] nvarchar(450) NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_CreditMemos] PRIMARY KEY ([Id]), + CONSTRAINT [FK_CreditMemos_AspNetUsers_IssuedById] FOREIGN KEY ([IssuedById]) REFERENCES [AspNetUsers] ([Id]), + CONSTRAINT [FK_CreditMemos_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE NO ACTION, + CONSTRAINT [FK_CreditMemos_Invoices_OriginalInvoiceId] FOREIGN KEY ([OriginalInvoiceId]) REFERENCES [Invoices] ([Id]), + CONSTRAINT [FK_CreditMemos_ReworkRecords_ReworkRecordId] FOREIGN KEY ([ReworkRecordId]) REFERENCES [ReworkRecords] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE TABLE [Refunds] ( + [Id] int NOT NULL IDENTITY, + [InvoiceId] int NOT NULL, + [PaymentId] int NULL, + [Amount] decimal(18,2) NOT NULL, + [RefundDate] datetime2 NOT NULL, + [RefundMethod] int NOT NULL, + [Reason] nvarchar(max) NOT NULL, + [Reference] nvarchar(max) NULL, + [Notes] nvarchar(max) NULL, + [Status] int NOT NULL, + [IssuedDate] datetime2 NULL, + [IssuedById] nvarchar(450) NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_Refunds] PRIMARY KEY ([Id]), + CONSTRAINT [FK_Refunds_AspNetUsers_IssuedById] FOREIGN KEY ([IssuedById]) REFERENCES [AspNetUsers] ([Id]), + CONSTRAINT [FK_Refunds_Invoices_InvoiceId] FOREIGN KEY ([InvoiceId]) REFERENCES [Invoices] ([Id]) ON DELETE NO ACTION, + CONSTRAINT [FK_Refunds_Payments_PaymentId] FOREIGN KEY ([PaymentId]) REFERENCES [Payments] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE TABLE [CreditMemoApplications] ( + [Id] int NOT NULL IDENTITY, + [CreditMemoId] int NOT NULL, + [InvoiceId] int NOT NULL, + [AmountApplied] decimal(18,2) NOT NULL, + [AppliedDate] datetime2 NOT NULL, + [AppliedById] nvarchar(450) NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_CreditMemoApplications] PRIMARY KEY ([Id]), + CONSTRAINT [FK_CreditMemoApplications_AspNetUsers_AppliedById] FOREIGN KEY ([AppliedById]) REFERENCES [AspNetUsers] ([Id]), + CONSTRAINT [FK_CreditMemoApplications_CreditMemos_CreditMemoId] FOREIGN KEY ([CreditMemoId]) REFERENCES [CreditMemos] ([Id]) ON DELETE NO ACTION, + CONSTRAINT [FK_CreditMemoApplications_Invoices_InvoiceId] FOREIGN KEY ([InvoiceId]) REFERENCES [Invoices] ([Id]) ON DELETE NO ACTION + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T22:26:44.9349567Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T22:26:44.9349573Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-18T22:26:44.9349575Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_CreditMemoApplications_AppliedById] ON [CreditMemoApplications] ([AppliedById]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_CreditMemoApplications_CreditMemoId] ON [CreditMemoApplications] ([CreditMemoId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_CreditMemoApplications_InvoiceId] ON [CreditMemoApplications] ([InvoiceId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE UNIQUE INDEX [IX_CreditMemos_CompanyId_MemoNumber] ON [CreditMemos] ([CompanyId], [MemoNumber]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_CreditMemos_CustomerId] ON [CreditMemos] ([CustomerId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_CreditMemos_IssuedById] ON [CreditMemos] ([IssuedById]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_CreditMemos_OriginalInvoiceId] ON [CreditMemos] ([OriginalInvoiceId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_CreditMemos_ReworkRecordId] ON [CreditMemos] ([ReworkRecordId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_Refunds_InvoiceId] ON [Refunds] ([InvoiceId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_Refunds_IssuedById] ON [Refunds] ([IssuedById]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + CREATE INDEX [IX_Refunds_PaymentId] ON [Refunds] ([PaymentId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260318222648_AddRefundsAndCreditMemos' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260318222648_AddRefundsAndCreditMemos', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE TABLE [JobTemplates] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + [Description] nvarchar(max) NULL, + [CustomerId] int NULL, + [SpecialInstructions] nvarchar(max) NULL, + [IsActive] bit NOT NULL, + [UsageCount] int NOT NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_JobTemplates] PRIMARY KEY ([Id]), + CONSTRAINT [FK_JobTemplates_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE TABLE [JobTemplateItems] ( + [Id] int NOT NULL IDENTITY, + [JobTemplateId] int NOT NULL, + [Description] nvarchar(max) NOT NULL, + [Quantity] decimal(18,2) NOT NULL, + [SurfaceAreaSqFt] decimal(18,2) NOT NULL, + [CatalogItemId] int NULL, + [IsGenericItem] bit NOT NULL, + [IsLaborItem] bit NOT NULL, + [ManualUnitPrice] decimal(18,2) NULL, + [RequiresSandblasting] bit NOT NULL, + [RequiresMasking] bit NOT NULL, + [IncludePrepCost] bit NOT NULL, + [EstimatedMinutes] int NOT NULL, + [Complexity] nvarchar(max) NULL, + [Notes] nvarchar(max) NULL, + [DisplayOrder] int NOT NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_JobTemplateItems] PRIMARY KEY ([Id]), + CONSTRAINT [FK_JobTemplateItems_CatalogItems_CatalogItemId] FOREIGN KEY ([CatalogItemId]) REFERENCES [CatalogItems] ([Id]), + CONSTRAINT [FK_JobTemplateItems_JobTemplates_JobTemplateId] FOREIGN KEY ([JobTemplateId]) REFERENCES [JobTemplates] ([Id]) ON DELETE CASCADE + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE TABLE [JobTemplateItemCoats] ( + [Id] int NOT NULL IDENTITY, + [JobTemplateItemId] int NOT NULL, + [CoatName] nvarchar(max) NOT NULL, + [Sequence] int NOT NULL, + [InventoryItemId] int NULL, + [ColorName] nvarchar(max) NULL, + [VendorId] int NULL, + [ColorCode] nvarchar(max) NULL, + [Finish] nvarchar(max) NULL, + [CoverageSqFtPerLb] decimal(18,2) NOT NULL, + [TransferEfficiency] decimal(18,2) NOT NULL, + [PowderCostPerLb] decimal(18,2) NULL, + [Notes] nvarchar(max) NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_JobTemplateItemCoats] PRIMARY KEY ([Id]), + CONSTRAINT [FK_JobTemplateItemCoats_InventoryItems_InventoryItemId] FOREIGN KEY ([InventoryItemId]) REFERENCES [InventoryItems] ([Id]), + CONSTRAINT [FK_JobTemplateItemCoats_JobTemplateItems_JobTemplateItemId] FOREIGN KEY ([JobTemplateItemId]) REFERENCES [JobTemplateItems] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_JobTemplateItemCoats_Vendors_VendorId] FOREIGN KEY ([VendorId]) REFERENCES [Vendors] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE TABLE [JobTemplateItemPrepServices] ( + [Id] int NOT NULL IDENTITY, + [JobTemplateItemId] int NOT NULL, + [PrepServiceId] int NOT NULL, + [EstimatedMinutes] int NOT NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_JobTemplateItemPrepServices] PRIMARY KEY ([Id]), + CONSTRAINT [FK_JobTemplateItemPrepServices_JobTemplateItems_JobTemplateItemId] FOREIGN KEY ([JobTemplateItemId]) REFERENCES [JobTemplateItems] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_JobTemplateItemPrepServices_PrepServices_PrepServiceId] FOREIGN KEY ([PrepServiceId]) REFERENCES [PrepServices] ([Id]) ON DELETE CASCADE + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-19T02:38:23.4195291Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-19T02:38:23.4195296Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-19T02:38:23.4195298Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE INDEX [IX_JobTemplateItemCoats_InventoryItemId] ON [JobTemplateItemCoats] ([InventoryItemId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE INDEX [IX_JobTemplateItemCoats_JobTemplateItemId] ON [JobTemplateItemCoats] ([JobTemplateItemId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE INDEX [IX_JobTemplateItemCoats_VendorId] ON [JobTemplateItemCoats] ([VendorId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE INDEX [IX_JobTemplateItemPrepServices_JobTemplateItemId] ON [JobTemplateItemPrepServices] ([JobTemplateItemId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE INDEX [IX_JobTemplateItemPrepServices_PrepServiceId] ON [JobTemplateItemPrepServices] ([PrepServiceId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE INDEX [IX_JobTemplateItems_CatalogItemId] ON [JobTemplateItems] ([CatalogItemId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE INDEX [IX_JobTemplateItems_JobTemplateId] ON [JobTemplateItems] ([JobTemplateId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + CREATE INDEX [IX_JobTemplates_CustomerId] ON [JobTemplates] ([CustomerId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319023827_AddJobTemplates' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260319023827_AddJobTemplates', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + ALTER TABLE [Invoices] ADD [GiftCertificateRedeemed] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + CREATE TABLE [GiftCertificates] ( + [Id] int NOT NULL IDENTITY, + [CertificateCode] nvarchar(max) NOT NULL, + [OriginalAmount] decimal(18,2) NOT NULL, + [RedeemedAmount] decimal(18,2) NOT NULL, + [RecipientCustomerId] int NULL, + [RecipientName] nvarchar(max) NULL, + [RecipientEmail] nvarchar(max) NULL, + [IssuedReason] int NOT NULL, + [PurchasePrice] decimal(18,2) NULL, + [PurchasingCustomerId] int NULL, + [Status] int NOT NULL, + [IssueDate] datetime2 NOT NULL, + [ExpiryDate] datetime2 NULL, + [Notes] nvarchar(max) NULL, + [IssuedById] nvarchar(450) NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_GiftCertificates] PRIMARY KEY ([Id]), + CONSTRAINT [FK_GiftCertificates_AspNetUsers_IssuedById] FOREIGN KEY ([IssuedById]) REFERENCES [AspNetUsers] ([Id]), + CONSTRAINT [FK_GiftCertificates_Customers_PurchasingCustomerId] FOREIGN KEY ([PurchasingCustomerId]) REFERENCES [Customers] ([Id]), + CONSTRAINT [FK_GiftCertificates_Customers_RecipientCustomerId] FOREIGN KEY ([RecipientCustomerId]) REFERENCES [Customers] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + CREATE TABLE [GiftCertificateRedemptions] ( + [Id] int NOT NULL IDENTITY, + [GiftCertificateId] int NOT NULL, + [InvoiceId] int NOT NULL, + [AmountRedeemed] decimal(18,2) NOT NULL, + [RedeemedDate] datetime2 NOT NULL, + [RedeemedById] nvarchar(450) NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_GiftCertificateRedemptions] PRIMARY KEY ([Id]), + CONSTRAINT [FK_GiftCertificateRedemptions_AspNetUsers_RedeemedById] FOREIGN KEY ([RedeemedById]) REFERENCES [AspNetUsers] ([Id]), + CONSTRAINT [FK_GiftCertificateRedemptions_GiftCertificates_GiftCertificateId] FOREIGN KEY ([GiftCertificateId]) REFERENCES [GiftCertificates] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_GiftCertificateRedemptions_Invoices_InvoiceId] FOREIGN KEY ([InvoiceId]) REFERENCES [Invoices] ([Id]) ON DELETE CASCADE + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-19T15:45:03.1454465Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-19T15:45:03.1454472Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-19T15:45:03.1454474Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + CREATE INDEX [IX_GiftCertificateRedemptions_GiftCertificateId] ON [GiftCertificateRedemptions] ([GiftCertificateId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + CREATE INDEX [IX_GiftCertificateRedemptions_InvoiceId] ON [GiftCertificateRedemptions] ([InvoiceId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + CREATE INDEX [IX_GiftCertificateRedemptions_RedeemedById] ON [GiftCertificateRedemptions] ([RedeemedById]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + CREATE INDEX [IX_GiftCertificates_IssuedById] ON [GiftCertificates] ([IssuedById]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + CREATE INDEX [IX_GiftCertificates_PurchasingCustomerId] ON [GiftCertificates] ([PurchasingCustomerId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + CREATE INDEX [IX_GiftCertificates_RecipientCustomerId] ON [GiftCertificates] ([RecipientCustomerId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260319154506_AddGiftCertificates' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260319154506_AddGiftCertificates', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320002450_AddRefundStoreCreditLink' +) +BEGIN + ALTER TABLE [Refunds] ADD [CreditMemoId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320002450_AddRefundStoreCreditLink' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T00:24:47.3611509Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320002450_AddRefundStoreCreditLink' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T00:24:47.3611518Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320002450_AddRefundStoreCreditLink' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T00:24:47.3611521Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320002450_AddRefundStoreCreditLink' +) +BEGIN + CREATE INDEX [IX_Refunds_CreditMemoId] ON [Refunds] ([CreditMemoId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320002450_AddRefundStoreCreditLink' +) +BEGIN + ALTER TABLE [Refunds] ADD CONSTRAINT [FK_Refunds_CreditMemos_CreditMemoId] FOREIGN KEY ([CreditMemoId]) REFERENCES [CreditMemos] ([Id]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320002450_AddRefundStoreCreditLink' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260320002450_AddRefundStoreCreditLink', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320005106_AddQuoteItemIsAiItem' +) +BEGIN + ALTER TABLE [QuoteItems] ADD [IsAiItem] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320005106_AddQuoteItemIsAiItem' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T00:51:03.2423766Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320005106_AddQuoteItemIsAiItem' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T00:51:03.2423772Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320005106_AddQuoteItemIsAiItem' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T00:51:03.2423774Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320005106_AddQuoteItemIsAiItem' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260320005106_AddQuoteItemIsAiItem', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + ALTER TABLE [Quotes] ADD [ItemsSubtotal] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + ALTER TABLE [Quotes] ADD [OvenBatchCost] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + ALTER TABLE [Quotes] ADD [OverheadAmount] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + ALTER TABLE [Quotes] ADD [OverheadPercent] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + ALTER TABLE [Quotes] ADD [ProfitMargin] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + ALTER TABLE [Quotes] ADD [ProfitPercent] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + ALTER TABLE [Quotes] ADD [ShopSuppliesAmount] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + ALTER TABLE [Quotes] ADD [ShopSuppliesPercent] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T01:10:54.1468159Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T01:10:54.1468166Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T01:10:54.1468176Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320011057_AddQuotePricingSnapshot' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260320011057_AddQuotePricingSnapshot', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [SubscriptionPlanConfigs] ADD [AllowOnlinePayments] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Quotes] ADD [DepositPercent] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Quotes] ADD [RequiresDeposit] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Invoices] ADD [OnlineAmountPaid] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Invoices] ADD [OnlinePaymentStatus] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Invoices] ADD [OnlineSurchargeCollected] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Invoices] ADD [PaymentLinkExpiresAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Invoices] ADD [PaymentLinkToken] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Invoices] ADD [StripePaymentIntentId] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Companies] ADD [OnlinePaymentSurchargeType] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Companies] ADD [OnlinePaymentSurchargeValue] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Companies] ADD [OnlineSurchargeAcknowledged] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Companies] ADD [StripeAccountId] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + ALTER TABLE [Companies] ADD [StripeConnectStatus] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T23:15:05.6886302Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T23:15:05.6886308Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-20T23:15:05.6886310Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260320231509_AddStripeConnectAndOnlinePayments' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260320231509_AddStripeConnectAndOnlinePayments', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260326230438_AddQuotePhotoSubscriptionLimits' +) +BEGIN + ALTER TABLE [SubscriptionPlanConfigs] ADD [MaxQuotePhotos] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260326230438_AddQuotePhotoSubscriptionLimits' +) +BEGIN + ALTER TABLE [Companies] ADD [MaxQuotePhotosOverride] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260326230438_AddQuotePhotoSubscriptionLimits' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-26T23:04:35.1353265Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260326230438_AddQuotePhotoSubscriptionLimits' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-26T23:04:35.1353273Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260326230438_AddQuotePhotoSubscriptionLimits' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-26T23:04:35.1353275Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260326230438_AddQuotePhotoSubscriptionLimits' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260326230438_AddQuotePhotoSubscriptionLimits', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260328133627_AddJobPhotoIsAiAnalysisPhoto' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-28T13:36:24.1548411Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260328133627_AddJobPhotoIsAiAnalysisPhoto' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-28T13:36:24.1548419Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260328133627_AddJobPhotoIsAiAnalysisPhoto' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-28T13:36:24.1548421Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260328133627_AddJobPhotoIsAiAnalysisPhoto' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260328133627_AddJobPhotoIsAiAnalysisPhoto', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329003300_AddJobDiscountRushFields' +) +BEGIN + ALTER TABLE [Jobs] ADD [DiscountReason] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329003300_AddJobDiscountRushFields' +) +BEGIN + ALTER TABLE [Jobs] ADD [DiscountType] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329003300_AddJobDiscountRushFields' +) +BEGIN + ALTER TABLE [Jobs] ADD [DiscountValue] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329003300_AddJobDiscountRushFields' +) +BEGIN + ALTER TABLE [Jobs] ADD [IsRushJob] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329003300_AddJobDiscountRushFields' +) +BEGIN + ALTER TABLE [JobPhotos] ADD [IsAiAnalysisPhoto] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329003300_AddJobDiscountRushFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T00:32:56.7368710Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329003300_AddJobDiscountRushFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T00:32:56.7368717Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329003300_AddJobDiscountRushFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T00:32:56.7368718Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329003300_AddJobDiscountRushFields' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260329003300_AddJobDiscountRushFields', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + CREATE TABLE [Deposits] ( + [Id] int NOT NULL IDENTITY, + [ReceiptNumber] nvarchar(max) NOT NULL, + [CustomerId] int NOT NULL, + [JobId] int NULL, + [QuoteId] int NULL, + [Amount] decimal(18,2) NOT NULL, + [PaymentMethod] int NOT NULL, + [ReceivedDate] datetime2 NOT NULL, + [Reference] nvarchar(max) NULL, + [Notes] nvarchar(max) NULL, + [RecordedById] nvarchar(450) NULL, + [AppliedToInvoiceId] int NULL, + [AppliedDate] datetime2 NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_Deposits] PRIMARY KEY ([Id]), + CONSTRAINT [FK_Deposits_AspNetUsers_RecordedById] FOREIGN KEY ([RecordedById]) REFERENCES [AspNetUsers] ([Id]), + CONSTRAINT [FK_Deposits_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_Deposits_Invoices_AppliedToInvoiceId] FOREIGN KEY ([AppliedToInvoiceId]) REFERENCES [Invoices] ([Id]) ON DELETE SET NULL, + CONSTRAINT [FK_Deposits_Jobs_JobId] FOREIGN KEY ([JobId]) REFERENCES [Jobs] ([Id]), + CONSTRAINT [FK_Deposits_Quotes_QuoteId] FOREIGN KEY ([QuoteId]) REFERENCES [Quotes] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T00:58:35.7576949Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T00:58:35.7576955Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T00:58:35.7576957Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + CREATE INDEX [IX_Deposits_AppliedToInvoiceId] ON [Deposits] ([AppliedToInvoiceId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + CREATE INDEX [IX_Deposits_CustomerId] ON [Deposits] ([CustomerId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + CREATE INDEX [IX_Deposits_JobId] ON [Deposits] ([JobId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + CREATE INDEX [IX_Deposits_QuoteId] ON [Deposits] ([QuoteId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + CREATE INDEX [IX_Deposits_RecordedById] ON [Deposits] ([RecordedById]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329005838_AddDeposits' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260329005838_AddDeposits', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + DROP INDEX [IX_Invoices_CompanyId_JobId] ON [Invoices]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + DROP INDEX [IX_Invoices_JobId] ON [Invoices]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + DECLARE @var2 sysname; + SELECT @var2 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Invoices]') AND [c].[name] = N'JobId'); + IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Invoices] DROP CONSTRAINT [' + @var2 + '];'); + ALTER TABLE [Invoices] ALTER COLUMN [JobId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + ALTER TABLE [InvoiceItems] ADD [CatalogItemId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + ALTER TABLE [CatalogItems] ADD [InventoryItemId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + ALTER TABLE [CatalogItems] ADD [IsMerchandise] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + ALTER TABLE [CatalogCategories] ADD [IsMerchandise] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T13:47:49.4176542Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T13:47:49.4176549Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T13:47:49.4176551Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [IX_Invoices_CompanyId_JobId] ON [Invoices] ([CompanyId], [JobId]) WHERE [JobId] IS NOT NULL'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [IX_Invoices_JobId] ON [Invoices] ([JobId]) WHERE [JobId] IS NOT NULL'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + CREATE INDEX [IX_InvoiceItems_CatalogItemId] ON [InvoiceItems] ([CatalogItemId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + CREATE INDEX [IX_CatalogItems_InventoryItemId] ON [CatalogItems] ([InventoryItemId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + ALTER TABLE [CatalogItems] ADD CONSTRAINT [FK_CatalogItems_InventoryItems_InventoryItemId] FOREIGN KEY ([InventoryItemId]) REFERENCES [InventoryItems] ([Id]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + ALTER TABLE [InvoiceItems] ADD CONSTRAINT [FK_InvoiceItems_CatalogItems_CatalogItemId] FOREIGN KEY ([CatalogItemId]) REFERENCES [CatalogItems] ([Id]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329134753_AddMerchandise' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260329134753_AddMerchandise', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + ALTER TABLE [InvoiceItems] ADD [GcExpiryDate] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + ALTER TABLE [InvoiceItems] ADD [GcRecipientEmail] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + ALTER TABLE [InvoiceItems] ADD [GcRecipientName] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + ALTER TABLE [InvoiceItems] ADD [GeneratedGiftCertificateId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + ALTER TABLE [InvoiceItems] ADD [IsGiftCertificate] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + ALTER TABLE [GiftCertificates] ADD [SourceInvoiceItemId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T14:11:34.2305437Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T14:11:34.2305443Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-29T14:11:34.2305445Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + CREATE INDEX [IX_InvoiceItems_GeneratedGiftCertificateId] ON [InvoiceItems] ([GeneratedGiftCertificateId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + ALTER TABLE [InvoiceItems] ADD CONSTRAINT [FK_InvoiceItems_GiftCertificates_GeneratedGiftCertificateId] FOREIGN KEY ([GeneratedGiftCertificateId]) REFERENCES [GiftCertificates] ([Id]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260329141137_AddGiftCertificateInvoiceItems' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260329141137_AddGiftCertificateInvoiceItems', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260330234034_AddSalesItemFields' +) +BEGIN + ALTER TABLE [QuoteItems] ADD [IsSalesItem] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260330234034_AddSalesItemFields' +) +BEGIN + ALTER TABLE [QuoteItems] ADD [Sku] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260330234034_AddSalesItemFields' +) +BEGIN + ALTER TABLE [JobItems] ADD [IsSalesItem] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260330234034_AddSalesItemFields' +) +BEGIN + ALTER TABLE [JobItems] ADD [Sku] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260330234034_AddSalesItemFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-30T23:40:30.1483162Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260330234034_AddSalesItemFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-30T23:40:30.1483168Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260330234034_AddSalesItemFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-03-30T23:40:30.1483170Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260330234034_AddSalesItemFields' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260330234034_AddSalesItemFields', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401125630_AddQuoteDepositPaymentFields' +) +BEGIN + ALTER TABLE [Quotes] ADD [DepositAmountPaid] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401125630_AddQuoteDepositPaymentFields' +) +BEGIN + ALTER TABLE [Quotes] ADD [DepositPaymentIntentId] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401125630_AddQuoteDepositPaymentFields' +) +BEGIN + ALTER TABLE [Quotes] ADD [DepositPaymentLinkExpiresAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401125630_AddQuoteDepositPaymentFields' +) +BEGIN + ALTER TABLE [Quotes] ADD [DepositPaymentLinkToken] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401125630_AddQuoteDepositPaymentFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-01T12:56:27.1808248Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401125630_AddQuoteDepositPaymentFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-01T12:56:27.1808254Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401125630_AddQuoteDepositPaymentFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-01T12:56:27.1808255Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401125630_AddQuoteDepositPaymentFields' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260401125630_AddQuoteDepositPaymentFields', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401131724_AddUniqueDocumentNumberConstraints' +) +BEGIN + DECLARE @var3 sysname; + SELECT @var3 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Bills]') AND [c].[name] = N'BalanceDue'); + IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [Bills] DROP CONSTRAINT [' + @var3 + '];'); + ALTER TABLE [Bills] DROP COLUMN [BalanceDue]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401131724_AddUniqueDocumentNumberConstraints' +) +BEGIN + DECLARE @var4 sysname; + SELECT @var4 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[GiftCertificates]') AND [c].[name] = N'CertificateCode'); + IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [GiftCertificates] DROP CONSTRAINT [' + @var4 + '];'); + ALTER TABLE [GiftCertificates] ALTER COLUMN [CertificateCode] nvarchar(450) NOT NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401131724_AddUniqueDocumentNumberConstraints' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-01T13:17:21.8121883Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401131724_AddUniqueDocumentNumberConstraints' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-01T13:17:21.8121891Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401131724_AddUniqueDocumentNumberConstraints' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-01T13:17:21.8121893Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401131724_AddUniqueDocumentNumberConstraints' +) +BEGIN + CREATE UNIQUE INDEX [IX_GiftCertificates_CertificateCode] ON [GiftCertificates] ([CertificateCode]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401131724_AddUniqueDocumentNumberConstraints' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260401131724_AddUniqueDocumentNumberConstraints', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401141653_FixGiftCertificateUniqueIndexPerCompany' +) +BEGIN + DROP INDEX [IX_GiftCertificates_CertificateCode] ON [GiftCertificates]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401141653_FixGiftCertificateUniqueIndexPerCompany' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-01T14:16:49.2887180Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401141653_FixGiftCertificateUniqueIndexPerCompany' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-01T14:16:49.2887185Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401141653_FixGiftCertificateUniqueIndexPerCompany' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-01T14:16:49.2887186Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401141653_FixGiftCertificateUniqueIndexPerCompany' +) +BEGIN + CREATE UNIQUE INDEX [IX_GiftCertificates_CompanyId_CertificateCode] ON [GiftCertificates] ([CompanyId], [CertificateCode]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260401141653_FixGiftCertificateUniqueIndexPerCompany' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260401141653_FixGiftCertificateUniqueIndexPerCompany', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402015422_AddInvoiceExternalReference' +) +BEGIN + ALTER TABLE [Invoices] ADD [ExternalReference] nvarchar(450) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402015422_AddInvoiceExternalReference' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T01:54:18.8649199Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402015422_AddInvoiceExternalReference' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T01:54:18.8649205Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402015422_AddInvoiceExternalReference' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T01:54:18.8649206Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402015422_AddInvoiceExternalReference' +) +BEGIN + CREATE INDEX [IX_Invoices_CompanyId_ExternalReference] ON [Invoices] ([CompanyId], [ExternalReference]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402015422_AddInvoiceExternalReference' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260402015422_AddInvoiceExternalReference', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402032156_AddMigratingFromQuickBooks' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [MigratingFromQuickBooks] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402032156_AddMigratingFromQuickBooks' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T03:21:53.0005398Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402032156_AddMigratingFromQuickBooks' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T03:21:53.0005405Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402032156_AddMigratingFromQuickBooks' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T03:21:53.0005406Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402032156_AddMigratingFromQuickBooks' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260402032156_AddMigratingFromQuickBooks', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402165758_AddQbMigrationStateJson' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [QbMigrationStateJson] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402165758_AddQbMigrationStateJson' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T16:57:55.0246999Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402165758_AddQbMigrationStateJson' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T16:57:55.0247004Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402165758_AddQbMigrationStateJson' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T16:57:55.0247006Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402165758_AddQbMigrationStateJson' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260402165758_AddQbMigrationStateJson', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex' +) +BEGIN + DROP INDEX [IX_InventoryItems_SKU] ON [InventoryItems]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:47:18.8788284Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:47:18.8788291Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:47:18.8788292Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex' +) +BEGIN + CREATE UNIQUE INDEX [IX_InventoryItems_CompanyId_SKU] ON [InventoryItems] ([CompanyId], [SKU]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260402184721_FixInventorySkuUniqueIndex', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex' +) +BEGIN + DROP INDEX [IX_Jobs_ShopAccessCode] ON [Jobs]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:52:13.7857008Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:52:13.7857015Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:52:13.7857016Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex' +) +BEGIN + CREATE UNIQUE INDEX [IX_Jobs_CompanyId_ShopAccessCode] ON [Jobs] ([CompanyId], [ShopAccessCode]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260402185216_FixJobShopAccessCodeUniqueIndex', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402224949_AddDashboardTips' +) +BEGIN + CREATE TABLE [DashboardTips] ( + [Id] int NOT NULL IDENTITY, + [TipText] nvarchar(max) NOT NULL, + [IsActive] bit NOT NULL, + [CreatedAt] datetime2 NOT NULL, + CONSTRAINT [PK_DashboardTips] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402224949_AddDashboardTips' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T22:49:46.0354841Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402224949_AddDashboardTips' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T22:49:46.0354847Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402224949_AddDashboardTips' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T22:49:46.0354849Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260402224949_AddDashboardTips' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260402224949_AddDashboardTips', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents' +) +BEGIN + CREATE TABLE [StripeWebhookEvents] ( + [Id] bigint NOT NULL IDENTITY, + [EventId] nvarchar(max) NOT NULL, + [EventType] nvarchar(max) NOT NULL, + [CompanyId] int NULL, + [RawJson] nvarchar(max) NOT NULL, + [Status] int NOT NULL, + [ErrorMessage] nvarchar(max) NULL, + [ReceivedAt] datetime2 NOT NULL, + [ProcessedAt] datetime2 NULL, + CONSTRAINT [PK_StripeWebhookEvents] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-03T00:06:46.7783905Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-03T00:06:46.7783912Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-03T00:06:46.7783913Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260403000650_AddStripeWebhookEvents', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan' +) +BEGIN + ALTER TABLE [SubscriptionPlanConfigs] ADD [AllowAccounting] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T15:16:32.2541952Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T15:16:32.2541958Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T15:16:32.2541968Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260404151636_AddAllowAccountingToPlan', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath' +) +BEGIN + ALTER TABLE [Bills] ADD [ReceiptFilePath] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T19:41:22.8540290Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T19:41:22.8540296Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T19:41:22.8540297Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260404194126_AddBillReceiptFilePath', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T00:33:47.2862744Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T00:33:47.2862750Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T00:33:47.2862752Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + CREATE INDEX [IX_InventoryTransactions_TransactionType_TransactionDate] ON [InventoryTransactions] ([TransactionType], [TransactionDate]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + CREATE INDEX [IX_InventoryItems_CompanyId_IsActive] ON [InventoryItems] ([CompanyId], [IsActive]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + CREATE INDEX [IX_InventoryItems_IsActive] ON [InventoryItems] ([IsActive]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + CREATE INDEX [IX_Bills_CompanyId_Status] ON [Bills] ([CompanyId], [Status]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + CREATE INDEX [IX_Bills_DueDate] ON [Bills] ([DueDate]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + CREATE INDEX [IX_Bills_Status] ON [Bills] ([Status]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + CREATE INDEX [IX_Appointments_ScheduledStartTime] ON [Appointments] ([ScheduledStartTime]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260405003350_AddPerformanceIndexes', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405155653_AddPlatformSettings' +) +BEGIN + CREATE TABLE [PlatformSettings] ( + [Id] int NOT NULL IDENTITY, + [Key] nvarchar(200) NOT NULL, + [Value] nvarchar(max) NULL, + [Label] nvarchar(max) NULL, + [Description] nvarchar(max) NULL, + [GroupName] nvarchar(max) NULL, + [UpdatedAt] datetime2 NULL, + [UpdatedBy] nvarchar(max) NULL, + CONSTRAINT [PK_PlatformSettings] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405155653_AddPlatformSettings' +) +BEGIN + CREATE UNIQUE INDEX [IX_PlatformSettings_Key] ON [PlatformSettings] ([Key]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405155653_AddPlatformSettings' +) +BEGIN + IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]')) + SET IDENTITY_INSERT [PlatformSettings] ON; + EXEC(N'INSERT INTO [PlatformSettings] ([Id], [Key], [Value], [Label], [Description], [GroupName]) + VALUES (1, N''AdminNotificationEmail'', NULL, N''Admin Notification Email'', N''Email address that receives platform event notifications (new signups, bug reports, subscription events). Leave blank to disable.'', N''Notifications'')'); + IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]')) + SET IDENTITY_INSERT [PlatformSettings] OFF; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405155653_AddPlatformSettings' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T15:56:49.8180443Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405155653_AddPlatformSettings' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T15:56:49.8180449Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405155653_AddPlatformSettings' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T15:56:49.8180450Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405155653_AddPlatformSettings' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260405155653_AddPlatformSettings', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2' +) +BEGIN + DECLARE @var5 sysname; + SELECT @var5 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[PlatformSettings]') AND [c].[name] = N'Key'); + IF @var5 IS NOT NULL EXEC(N'ALTER TABLE [PlatformSettings] DROP CONSTRAINT [' + @var5 + '];'); + ALTER TABLE [PlatformSettings] ALTER COLUMN [Key] nvarchar(200) NOT NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2' +) +BEGIN + IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]')) + SET IDENTITY_INSERT [PlatformSettings] ON; + EXEC(N'INSERT INTO [PlatformSettings] ([Id], [Key], [Value], [Label], [Description], [GroupName]) + VALUES (2, N''BaseUrl'', NULL, N''Base URL'', N''Public URL of this application (e.g. https://app.powdercoatinglogix.com). Used in email links. Falls back to the current request URL if blank.'', N''General''), + (3, N''TrialPeriodDays'', N''7'', N''Trial Period (days)'', N''Number of days a new company gets on the free trial before their subscription expires.'', N''Subscriptions''), + (4, N''QuoteApprovalTokenDays'', N''30'', N''Quote Approval Token Validity (days)'', N''How many days a customer quote-approval link remains valid before expiring.'', N''Quotes''), + (5, N''AuditLogRetentionDays'', N''365'', N''Audit Log Retention (days)'', N''Audit log entries older than this many days are automatically purged by the nightly job.'', N''Data Retention''), + (6, N''StripeWebhookRetentionDays'', N''90'', N''Stripe Webhook Retention (days)'', N''Processed Stripe webhook events older than this many days are automatically purged.'', N''Data Retention'')'); + IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]')) + SET IDENTITY_INSERT [PlatformSettings] OFF; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:12:38.5900904Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:12:38.5900913Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:12:38.5900914Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260405161241_AddPlatformSettingsV2', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription' +) +BEGIN + EXEC(N'UPDATE [PlatformSettings] SET [Description] = N''Email address(es) that receive platform event notifications (new signups, bug reports, subscription events). Separate multiple addresses with commas. Leave blank to disable.'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:21:34.4700837Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:21:34.4700844Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:21:34.4700846Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260405162137_UpdateAdminEmailDescription', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260406191501_MakeBillLineItemAccountIdNullable' +) +BEGIN + DECLARE @var6 sysname; + SELECT @var6 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[BillLineItems]') AND [c].[name] = N'AccountId'); + IF @var6 IS NOT NULL EXEC(N'ALTER TABLE [BillLineItems] DROP CONSTRAINT [' + @var6 + '];'); + ALTER TABLE [BillLineItems] ALTER COLUMN [AccountId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260406191501_MakeBillLineItemAccountIdNullable' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-06T19:14:56.7157942Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260406191501_MakeBillLineItemAccountIdNullable' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-06T19:14:56.7157953Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260406191501_MakeBillLineItemAccountIdNullable' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-06T19:14:56.7157955Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260406191501_MakeBillLineItemAccountIdNullable' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260406191501_MakeBillLineItemAccountIdNullable', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + ALTER TABLE [Jobs] ADD [IntakeCheckedByUserId] nvarchar(450) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + ALTER TABLE [Jobs] ADD [IntakeConditionNotes] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + ALTER TABLE [Jobs] ADD [IntakeDate] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + ALTER TABLE [Jobs] ADD [IntakePartCount] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-08T20:53:42.2947842Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-08T20:53:42.2947847Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-08T20:53:42.2947849Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + CREATE INDEX [IX_Jobs_IntakeCheckedByUserId] ON [Jobs] ([IntakeCheckedByUserId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + ALTER TABLE [Jobs] ADD CONSTRAINT [FK_Jobs_AspNetUsers_IntakeCheckedByUserId] FOREIGN KEY ([IntakeCheckedByUserId]) REFERENCES [AspNetUsers] ([Id]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260408205345_AddJobIntakeFields' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260408205345_AddJobIntakeFields', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260409013822_AddInAppNotifications' +) +BEGIN + CREATE TABLE [InAppNotifications] ( + [Id] int NOT NULL IDENTITY, + [Title] nvarchar(max) NOT NULL, + [Message] nvarchar(max) NOT NULL, + [Link] nvarchar(max) NULL, + [NotificationType] nvarchar(max) NOT NULL, + [IsRead] bit NOT NULL, + [ReadAt] datetime2 NULL, + [QuoteId] int NULL, + [InvoiceId] int NULL, + [CustomerId] int NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_InAppNotifications] PRIMARY KEY ([Id]), + CONSTRAINT [FK_InAppNotifications_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]), + CONSTRAINT [FK_InAppNotifications_Invoices_InvoiceId] FOREIGN KEY ([InvoiceId]) REFERENCES [Invoices] ([Id]), + CONSTRAINT [FK_InAppNotifications_Quotes_QuoteId] FOREIGN KEY ([QuoteId]) REFERENCES [Quotes] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260409013822_AddInAppNotifications' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-09T01:38:18.3630787Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260409013822_AddInAppNotifications' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-09T01:38:18.3630794Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260409013822_AddInAppNotifications' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-09T01:38:18.3630795Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260409013822_AddInAppNotifications' +) +BEGIN + CREATE INDEX [IX_InAppNotifications_CustomerId] ON [InAppNotifications] ([CustomerId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260409013822_AddInAppNotifications' +) +BEGIN + CREATE INDEX [IX_InAppNotifications_InvoiceId] ON [InAppNotifications] ([InvoiceId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260409013822_AddInAppNotifications' +) +BEGIN + CREATE INDEX [IX_InAppNotifications_QuoteId] ON [InAppNotifications] ([QuoteId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260409013822_AddInAppNotifications' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260409013822_AddInAppNotifications', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410021934_AddLegalCompliance' +) +BEGIN + ALTER TABLE [Companies] ADD [MarketingEmailOptOut] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410021934_AddLegalCompliance' +) +BEGIN + ALTER TABLE [Companies] ADD [MarketingUnsubscribeToken] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410021934_AddLegalCompliance' +) +BEGIN + + UPDATE Companies + SET MarketingUnsubscribeToken = LOWER(REPLACE(NEWID(), '-', '')) + WHERE MarketingUnsubscribeToken IS NULL + +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410021934_AddLegalCompliance' +) +BEGIN + DECLARE @var7 sysname; + SELECT @var7 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Companies]') AND [c].[name] = N'MarketingUnsubscribeToken'); + IF @var7 IS NOT NULL EXEC(N'ALTER TABLE [Companies] DROP CONSTRAINT [' + @var7 + '];'); + ALTER TABLE [Companies] ALTER COLUMN [MarketingUnsubscribeToken] nvarchar(max) NOT NULL; + ALTER TABLE [Companies] ADD DEFAULT N'' FOR [MarketingUnsubscribeToken]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410021934_AddLegalCompliance' +) +BEGIN + CREATE TABLE [TermsAcceptances] ( + [Id] int NOT NULL IDENTITY, + [UserId] nvarchar(max) NOT NULL, + [CompanyId] int NOT NULL, + [TosVersion] nvarchar(max) NOT NULL, + [AcceptedAt] datetime2 NOT NULL, + [IpAddress] nvarchar(max) NULL, + [UserAgent] nvarchar(max) NULL, + CONSTRAINT [PK_TermsAcceptances] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410021934_AddLegalCompliance' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-10T02:19:30.4105127Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410021934_AddLegalCompliance' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-10T02:19:30.4105133Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410021934_AddLegalCompliance' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-10T02:19:30.4105135Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410021934_AddLegalCompliance' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260410021934_AddLegalCompliance', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410025353_AddAiFeaturesToPlanConfig' +) +BEGIN + ALTER TABLE [SubscriptionPlanConfigs] ADD [AllowAiInventoryAssist] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410025353_AddAiFeaturesToPlanConfig' +) +BEGIN + ALTER TABLE [SubscriptionPlanConfigs] ADD [AllowAiPhotoQuotes] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410025353_AddAiFeaturesToPlanConfig' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-10T02:53:49.5828243Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410025353_AddAiFeaturesToPlanConfig' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-10T02:53:49.5828250Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410025353_AddAiFeaturesToPlanConfig' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-10T02:53:49.5828252Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410025353_AddAiFeaturesToPlanConfig' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260410025353_AddAiFeaturesToPlanConfig', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410032027_AddTrialsEnabledSetting' +) +BEGIN + IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]')) + SET IDENTITY_INSERT [PlatformSettings] ON; + EXEC(N'INSERT INTO [PlatformSettings] ([Id], [Key], [Value], [Label], [Description], [GroupName]) + VALUES (7, N''TrialsEnabled'', N''true'', N''Free Trials Enabled'', N''When true (default), new signups start with a free trial period. When false, a credit card is required at signup — registrants are sent through Stripe Checkout before their account is created.'', N''Subscriptions'')'); + IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]')) + SET IDENTITY_INSERT [PlatformSettings] OFF; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410032027_AddTrialsEnabledSetting' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-10T03:20:23.1587184Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410032027_AddTrialsEnabledSetting' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-10T03:20:23.1587190Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410032027_AddTrialsEnabledSetting' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-10T03:20:23.1587191Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260410032027_AddTrialsEnabledSetting' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260410032027_AddTrialsEnabledSetting', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412005228_AddMaxTenantsSetting' +) +BEGIN + IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]')) + SET IDENTITY_INSERT [PlatformSettings] ON; + EXEC(N'INSERT INTO [PlatformSettings] ([Id], [Key], [Value], [Label], [Description], [GroupName]) + VALUES (8, N''MaxTenants'', N'''', N''Max Tenants'', N''Maximum number of tenant companies allowed to self-register. Leave blank or 0 for unlimited. Once the limit is reached, the signup page and login signup link are hidden.'', N''Access Control'')'); + IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]')) + SET IDENTITY_INSERT [PlatformSettings] OFF; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412005228_AddMaxTenantsSetting' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-12T00:52:25.3071531Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412005228_AddMaxTenantsSetting' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-12T00:52:25.3071537Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412005228_AddMaxTenantsSetting' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-12T00:52:25.3071538Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412005228_AddMaxTenantsSetting' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260412005228_AddMaxTenantsSetting', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412174157_AddCompanyFeatureOverrides' +) +BEGIN + ALTER TABLE [Companies] ADD [AccountingOverride] bit NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412174157_AddCompanyFeatureOverrides' +) +BEGIN + ALTER TABLE [Companies] ADD [OnlinePaymentsOverride] bit NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412174157_AddCompanyFeatureOverrides' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-12T17:41:53.0142151Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412174157_AddCompanyFeatureOverrides' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-12T17:41:53.0142159Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412174157_AddCompanyFeatureOverrides' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-12T17:41:53.0142161Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412174157_AddCompanyFeatureOverrides' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260412174157_AddCompanyFeatureOverrides', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412183411_AddIsAnnualBilling' +) +BEGIN + ALTER TABLE [Companies] ADD [IsAnnualBilling] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412183411_AddIsAnnualBilling' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-12T18:34:08.9093047Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412183411_AddIsAnnualBilling' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-12T18:34:08.9093054Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412183411_AddIsAnnualBilling' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-12T18:34:08.9093055Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260412183411_AddIsAnnualBilling' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260412183411_AddIsAnnualBilling', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260414135810_AddPendingRegistrationSession' +) +BEGIN + CREATE TABLE [PendingRegistrationSessions] ( + [Id] int NOT NULL IDENTITY, + [Token] nvarchar(max) NOT NULL, + [CompanyName] nvarchar(max) NOT NULL, + [CompanyPhone] nvarchar(max) NULL, + [FirstName] nvarchar(max) NOT NULL, + [LastName] nvarchar(max) NOT NULL, + [Email] nvarchar(max) NOT NULL, + [Plan] int NOT NULL, + [IsAnnual] bit NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [IsCompleted] bit NOT NULL, + CONSTRAINT [PK_PendingRegistrationSessions] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260414135810_AddPendingRegistrationSession' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-14T13:58:07.0916607Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260414135810_AddPendingRegistrationSession' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-14T13:58:07.0916613Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260414135810_AddPendingRegistrationSession' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-14T13:58:07.0916614Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260414135810_AddPendingRegistrationSession' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260414135810_AddPendingRegistrationSession', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415010203_AddSetupWizardCompletionTracking' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [SetupWizardCompletedAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415010203_AddSetupWizardCompletionTracking' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [SetupWizardCompletedByName] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415010203_AddSetupWizardCompletionTracking' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [SetupWizardCompletedByUserId] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415010203_AddSetupWizardCompletionTracking' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-15T01:02:00.3083161Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415010203_AddSetupWizardCompletionTracking' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-15T01:02:00.3083167Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415010203_AddSetupWizardCompletionTracking' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-15T01:02:00.3083169Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415010203_AddSetupWizardCompletionTracking' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260415010203_AddSetupWizardCompletionTracking', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415120000_AddAuditLogTable' +) +BEGIN + CREATE TABLE [AuditLogs] ( + [Id] bigint NOT NULL IDENTITY, + [UserId] nvarchar(max) NULL, + [UserName] nvarchar(max) NOT NULL, + [CompanyId] int NULL, + [CompanyName] nvarchar(max) NULL, + [Action] nvarchar(max) NOT NULL, + [EntityType] nvarchar(450) NOT NULL, + [EntityId] nvarchar(450) NULL, + [EntityDescription] nvarchar(max) NULL, + [OldValues] nvarchar(max) NULL, + [NewValues] nvarchar(max) NULL, + [IpAddress] nvarchar(max) NULL, + [Timestamp] datetime2 NOT NULL, + CONSTRAINT [PK_AuditLogs] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415120000_AddAuditLogTable' +) +BEGIN + CREATE INDEX [IX_AuditLogs_CompanyId_Timestamp] ON [AuditLogs] ([CompanyId], [Timestamp]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415120000_AddAuditLogTable' +) +BEGIN + CREATE INDEX [IX_AuditLogs_EntityType_EntityId] ON [AuditLogs] ([EntityType], [EntityId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260415120000_AddAuditLogTable' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260415120000_AddAuditLogTable', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417044255_AddInventoryTransactionJobId' +) +BEGIN + ALTER TABLE [InventoryTransactions] ADD [JobId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417044255_AddInventoryTransactionJobId' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-17T04:42:51.1510259Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417044255_AddInventoryTransactionJobId' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-17T04:42:51.1510266Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417044255_AddInventoryTransactionJobId' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-17T04:42:51.1510268Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417044255_AddInventoryTransactionJobId' +) +BEGIN + CREATE INDEX [IX_InventoryTransactions_JobId] ON [InventoryTransactions] ([JobId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417044255_AddInventoryTransactionJobId' +) +BEGIN + ALTER TABLE [InventoryTransactions] ADD CONSTRAINT [FK_InventoryTransactions_Jobs_JobId] FOREIGN KEY ([JobId]) REFERENCES [Jobs] ([Id]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417044255_AddInventoryTransactionJobId' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260417044255_AddInventoryTransactionJobId', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417160645_AddSamplePanel' +) +BEGIN + ALTER TABLE [InventoryItems] ADD [HasSamplePanel] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417160645_AddSamplePanel' +) +BEGIN + ALTER TABLE [InventoryItems] ADD [IsCoating] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417160645_AddSamplePanel' +) +BEGIN + ALTER TABLE [InventoryItems] ADD [SamplePanelNotes] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417160645_AddSamplePanel' +) +BEGIN + + UPDATE ii + SET ii.IsCoating = 1 + FROM InventoryItems ii + INNER JOIN InventoryCategoryLookups icl ON icl.Id = ii.InventoryCategoryId + WHERE icl.IsCoating = 1 AND icl.IsDeleted = 0 + +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417160645_AddSamplePanel' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-17T16:06:41.3326769Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417160645_AddSamplePanel' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-17T16:06:41.3326775Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417160645_AddSamplePanel' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-17T16:06:41.3326776Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417160645_AddSamplePanel' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260417160645_AddSamplePanel', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417170543_DropItemIsCoatingColumn' +) +BEGIN + DECLARE @var8 sysname; + SELECT @var8 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[InventoryItems]') AND [c].[name] = N'IsCoating'); + IF @var8 IS NOT NULL EXEC(N'ALTER TABLE [InventoryItems] DROP CONSTRAINT [' + @var8 + '];'); + ALTER TABLE [InventoryItems] DROP COLUMN [IsCoating]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417170543_DropItemIsCoatingColumn' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-17T17:05:40.0377587Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417170543_DropItemIsCoatingColumn' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-17T17:05:40.0377592Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417170543_DropItemIsCoatingColumn' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-17T17:05:40.0377594Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417170543_DropItemIsCoatingColumn' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260417170543_DropItemIsCoatingColumn', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418220837_AddWorkOrderTemplate' +) +BEGIN + DECLARE @var9 sysname; + SELECT @var9 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[InventoryItems]') AND [c].[name] = N'SamplePanelNotes'); + IF @var9 IS NOT NULL EXEC(N'ALTER TABLE [InventoryItems] DROP CONSTRAINT [' + @var9 + '];'); + ALTER TABLE [InventoryItems] DROP COLUMN [SamplePanelNotes]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418220837_AddWorkOrderTemplate' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [WoAccentColor] nvarchar(max) NOT NULL DEFAULT N''; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418220837_AddWorkOrderTemplate' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [WoTerms] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418220837_AddWorkOrderTemplate' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-18T22:08:33.1580643Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418220837_AddWorkOrderTemplate' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-18T22:08:33.1580650Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418220837_AddWorkOrderTemplate' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-18T22:08:33.1580651Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418220837_AddWorkOrderTemplate' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260418220837_AddWorkOrderTemplate', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419201818_AddInvoiceNumberPrefix' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [InvoiceNumberPrefix] nvarchar(max) NOT NULL DEFAULT N''; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419201818_AddInvoiceNumberPrefix' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-19T20:18:14.2123100Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419201818_AddInvoiceNumberPrefix' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-19T20:18:14.2123106Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419201818_AddInvoiceNumberPrefix' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-19T20:18:14.2123107Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419201818_AddInvoiceNumberPrefix' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260419201818_AddInvoiceNumberPrefix', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419215302_AddContactSubmissions' +) +BEGIN + CREATE TABLE [ContactSubmissions] ( + [Id] int NOT NULL IDENTITY, + [SenderName] nvarchar(max) NOT NULL, + [SenderEmail] nvarchar(max) NOT NULL, + [CompanyName] nvarchar(max) NOT NULL, + [Category] nvarchar(max) NOT NULL, + [Subject] nvarchar(max) NOT NULL, + [Message] nvarchar(max) NOT NULL, + [IsRead] bit NOT NULL, + [ReadAt] datetime2 NULL, + [ReadByUserId] nvarchar(max) NULL, + [ReadByUserName] nvarchar(max) NULL, + [AdminNotes] nvarchar(max) NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_ContactSubmissions] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419215302_AddContactSubmissions' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-19T21:52:59.4160772Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419215302_AddContactSubmissions' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-19T21:52:59.4160778Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419215302_AddContactSubmissions' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-19T21:52:59.4160779Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419215302_AddContactSubmissions' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260419215302_AddContactSubmissions', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [BlastNozzleSize] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [BlastRateSqFtPerHourOverride] decimal(18,2) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [BlastSetupType] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [CoatingGunType] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [CoatingRateSqFtPerHourOverride] decimal(18,2) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [CompressorCfm] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [PrimaryBlastSubstrate] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [ShopCapabilityTier] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-20T23:36:06.0440591Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-20T23:36:06.0440597Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-20T23:36:06.0440599Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420233610_AddShopCapabilityProfile' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260420233610_AddShopCapabilityProfile', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421003346_AddCompanyBlastSetups' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T00:33:43.0886131Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421003346_AddCompanyBlastSetups' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T00:33:43.0886138Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421003346_AddCompanyBlastSetups' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T00:33:43.0886139Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421003346_AddCompanyBlastSetups' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260421003346_AddCompanyBlastSetups', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + ALTER TABLE [QuoteItemPrepServices] ADD [BlastSetupId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + ALTER TABLE [PrepServices] ADD [RequiresBlastSetup] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + ALTER TABLE [JobItemPrepServices] ADD [BlastSetupId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + CREATE TABLE [CompanyBlastSetups] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(100) NOT NULL, + [SetupType] int NOT NULL, + [CompressorCfm] decimal(18,2) NOT NULL, + [BlastNozzleSize] int NOT NULL, + [PrimarySubstrate] int NOT NULL, + [BlastRateSqFtPerHourOverride] decimal(18,2) NULL, + [IsDefault] bit NOT NULL, + [IsActive] bit NOT NULL, + [DisplayOrder] int NOT NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_CompanyBlastSetups] PRIMARY KEY ([Id]), + CONSTRAINT [FK_CompanyBlastSetups_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) ON DELETE NO ACTION + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T01:24:05.9835208Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T01:24:05.9835215Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T01:24:05.9835220Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + CREATE INDEX [IX_QuoteItemPrepServices_BlastSetupId] ON [QuoteItemPrepServices] ([BlastSetupId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + CREATE INDEX [IX_JobItemPrepServices_BlastSetupId] ON [JobItemPrepServices] ([BlastSetupId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + CREATE INDEX [IX_CompanyBlastSetups_CompanyId] ON [CompanyBlastSetups] ([CompanyId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + ALTER TABLE [JobItemPrepServices] ADD CONSTRAINT [FK_JobItemPrepServices_CompanyBlastSetups_BlastSetupId] FOREIGN KEY ([BlastSetupId]) REFERENCES [CompanyBlastSetups] ([Id]) ON DELETE SET NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + ALTER TABLE [QuoteItemPrepServices] ADD CONSTRAINT [FK_QuoteItemPrepServices_CompanyBlastSetups_BlastSetupId] FOREIGN KEY ([BlastSetupId]) REFERENCES [CompanyBlastSetups] ([Id]) ON DELETE SET NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421012409_AddCompanyBlastSetupsTable' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260421012409_AddCompanyBlastSetupsTable', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421125956_AddQuoteCostBreakdownColumns2' +) +BEGIN + ALTER TABLE [Quotes] ADD [EquipmentCosts] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421125956_AddQuoteCostBreakdownColumns2' +) +BEGIN + ALTER TABLE [Quotes] ADD [LaborCosts] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421125956_AddQuoteCostBreakdownColumns2' +) +BEGIN + ALTER TABLE [Quotes] ADD [MaterialCosts] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421125956_AddQuoteCostBreakdownColumns2' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T12:59:53.4982640Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421125956_AddQuoteCostBreakdownColumns2' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T12:59:53.4982651Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421125956_AddQuoteCostBreakdownColumns2' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T12:59:53.4982653Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421125956_AddQuoteCostBreakdownColumns2' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260421125956_AddQuoteCostBreakdownColumns2', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421132123_AddQuoteItemCostSnapshot' +) +BEGIN + ALTER TABLE [QuoteItems] ADD [ItemEquipmentCost] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421132123_AddQuoteItemCostSnapshot' +) +BEGIN + ALTER TABLE [QuoteItems] ADD [ItemLaborCost] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421132123_AddQuoteItemCostSnapshot' +) +BEGIN + ALTER TABLE [QuoteItems] ADD [ItemMaterialCost] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421132123_AddQuoteItemCostSnapshot' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T13:21:19.6983052Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421132123_AddQuoteItemCostSnapshot' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T13:21:19.6983058Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421132123_AddQuoteItemCostSnapshot' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T13:21:19.6983060Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421132123_AddQuoteItemCostSnapshot' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260421132123_AddQuoteItemCostSnapshot', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421135923_AddPricingModeAndHideDiscount' +) +BEGIN + ALTER TABLE [Quotes] ADD [HideDiscountFromCustomer] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421135923_AddPricingModeAndHideDiscount' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [PricingMode] int NOT NULL DEFAULT 0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421135923_AddPricingModeAndHideDiscount' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [TargetMarginPercent] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421135923_AddPricingModeAndHideDiscount' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T13:59:19.8113639Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421135923_AddPricingModeAndHideDiscount' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T13:59:19.8113645Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421135923_AddPricingModeAndHideDiscount' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-21T13:59:19.8113646Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260421135923_AddPricingModeAndHideDiscount' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260421135923_AddPricingModeAndHideDiscount', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422040844_AddUserBan' +) +BEGIN + ALTER TABLE [AspNetUsers] ADD [IsBanned] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422040844_AddUserBan' +) +BEGIN + ALTER TABLE [AspNetUsers] ADD [BannedAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422040844_AddUserBan' +) +BEGIN + ALTER TABLE [AspNetUsers] ADD [BanReason] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422040844_AddUserBan' +) +BEGIN + ALTER TABLE [AspNetUsers] ADD [BannedByUserId] nvarchar(450) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422040844_AddUserBan' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-22T04:08:40.9182111Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422040844_AddUserBan' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-22T04:08:40.9182118Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422040844_AddUserBan' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-22T04:08:40.9182119Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422040844_AddUserBan' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260422040844_AddUserBan', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422042129_AddBannedIps' +) +BEGIN + CREATE TABLE [BannedIps] ( + [Id] int NOT NULL IDENTITY, + [IpAddress] nvarchar(45) NOT NULL, + [Reason] nvarchar(500) NULL, + [BannedByUserId] nvarchar(450) NULL, + [BannedAt] datetime2 NOT NULL, + [ExpiresAt] datetime2 NULL, + [IsActive] bit NOT NULL, + CONSTRAINT [PK_BannedIps] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422042129_AddBannedIps' +) +BEGIN + CREATE INDEX [IX_BannedIps_IpAddress_IsActive] ON [BannedIps] ([IpAddress], [IsActive]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422042129_AddBannedIps' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-22T04:21:26.9627092Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422042129_AddBannedIps' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-22T04:21:26.9627100Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422042129_AddBannedIps' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-22T04:21:26.9627102Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260422042129_AddBannedIps' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260422042129_AddBannedIps', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423011936_AddAiUsageLog' +) +BEGIN + CREATE TABLE [AiUsageLogs] ( + [Id] bigint NOT NULL IDENTITY, + [CompanyId] int NOT NULL, + [UserId] nvarchar(max) NOT NULL, + [Feature] nvarchar(max) NOT NULL, + [Success] bit NOT NULL, + [InputLength] int NOT NULL, + [CalledAt] datetime2 NOT NULL, + CONSTRAINT [PK_AiUsageLogs] PRIMARY KEY ([Id]), + CONSTRAINT [FK_AiUsageLogs_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423011936_AddAiUsageLog' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-23T01:19:33.1714669Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423011936_AddAiUsageLog' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-23T01:19:33.1714674Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423011936_AddAiUsageLog' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-23T01:19:33.1714676Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423011936_AddAiUsageLog' +) +BEGIN + CREATE INDEX [IX_AiUsageLogs_CompanyId_CalledAt] ON [AiUsageLogs] ([CompanyId], [CalledAt]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423011936_AddAiUsageLog' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260423011936_AddAiUsageLog', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423015446_AddSmsOptedOutAt' +) +BEGIN + ALTER TABLE [Customers] ADD [SmsOptedOutAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423015446_AddSmsOptedOutAt' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-23T01:54:43.1815272Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423015446_AddSmsOptedOutAt' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-23T01:54:43.1815281Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423015446_AddSmsOptedOutAt' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-23T01:54:43.1815283Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260423015446_AddSmsOptedOutAt' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260423015446_AddSmsOptedOutAt', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260424232825_AddFacilityOverheadFields' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [MonthlyBillableHours] int NOT NULL DEFAULT 160; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260424232825_AddFacilityOverheadFields' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [MonthlyRent] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260424232825_AddFacilityOverheadFields' +) +BEGIN + ALTER TABLE [CompanyOperatingCosts] ADD [MonthlyUtilities] decimal(18,2) NOT NULL DEFAULT 0.0; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260424232825_AddFacilityOverheadFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-24T23:28:22.1047155Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260424232825_AddFacilityOverheadFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-24T23:28:22.1047162Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260424232825_AddFacilityOverheadFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-24T23:28:22.1047164Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260424232825_AddFacilityOverheadFields' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260424232825_AddFacilityOverheadFields', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425123256_AddCatalogItemImages' +) +BEGIN + ALTER TABLE [CatalogItems] ADD [ImagePath] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425123256_AddCatalogItemImages' +) +BEGIN + ALTER TABLE [CatalogItems] ADD [ThumbnailPath] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425123256_AddCatalogItemImages' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-25T12:32:52.2955147Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425123256_AddCatalogItemImages' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-25T12:32:52.2955155Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425123256_AddCatalogItemImages' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-25T12:32:52.2955156Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425123256_AddCatalogItemImages' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260425123256_AddCatalogItemImages', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425182712_AddUserPasskeys' +) +BEGIN + CREATE TABLE [UserPasskeys] ( + [Id] int NOT NULL IDENTITY, + [UserId] nvarchar(max) NOT NULL, + [CompanyId] int NOT NULL, + [CredentialId] varbinary(900) NOT NULL, + [PublicKey] varbinary(max) NOT NULL, + [UserHandle] varbinary(max) NOT NULL, + [SignCount] bigint NOT NULL, + [DeviceFriendlyName] nvarchar(max) NULL, + [CreatedAt] datetime2 NOT NULL, + [LastUsedAt] datetime2 NULL, + CONSTRAINT [PK_UserPasskeys] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425182712_AddUserPasskeys' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-25T18:27:08.5374555Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425182712_AddUserPasskeys' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-25T18:27:08.5374562Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425182712_AddUserPasskeys' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-25T18:27:08.5374563Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425182712_AddUserPasskeys' +) +BEGIN + CREATE UNIQUE INDEX [IX_UserPasskeys_CredentialId] ON [UserPasskeys] ([CredentialId]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425182712_AddUserPasskeys' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260425182712_AddUserPasskeys', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425223454_AddCatalogPriceCheckReport' +) +BEGIN + CREATE TABLE [CatalogPriceCheckReports] ( + [Id] int NOT NULL IDENTITY, + [RunAt] datetime2 NOT NULL, + [ItemsChecked] int NOT NULL, + [ResultsJson] nvarchar(max) NOT NULL, + [OperatingCostsSummary] nvarchar(max) NOT NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_CatalogPriceCheckReports] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425223454_AddCatalogPriceCheckReport' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-25T22:34:50.0016987Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425223454_AddCatalogPriceCheckReport' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-25T22:34:50.0016993Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425223454_AddCatalogPriceCheckReport' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-25T22:34:50.0016994Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260425223454_AddCatalogPriceCheckReport' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260425223454_AddCatalogPriceCheckReport', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426122625_AddAiCatalogPriceCheckGating' +) +BEGIN + ALTER TABLE [SubscriptionPlanConfigs] ADD [AllowAiCatalogPriceCheck] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426122625_AddAiCatalogPriceCheckGating' +) +BEGIN + ALTER TABLE [Companies] ADD [AiCatalogPriceCheckEnabled] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426122625_AddAiCatalogPriceCheckGating' +) +BEGIN + IF NOT EXISTS (SELECT 1 FROM [PlatformSettings] WHERE [Key] = N'AiCatalogPriceCheckEnabled') + BEGIN + INSERT INTO [PlatformSettings] ([Key], [Value], [Label], [Description], [GroupName]) + VALUES (N'AiCatalogPriceCheckEnabled', N'true', N'AI Catalog Price Check Enabled', + N'When true (default), the AI Catalog Price Check feature is available to companies on qualifying plans. Set to false to disable it platform-wide.', + N'AI Features'); + END +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426122625_AddAiCatalogPriceCheckGating' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-26T12:26:21.7275012Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426122625_AddAiCatalogPriceCheckGating' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-26T12:26:21.7275018Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426122625_AddAiCatalogPriceCheckGating' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-26T12:26:21.7275020Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426122625_AddAiCatalogPriceCheckGating' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260426122625_AddAiCatalogPriceCheckGating', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426142825_AddPasskeyPromptDismissed' +) +BEGIN + ALTER TABLE [AspNetUsers] ADD [PasskeyPromptDismissed] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426142825_AddPasskeyPromptDismissed' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-26T14:28:21.4545921Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426142825_AddPasskeyPromptDismissed' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-26T14:28:21.4545931Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426142825_AddPasskeyPromptDismissed' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-26T14:28:21.4545932Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260426142825_AddPasskeyPromptDismissed' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260426142825_AddPasskeyPromptDismissed', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [FirstInvoiceCreatedAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [FirstJobCreatedAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [FirstQuoteCreatedAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [FirstWorkflowCompleted] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [FirstWorkflowCompletedAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [GuidedActivationDismissedAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + ALTER TABLE [CompanyPreferences] ADD [OnboardingPath] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-28T16:40:22.3595055Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-28T16:40:22.3595063Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-28T16:40:22.3595065Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260428164026_AddGuidedActivationFields' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260428164026_AddGuidedActivationFields', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429220019_AddJobQuoteSnapshotUpdatedAt' +) +BEGIN + ALTER TABLE [Jobs] ADD [QuoteSnapshotUpdatedAt] datetime2 NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429220019_AddJobQuoteSnapshotUpdatedAt' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-29T22:00:14.7474877Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429220019_AddJobQuoteSnapshotUpdatedAt' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-29T22:00:14.7474884Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429220019_AddJobQuoteSnapshotUpdatedAt' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-29T22:00:14.7474886Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429220019_AddJobQuoteSnapshotUpdatedAt' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260429220019_AddJobQuoteSnapshotUpdatedAt', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429221217_AddInventoryItemImageUrl' +) +BEGIN + ALTER TABLE [InventoryItems] ADD [ImageUrl] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429221217_AddInventoryItemImageUrl' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-29T22:12:13.9939171Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429221217_AddInventoryItemImageUrl' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-29T22:12:13.9939177Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429221217_AddInventoryItemImageUrl' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-29T22:12:13.9939179Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260429221217_AddInventoryItemImageUrl' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260429221217_AddInventoryItemImageUrl', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502000902_AddSmsGating' +) +BEGIN + ALTER TABLE [SubscriptionPlanConfigs] ADD [AllowSms] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502000902_AddSmsGating' +) +BEGIN + ALTER TABLE [Companies] ADD [SmsDisabledByAdmin] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502000902_AddSmsGating' +) +BEGIN + ALTER TABLE [Companies] ADD [SmsEnabled] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502000902_AddSmsGating' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-02T00:08:58.8800523Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502000902_AddSmsGating' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-02T00:08:58.8800529Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502000902_AddSmsGating' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-02T00:08:58.8800531Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502000902_AddSmsGating' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260502000902_AddSmsGating', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502002653_AddCompanySmsAgreement' +) +BEGIN + CREATE TABLE [CompanySmsAgreements] ( + [Id] int NOT NULL IDENTITY, + [AgreedByUserId] nvarchar(max) NOT NULL, + [AgreedByUserName] nvarchar(max) NOT NULL, + [AgreedAt] datetime2 NOT NULL, + [IpAddress] nvarchar(max) NULL, + [UserAgent] nvarchar(max) NULL, + [TermsVersion] nvarchar(max) NOT NULL, + [CompanyId] int NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [CreatedBy] nvarchar(max) NULL, + [UpdatedBy] nvarchar(max) NULL, + [IsDeleted] bit NOT NULL, + [DeletedAt] datetime2 NULL, + [DeletedBy] nvarchar(max) NULL, + CONSTRAINT [PK_CompanySmsAgreements] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502002653_AddCompanySmsAgreement' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-02T00:26:49.3814933Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502002653_AddCompanySmsAgreement' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-02T00:26:49.3814939Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502002653_AddCompanySmsAgreement' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-02T00:26:49.3814941Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260502002653_AddCompanySmsAgreement' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260502002653_AddCompanySmsAgreement', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503165943_AddPowderCatalogItem' +) +BEGIN + ALTER TABLE [InventoryItems] ADD [SdsUrl] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503165943_AddPowderCatalogItem' +) +BEGIN + ALTER TABLE [InventoryItems] ADD [TdsUrl] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503165943_AddPowderCatalogItem' +) +BEGIN + CREATE TABLE [PowderCatalogItems] ( + [Id] int NOT NULL IDENTITY, + [VendorName] nvarchar(450) NOT NULL, + [Sku] nvarchar(450) NOT NULL, + [ColorName] nvarchar(450) NOT NULL, + [Description] nvarchar(max) NULL, + [UnitPrice] decimal(18,2) NOT NULL, + [PriceTiersJson] nvarchar(max) NULL, + [ImageUrl] nvarchar(max) NULL, + [SdsUrl] nvarchar(max) NULL, + [TdsUrl] nvarchar(max) NULL, + [ApplicationGuideUrl] nvarchar(max) NULL, + [ProductUrl] nvarchar(max) NULL, + [IsDiscontinued] bit NOT NULL, + [CreatedAt] datetime2 NOT NULL, + [UpdatedAt] datetime2 NULL, + [LastSyncedAt] datetime2 NULL, + CONSTRAINT [PK_PowderCatalogItems] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503165943_AddPowderCatalogItem' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-03T16:59:39.5543667Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503165943_AddPowderCatalogItem' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-03T16:59:39.5543674Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503165943_AddPowderCatalogItem' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-03T16:59:39.5543675Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503165943_AddPowderCatalogItem' +) +BEGIN + CREATE INDEX [IX_PowderCatalogItems_ColorName] ON [PowderCatalogItems] ([ColorName]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503165943_AddPowderCatalogItem' +) +BEGIN + CREATE UNIQUE INDEX [IX_PowderCatalogItems_Vendor_Sku] ON [PowderCatalogItems] ([VendorName], [Sku]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503165943_AddPowderCatalogItem' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260503165943_AddPowderCatalogItem', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + ALTER TABLE [PowderCatalogItems] ADD [ColorFamilies] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + ALTER TABLE [PowderCatalogItems] ADD [CoverageSqFtPerLb] decimal(18,2) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + ALTER TABLE [PowderCatalogItems] ADD [CureTemperatureF] decimal(18,2) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + ALTER TABLE [PowderCatalogItems] ADD [CureTimeMinutes] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + ALTER TABLE [PowderCatalogItems] ADD [Finish] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + ALTER TABLE [PowderCatalogItems] ADD [IsUserContributed] bit NOT NULL DEFAULT CAST(0 AS bit); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + ALTER TABLE [PowderCatalogItems] ADD [RequiresClearCoat] bit NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + ALTER TABLE [PowderCatalogItems] ADD [TransferEfficiency] decimal(18,2) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-03T20:30:44.9555184Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-03T20:30:44.9555189Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-03T20:30:44.9555191Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260503203048_AddPowderCatalogSpecFields' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260503203048_AddPowderCatalogSpecFields', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505161757_AddGracePeriodDaysSetting' +) +BEGIN + IF NOT EXISTS (SELECT 1 FROM [PlatformSettings] WHERE [Key] = N'GracePeriodDays') + BEGIN + INSERT INTO [PlatformSettings] ([Key], [Value], [Label], [Description], [GroupName]) + VALUES (N'GracePeriodDays', N'14', N'Grace Period (days)', + N'Number of days a company can continue to log in after their subscription expires before being fully locked out.', + N'Subscriptions'); + END +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505161757_AddGracePeriodDaysSetting' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-05T16:17:52.7808281Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505161757_AddGracePeriodDaysSetting' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-05T16:17:52.7808287Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505161757_AddGracePeriodDaysSetting' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-05T16:17:52.7808289Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505161757_AddGracePeriodDaysSetting' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260505161757_AddGracePeriodDaysSetting', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505191333_AddGracePeriodAppliesToTrials' +) +BEGIN + IF NOT EXISTS (SELECT 1 FROM [PlatformSettings] WHERE [Key] = N'GracePeriodAppliesToTrials') + BEGIN + INSERT INTO [PlatformSettings] ([Key], [Value], [Label], [Description], [GroupName]) + VALUES (N'GracePeriodAppliesToTrials', N'false', N'Grace Period Applies to Trials', + N'When false (default), trial accounts (no Stripe subscription) are locked out immediately when their trial expires — no grace period. Enable to give trial accounts the same grace period as paid accounts.', + N'Subscriptions'); + END +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505191333_AddGracePeriodAppliesToTrials' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-05T19:13:29.5374011Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505191333_AddGracePeriodAppliesToTrials' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-05T19:13:29.5374019Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505191333_AddGracePeriodAppliesToTrials' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-05T19:13:29.5374021Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505191333_AddGracePeriodAppliesToTrials' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260505191333_AddGracePeriodAppliesToTrials', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505231018_MigrateTimeEntriesToUserId' +) +BEGIN + ALTER TABLE [JobTimeEntries] DROP CONSTRAINT [FK_JobTimeEntries_ShopWorkers_ShopWorkerId]; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505231018_MigrateTimeEntriesToUserId' +) +BEGIN + DECLARE @var10 sysname; + SELECT @var10 = [d].[name] + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[JobTimeEntries]') AND [c].[name] = N'ShopWorkerId'); + IF @var10 IS NOT NULL EXEC(N'ALTER TABLE [JobTimeEntries] DROP CONSTRAINT [' + @var10 + '];'); + ALTER TABLE [JobTimeEntries] ALTER COLUMN [ShopWorkerId] int NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505231018_MigrateTimeEntriesToUserId' +) +BEGIN + ALTER TABLE [JobTimeEntries] ADD [UserDisplayName] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505231018_MigrateTimeEntriesToUserId' +) +BEGIN + ALTER TABLE [JobTimeEntries] ADD [UserId] nvarchar(max) NULL; +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505231018_MigrateTimeEntriesToUserId' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-05T23:10:14.7638603Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505231018_MigrateTimeEntriesToUserId' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-05T23:10:14.7638610Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505231018_MigrateTimeEntriesToUserId' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-05T23:10:14.7638612Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505231018_MigrateTimeEntriesToUserId' +) +BEGIN + ALTER TABLE [JobTimeEntries] ADD CONSTRAINT [FK_JobTimeEntries_ShopWorkers_ShopWorkerId] FOREIGN KEY ([ShopWorkerId]) REFERENCES [ShopWorkers] ([Id]); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260505231018_MigrateTimeEntriesToUserId' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260505231018_MigrateTimeEntriesToUserId', N'8.0.11'); +END; +GO + +COMMIT; +GO + +BEGIN TRANSACTION; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260506020726_AddDataProtectionKeys' +) +BEGIN + CREATE TABLE [DataProtectionKeys] ( + [Id] int NOT NULL IDENTITY, + [FriendlyName] nvarchar(max) NULL, + [Xml] nvarchar(max) NULL, + CONSTRAINT [PK_DataProtectionKeys] PRIMARY KEY ([Id]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260506020726_AddDataProtectionKeys' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-06T02:07:22.6252199Z'' + WHERE [Id] = 1; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260506020726_AddDataProtectionKeys' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-06T02:07:22.6252206Z'' + WHERE [Id] = 2; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260506020726_AddDataProtectionKeys' +) +BEGIN + EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-05-06T02:07:22.6252208Z'' + WHERE [Id] = 3; + SELECT @@ROWCOUNT'); +END; +GO + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260506020726_AddDataProtectionKeys' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260506020726_AddDataProtectionKeys', N'8.0.11'); +END; +GO + +COMMIT; +GO + diff --git a/src/PowderCoating.Application/Interfaces/IInventoryAiLookupService.cs b/src/PowderCoating.Application/Interfaces/IInventoryAiLookupService.cs index 92da698..97ea248 100644 --- a/src/PowderCoating.Application/Interfaces/IInventoryAiLookupService.cs +++ b/src/PowderCoating.Application/Interfaces/IInventoryAiLookupService.cs @@ -20,6 +20,7 @@ public class InventoryAiLookupResult public bool? RequiresClearCoat { get; set; } // Application properties + public decimal? SpecificGravity { get; set; } // used to derive theoretical coverage when docs omit coverage public decimal? CoverageSqFtPerLb { get; set; } // typical ~80-120 sq ft/lb public decimal? TransferEfficiency { get; set; } // typical 50-75% public decimal? UnitCostPerLb { get; set; } // price per lb/unit if found in search results diff --git a/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs b/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs index 497ca52..a49bb70 100644 --- a/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs +++ b/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs @@ -27,6 +27,9 @@ namespace PowderCoating.Infrastructure.Services; /// public class InventoryAiLookupService : IInventoryAiLookupService { + private const decimal DefaultTransferEfficiency = 65m; + private const decimal TheoreticalCoverageAtOneMilFactor = 192.3m; + private readonly IConfiguration _config; private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; @@ -47,6 +50,7 @@ Respond ONLY with a valid JSON object — no markdown, no explanation: ""cureTimeMinutes"": number or null, ""colorFamilies"": ""comma-separated list from: Red,Orange,Yellow,Green,Blue,Purple,Pink,Brown,Black,White,Gray,Silver,Gold,Bronze,Copper,Clear — or null if unknown"", ""requiresClearCoat"": true or false or null, + ""specificGravity"": number or null, ""coverageSqFtPerLb"": number or null, ""transferEfficiency"": number or null, ""unitCostPerLb"": number or null, @@ -250,6 +254,7 @@ Rules: result.CureTimeMinutes = GetInt(parsed, "cureTimeMinutes"); result.ColorFamilies = GetString(parsed, "colorFamilies"); result.RequiresClearCoat = GetBool(parsed, "requiresClearCoat"); + result.SpecificGravity = GetDecimal(parsed, "specificGravity"); result.CoverageSqFtPerLb = GetDecimal(parsed, "coverageSqFtPerLb"); result.TransferEfficiency = GetDecimal(parsed, "transferEfficiency"); result.UnitCostPerLb = GetDecimal(parsed, "unitCostPerLb"); @@ -260,6 +265,7 @@ Rules: result.ImageUrl = pageImageUrl; result.Reasoning = GetString(parsed, "reasoning"); + ApplyPowderFallbacks(result); return result; } catch (Exception ex) @@ -366,7 +372,7 @@ Rules: } var parsed = JsonSerializer.Deserialize(rawText); - return new InventoryAiLookupResult + var result = new InventoryAiLookupResult { Success = true, Manufacturer = GetString(parsed, "manufacturer"), @@ -378,11 +384,15 @@ Rules: CureTimeMinutes = GetInt(parsed, "cureTimeMinutes"), ColorFamilies = GetString(parsed, "colorFamilies"), RequiresClearCoat = GetBool(parsed, "requiresClearCoat"), + SpecificGravity = GetDecimal(parsed, "specificGravity"), CoverageSqFtPerLb = GetDecimal(parsed, "coverageSqFtPerLb"), TransferEfficiency = GetDecimal(parsed, "transferEfficiency"), VendorName = GetString(parsed, "vendorName"), Reasoning = GetString(parsed, "reasoning"), }; + + ApplyPowderFallbacks(result); + return result; } catch (Exception ex) { @@ -447,7 +457,7 @@ Rules: } var parsed = JsonSerializer.Deserialize(rawText); - return new InventoryAiLookupResult + var result = new InventoryAiLookupResult { Success = true, Manufacturer = GetString(parsed, "manufacturer"), @@ -460,6 +470,7 @@ Rules: CureTimeMinutes = GetInt(parsed, "cureTimeMinutes"), ColorFamilies = GetString(parsed, "colorFamilies"), RequiresClearCoat = GetBool(parsed, "requiresClearCoat"), + SpecificGravity = GetDecimal(parsed, "specificGravity"), CoverageSqFtPerLb = GetDecimal(parsed, "coverageSqFtPerLb"), TransferEfficiency = GetDecimal(parsed, "transferEfficiency"), UnitCostPerLb = GetDecimal(parsed, "unitCostPerLb"), @@ -470,6 +481,9 @@ Rules: ImageUrl = pageImageUrl, Reasoning = GetString(parsed, "reasoning"), }; + + ApplyPowderFallbacks(result); + return result; } catch (Exception ex) { @@ -1226,4 +1240,15 @@ Rules: } return null; } + + private static void ApplyPowderFallbacks(InventoryAiLookupResult result) + { + result.TransferEfficiency ??= DefaultTransferEfficiency; + + if (!result.CoverageSqFtPerLb.HasValue && result.SpecificGravity is > 0) + { + var calculatedCoverage = TheoreticalCoverageAtOneMilFactor / result.SpecificGravity.Value; + result.CoverageSqFtPerLb = Math.Round(calculatedCoverage, 2, MidpointRounding.AwayFromZero); + } + } } diff --git a/src/PowderCoating.Web/Controllers/InventoryController.cs b/src/PowderCoating.Web/Controllers/InventoryController.cs index ca3313f..7afdf85 100644 --- a/src/PowderCoating.Web/Controllers/InventoryController.cs +++ b/src/PowderCoating.Web/Controllers/InventoryController.cs @@ -20,6 +20,8 @@ namespace PowderCoating.Web.Controllers; [Authorize(Policy = AppConstants.Policies.CanManageInventory)] public class InventoryController : Controller { + private const decimal DefaultTransferEfficiency = 65m; + private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; private readonly ILogger _logger; @@ -745,7 +747,7 @@ public class InventoryController : Controller if (match.ColorFamilies != null) result.ColorFamilies = match.ColorFamilies; if (match.RequiresClearCoat != null) result.RequiresClearCoat = match.RequiresClearCoat; if (match.CoverageSqFtPerLb != null) result.CoverageSqFtPerLb = match.CoverageSqFtPerLb; - if (match.TransferEfficiency != null) result.TransferEfficiency = match.TransferEfficiency; + result.TransferEfficiency ??= GetEffectiveTransferEfficiency(match.TransferEfficiency); // URL / price fields: fill gaps only — AI may have found something better result.ImageUrl ??= match.ImageUrl; result.SpecPageUrl ??= match.ProductUrl; @@ -775,7 +777,7 @@ public class InventoryController : Controller ColorFamilies = result.ColorFamilies, RequiresClearCoat = result.RequiresClearCoat, CoverageSqFtPerLb = result.CoverageSqFtPerLb, - TransferEfficiency = result.TransferEfficiency, + TransferEfficiency = GetEffectiveTransferEfficiency(result.TransferEfficiency), ImageUrl = result.ImageUrl, ProductUrl = result.SpecPageUrl, SdsUrl = result.SdsUrl, @@ -873,7 +875,7 @@ public class InventoryController : Controller aiResult.CureTimeMinutes ??= full.CureTimeMinutes; aiResult.RequiresClearCoat ??= full.RequiresClearCoat; aiResult.CoverageSqFtPerLb ??= full.CoverageSqFtPerLb; - aiResult.TransferEfficiency ??= full.TransferEfficiency; + aiResult.TransferEfficiency ??= GetEffectiveTransferEfficiency(full.TransferEfficiency); aiResult.ManufacturerPartNumber ??= full.ManufacturerPartNumber; aiResult.ColorName ??= full.ColorName; aiResult.ColorCode ??= full.ColorCode; @@ -952,7 +954,7 @@ public class InventoryController : Controller colorFamilies = aiResult.ColorFamilies, requiresClearCoat = aiResult.RequiresClearCoat, coverageSqFtPerLb = aiResult.CoverageSqFtPerLb, - transferEfficiency = aiResult.TransferEfficiency, + transferEfficiency = aiResult.TransferEfficiency ?? DefaultTransferEfficiency, unitPrice = aiResult.UnitCostPerLb ?? 0m, imageUrl = aiResult.ImageUrl, productUrl = aiResult.SpecPageUrl, @@ -1104,13 +1106,18 @@ public class InventoryController : Controller colorFamilies = p.ColorFamilies, requiresClearCoat = p.RequiresClearCoat, coverageSqFtPerLb = p.CoverageSqFtPerLb, - transferEfficiency = p.TransferEfficiency + transferEfficiency = GetEffectiveTransferEfficiency(p.TransferEfficiency) }) .ToList(); return Json(results); } + private static decimal GetEffectiveTransferEfficiency(decimal? transferEfficiency) + { + return transferEfficiency ?? DefaultTransferEfficiency; + } + /// /// Normalizes a string to title-case using the current culture's TextInfo. Applied to /// inventory item names on create and edit so the list view is consistently formatted diff --git a/src/PowderCoating.Web/Controllers/PowderCatalogController.cs b/src/PowderCoating.Web/Controllers/PowderCatalogController.cs index 3741e09..9160d3c 100644 --- a/src/PowderCoating.Web/Controllers/PowderCatalogController.cs +++ b/src/PowderCoating.Web/Controllers/PowderCatalogController.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using PowderCoating.Application.DTOs.Common; using PowderCoating.Application.DTOs.Inventory; +using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; using PowderCoating.Core.Interfaces; using PowderCoating.Shared.Constants; +using PowderCoating.Web.ViewModels.PowderCatalog; using System.Text.Json; using System.Text.RegularExpressions; @@ -12,34 +15,324 @@ namespace PowderCoating.Web.Controllers; [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public class PowderCatalogController : Controller { + private const decimal DefaultTransferEfficiency = 65m; + private readonly IUnitOfWork _unitOfWork; + private readonly IInventoryAiLookupService _aiLookupService; private readonly ILogger _logger; - public PowderCatalogController(IUnitOfWork unitOfWork, ILogger logger) + public PowderCatalogController( + IUnitOfWork unitOfWork, + IInventoryAiLookupService aiLookupService, + ILogger logger) { _unitOfWork = unitOfWork; + _aiLookupService = aiLookupService; _logger = logger; } /// /// Shows platform-level catalog stats and the import form. /// - public async Task Index() + public async Task Index( + string? searchTerm, + string? vendorName, + string status = "all", + string source = "all", + string completeness = "all", + string? sortColumn = null, + string sortDirection = "asc", + int pageNumber = 1, + int pageSize = 25) { var all = await _unitOfWork.PowderCatalog.GetAllAsync(); var list = all.ToList(); - var stats = new PowderCatalogStatsDto + var stats = BuildStats(list); + var vendors = list + .Select(p => p.VendorName) + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(v => v) + .ToList(); + + status = NormalizeFilter(status, "all", "active", "discontinued"); + source = NormalizeFilter(source, "all", "curated", "contributed"); + completeness = NormalizeFilter(completeness, "all", "ready", "missing-specs", "missing-docs", "missing-image"); + sortDirection = sortDirection?.Equals("desc", StringComparison.OrdinalIgnoreCase) == true ? "desc" : "asc"; + sortColumn = NormalizeSortColumn(sortColumn); + pageNumber = Math.Max(pageNumber, 1); + pageSize = pageSize is < 1 or > 100 ? 25 : pageSize; + + IEnumerable query = list; + + if (!string.IsNullOrWhiteSpace(searchTerm)) { - TotalProducts = list.Count, - ActiveProducts = list.Count(p => !p.IsDiscontinued), - DiscontinuedProducts = list.Count(p => p.IsDiscontinued), - VendorCount = list.Select(p => p.VendorName).Distinct(StringComparer.OrdinalIgnoreCase).Count(), - UserContributedProducts = list.Count(p => p.IsUserContributed), - LastImportedAt = list.Any() ? list.Max(p => p.LastSyncedAt) : null + var term = searchTerm.Trim(); + query = query.Where(p => + ContainsIgnoreCase(p.VendorName, term) || + ContainsIgnoreCase(p.Sku, term) || + ContainsIgnoreCase(p.ColorName, term) || + ContainsIgnoreCase(p.Description, term) || + ContainsIgnoreCase(p.Finish, term)); + } + + if (!string.IsNullOrWhiteSpace(vendorName)) + query = query.Where(p => string.Equals(p.VendorName, vendorName.Trim(), StringComparison.OrdinalIgnoreCase)); + + query = status switch + { + "active" => query.Where(p => !p.IsDiscontinued), + "discontinued" => query.Where(p => p.IsDiscontinued), + _ => query }; - return View(stats); + query = source switch + { + "curated" => query.Where(p => !p.IsUserContributed), + "contributed" => query.Where(p => p.IsUserContributed), + _ => query + }; + + query = completeness switch + { + "ready" => query.Where(IsCatalogReady), + "missing-specs" => query.Where(p => !HasCoreSpecs(p)), + "missing-docs" => query.Where(p => !HasDocuments(p)), + "missing-image" => query.Where(p => string.IsNullOrWhiteSpace(p.ImageUrl)), + _ => query + }; + + query = ApplySort(query, sortColumn, sortDirection); + + var totalCount = query.Count(); + var items = query + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Select(MapListItem) + .ToList(); + + var vm = new PowderCatalogIndexViewModel + { + Stats = stats, + Vendors = vendors, + SearchTerm = searchTerm, + VendorName = vendorName, + Status = status, + Source = source, + Completeness = completeness, + SortColumn = sortColumn, + SortDirection = sortDirection, + Catalog = new PagedResult + { + Items = items, + PageNumber = pageNumber, + PageSize = pageSize, + TotalCount = totalCount, + SortColumn = sortColumn, + SortDirection = sortDirection, + SearchTerm = searchTerm + } + }; + + return View(vm); + } + + public IActionResult Create() + { + return View(new PowderCatalogFormViewModel + { + CreatedAt = DateTime.UtcNow, + TransferEfficiency = DefaultTransferEfficiency + }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(PowderCatalogFormViewModel model) + { + NormalizeModel(model); + + if (!ModelState.IsValid) + return View(model); + + if (await CatalogRecordExistsAsync(model.VendorName, model.Sku)) + { + ModelState.AddModelError(string.Empty, "A powder catalog item with this vendor and SKU already exists."); + return View(model); + } + + var now = DateTime.UtcNow; + var entity = new PowderCatalogItem + { + VendorName = model.VendorName, + Sku = model.Sku, + ColorName = model.ColorName, + Description = NullIfWhiteSpace(model.Description), + UnitPrice = model.UnitPrice, + ImageUrl = NullIfWhiteSpace(model.ImageUrl), + SdsUrl = NullIfWhiteSpace(model.SdsUrl), + TdsUrl = NullIfWhiteSpace(model.TdsUrl), + ApplicationGuideUrl = NullIfWhiteSpace(model.ApplicationGuideUrl), + ProductUrl = NullIfWhiteSpace(model.ProductUrl), + CureTemperatureF = model.CureTemperatureF, + CureTimeMinutes = model.CureTimeMinutes, + Finish = NullIfWhiteSpace(model.Finish), + ColorFamilies = NullIfWhiteSpace(model.ColorFamilies), + RequiresClearCoat = model.RequiresClearCoat, + CoverageSqFtPerLb = model.CoverageSqFtPerLb, + TransferEfficiency = model.TransferEfficiency, + IsDiscontinued = model.IsDiscontinued, + IsUserContributed = model.IsUserContributed, + CreatedAt = now, + UpdatedAt = now + }; + + try + { + await _unitOfWork.PowderCatalog.AddAsync(entity); + await _unitOfWork.CompleteAsync(); + TempData["Success"] = $"Added powder catalog item \"{entity.ColorName}\" ({entity.Sku})."; + return RedirectToAction(nameof(Index)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating powder catalog item {VendorName} {Sku}", model.VendorName, model.Sku); + TempData["Error"] = "An error occurred while creating the powder catalog item."; + return View(model); + } + } + + public async Task Edit(int id) + { + var entity = await _unitOfWork.PowderCatalog.GetByIdAsync(id); + if (entity == null) + return NotFound(); + + return View(MapForm(entity)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(int id, PowderCatalogFormViewModel model) + { + if (id != model.Id) + return NotFound(); + + NormalizeModel(model); + + if (!ModelState.IsValid) + return View(model); + + var entity = await _unitOfWork.PowderCatalog.GetByIdAsync(id); + if (entity == null) + return NotFound(); + + if (await CatalogRecordExistsAsync(model.VendorName, model.Sku, id)) + { + ModelState.AddModelError(string.Empty, "A powder catalog item with this vendor and SKU already exists."); + return View(model); + } + + entity.VendorName = model.VendorName; + entity.Sku = model.Sku; + entity.ColorName = model.ColorName; + entity.Description = NullIfWhiteSpace(model.Description); + entity.UnitPrice = model.UnitPrice; + entity.ImageUrl = NullIfWhiteSpace(model.ImageUrl); + entity.SdsUrl = NullIfWhiteSpace(model.SdsUrl); + entity.TdsUrl = NullIfWhiteSpace(model.TdsUrl); + entity.ApplicationGuideUrl = NullIfWhiteSpace(model.ApplicationGuideUrl); + entity.ProductUrl = NullIfWhiteSpace(model.ProductUrl); + entity.CureTemperatureF = model.CureTemperatureF; + entity.CureTimeMinutes = model.CureTimeMinutes; + entity.Finish = NullIfWhiteSpace(model.Finish); + entity.ColorFamilies = NullIfWhiteSpace(model.ColorFamilies); + entity.RequiresClearCoat = model.RequiresClearCoat; + entity.CoverageSqFtPerLb = model.CoverageSqFtPerLb; + entity.TransferEfficiency = model.TransferEfficiency; + entity.IsDiscontinued = model.IsDiscontinued; + entity.IsUserContributed = model.IsUserContributed; + entity.UpdatedAt = DateTime.UtcNow; + + try + { + await _unitOfWork.PowderCatalog.UpdateAsync(entity); + await _unitOfWork.CompleteAsync(); + TempData["Success"] = $"Updated powder catalog item \"{entity.ColorName}\" ({entity.Sku})."; + return RedirectToAction(nameof(Index)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating powder catalog item {Id}", id); + TempData["Error"] = "An error occurred while updating the powder catalog item."; + return View(model); + } + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ToggleDiscontinued(int id) + { + var entity = await _unitOfWork.PowderCatalog.GetByIdAsync(id); + if (entity == null) + return NotFound(); + + entity.IsDiscontinued = !entity.IsDiscontinued; + entity.UpdatedAt = DateTime.UtcNow; + + try + { + await _unitOfWork.PowderCatalog.UpdateAsync(entity); + await _unitOfWork.CompleteAsync(); + TempData["Success"] = entity.IsDiscontinued + ? $"Marked \"{entity.ColorName}\" as discontinued." + : $"Reactivated \"{entity.ColorName}\"."; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error toggling discontinued status for powder catalog item {Id}", id); + TempData["Error"] = "An error occurred while updating the catalog item status."; + } + + return RedirectToAction(nameof(Index)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task AiLookup( + [FromForm] string? vendorName, + [FromForm] string? colorName, + [FromForm] string? sku) + { + if (string.IsNullOrWhiteSpace(vendorName) + && string.IsNullOrWhiteSpace(colorName) + && string.IsNullOrWhiteSpace(sku)) + { + return Json(new { success = false, errorMessage = "Enter a vendor, color name, or SKU first." }); + } + + var result = await _aiLookupService.LookupAsync(vendorName, colorName, null, sku); + if (result.Success) + await ApplyTdsCureFallbackAsync(result, colorName); + + return Json(result); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task AiAugmentFromUrl( + [FromForm] string? productUrl, + [FromForm] string? colorName) + { + if (string.IsNullOrWhiteSpace(productUrl)) + return Json(new { success = false, errorMessage = "No product URL provided." }); + + var result = await _aiLookupService.LookupByUrlAsync(productUrl, colorName); + if (result.Success) + await ApplyTdsCureFallbackAsync(result, colorName); + + return Json(result); } /// @@ -75,7 +368,7 @@ public class PowderCatalogController : Controller } else { - TempData["Success"] = $"Import complete — {result.Inserted:N0} inserted, {result.Updated:N0} updated, {result.Skipped:N0} skipped."; + TempData["Success"] = $"Import complete - {result.Inserted:N0} inserted, {result.Updated:N0} updated, {result.Skipped:N0} skipped."; } return RedirectToAction(nameof(Index)); @@ -126,7 +419,21 @@ public class PowderCatalogController : Controller return Json(results); } - // ── Private helpers ──────────────────────────────────────────────────────── + // Private helpers + + private async Task ApplyTdsCureFallbackAsync(InventoryAiLookupResult result, string? colorName) + { + if ((result.CureTemperatureF == null || result.CureTimeMinutes == null) + && !string.IsNullOrWhiteSpace(result.TdsUrl)) + { + var tds = await _aiLookupService.FetchTdsCureSpecsAsync(result.TdsUrl, colorName); + if (tds.Success) + { + result.CureTemperatureF ??= tds.CureTemperatureF; + result.CureTimeMinutes ??= tds.CureTimeMinutes; + } + } + } private async Task ImportJsonAsync(IFormFile file, string vendorName) { @@ -275,6 +582,184 @@ public class PowderCatalogController : Controller return price ?? 0m; } + + private static PowderCatalogStatsDto BuildStats(List list) + { + return new PowderCatalogStatsDto + { + TotalProducts = list.Count, + ActiveProducts = list.Count(p => !p.IsDiscontinued), + DiscontinuedProducts = list.Count(p => p.IsDiscontinued), + VendorCount = list.Select(p => p.VendorName).Distinct(StringComparer.OrdinalIgnoreCase).Count(), + UserContributedProducts = list.Count(p => p.IsUserContributed), + LastImportedAt = list.Any() ? list.Max(p => p.LastSyncedAt) : null + }; + } + + private static IEnumerable ApplySort(IEnumerable query, string sortColumn, string sortDirection) + { + var descending = sortDirection.Equals("desc", StringComparison.OrdinalIgnoreCase); + + return (sortColumn, descending) switch + { + ("Sku", false) => query.OrderBy(p => p.Sku).ThenBy(p => p.ColorName), + ("Sku", true) => query.OrderByDescending(p => p.Sku).ThenBy(p => p.ColorName), + ("ColorName", false) => query.OrderBy(p => p.ColorName).ThenBy(p => p.Sku), + ("ColorName", true) => query.OrderByDescending(p => p.ColorName).ThenBy(p => p.Sku), + ("UnitPrice", false) => query.OrderBy(p => p.UnitPrice).ThenBy(p => p.ColorName), + ("UnitPrice", true) => query.OrderByDescending(p => p.UnitPrice).ThenBy(p => p.ColorName), + ("Finish", false) => query.OrderBy(p => p.Finish).ThenBy(p => p.ColorName), + ("Finish", true) => query.OrderByDescending(p => p.Finish).ThenBy(p => p.ColorName), + ("UpdatedAt", false) => query.OrderBy(p => p.UpdatedAt ?? p.CreatedAt).ThenBy(p => p.ColorName), + ("UpdatedAt", true) => query.OrderByDescending(p => p.UpdatedAt ?? p.CreatedAt).ThenBy(p => p.ColorName), + ("LastSyncedAt", false) => query.OrderBy(p => p.LastSyncedAt ?? DateTime.MinValue).ThenBy(p => p.ColorName), + ("LastSyncedAt", true) => query.OrderByDescending(p => p.LastSyncedAt ?? DateTime.MinValue).ThenBy(p => p.ColorName), + ("VendorName", true) => query.OrderByDescending(p => p.VendorName).ThenBy(p => p.ColorName), + _ => query.OrderBy(p => p.VendorName).ThenBy(p => p.ColorName) + }; + } + + private static PowderCatalogListItemViewModel MapListItem(PowderCatalogItem item) + { + return new PowderCatalogListItemViewModel + { + Id = item.Id, + VendorName = item.VendorName, + Sku = item.Sku, + ColorName = item.ColorName, + Finish = item.Finish, + UnitPrice = item.UnitPrice, + IsDiscontinued = item.IsDiscontinued, + IsUserContributed = item.IsUserContributed, + HasImage = !string.IsNullOrWhiteSpace(item.ImageUrl), + HasCoreSpecs = HasCoreSpecs(item), + HasDocuments = HasDocuments(item), + CreatedAt = item.CreatedAt, + UpdatedAt = item.UpdatedAt, + LastSyncedAt = item.LastSyncedAt + }; + } + + private static PowderCatalogFormViewModel MapForm(PowderCatalogItem item) + { + return new PowderCatalogFormViewModel + { + Id = item.Id, + VendorName = item.VendorName, + Sku = item.Sku, + ColorName = item.ColorName, + Description = item.Description, + UnitPrice = item.UnitPrice, + ImageUrl = item.ImageUrl, + SdsUrl = item.SdsUrl, + TdsUrl = item.TdsUrl, + ApplicationGuideUrl = item.ApplicationGuideUrl, + ProductUrl = item.ProductUrl, + CureTemperatureF = item.CureTemperatureF, + CureTimeMinutes = item.CureTimeMinutes, + Finish = item.Finish, + ColorFamilies = item.ColorFamilies, + RequiresClearCoat = item.RequiresClearCoat, + CoverageSqFtPerLb = item.CoverageSqFtPerLb, + TransferEfficiency = GetEffectiveTransferEfficiency(item.TransferEfficiency), + IsDiscontinued = item.IsDiscontinued, + IsUserContributed = item.IsUserContributed, + CreatedAt = item.CreatedAt, + UpdatedAt = item.UpdatedAt, + LastSyncedAt = item.LastSyncedAt + }; + } + + private static bool HasCoreSpecs(PowderCatalogItem item) + { + return !string.IsNullOrWhiteSpace(item.Finish) + && item.CureTemperatureF.HasValue + && item.CureTimeMinutes.HasValue + && item.CoverageSqFtPerLb.HasValue + && GetEffectiveTransferEfficiency(item.TransferEfficiency).HasValue; + } + + private static bool HasDocuments(PowderCatalogItem item) + { + return !string.IsNullOrWhiteSpace(item.ProductUrl) + && !string.IsNullOrWhiteSpace(item.SdsUrl) + && !string.IsNullOrWhiteSpace(item.TdsUrl); + } + + private static bool IsCatalogReady(PowderCatalogItem item) + { + return HasCoreSpecs(item) + && HasDocuments(item) + && !string.IsNullOrWhiteSpace(item.ImageUrl); + } + + private static bool ContainsIgnoreCase(string? value, string searchTerm) + { + return !string.IsNullOrWhiteSpace(value) + && value.Contains(searchTerm, StringComparison.OrdinalIgnoreCase); + } + + private static string NormalizeFilter(string? value, string fallback, params string[] allowedValues) + { + if (string.IsNullOrWhiteSpace(value)) + return fallback; + + return allowedValues.Contains(value, StringComparer.OrdinalIgnoreCase) + ? value.ToLowerInvariant() + : fallback; + } + + private static string NormalizeSortColumn(string? sortColumn) + { + var allowed = new[] + { + "VendorName", "Sku", "ColorName", "Finish", "UnitPrice", "UpdatedAt", "LastSyncedAt" + }; + + return allowed.Contains(sortColumn, StringComparer.OrdinalIgnoreCase) + ? allowed.First(s => s.Equals(sortColumn, StringComparison.OrdinalIgnoreCase)) + : "VendorName"; + } + + private async Task CatalogRecordExistsAsync(string vendorName, string sku, int? excludeId = null) + { + var existing = await _unitOfWork.PowderCatalog.FindAsync(p => + p.VendorName.ToLower() == vendorName.ToLower() && + p.Sku.ToLower() == sku.ToLower()); + + return existing.Any(p => excludeId == null || p.Id != excludeId.Value); + } + + private static void NormalizeModel(PowderCatalogFormViewModel model) + { + model.VendorName = model.VendorName?.Trim() ?? string.Empty; + model.Sku = model.Sku?.Trim() ?? string.Empty; + model.ColorName = model.ColorName?.Trim() ?? string.Empty; + model.Description = TrimToNull(model.Description); + model.ImageUrl = TrimToNull(model.ImageUrl); + model.SdsUrl = TrimToNull(model.SdsUrl); + model.TdsUrl = TrimToNull(model.TdsUrl); + model.ApplicationGuideUrl = TrimToNull(model.ApplicationGuideUrl); + model.ProductUrl = TrimToNull(model.ProductUrl); + model.Finish = TrimToNull(model.Finish); + model.ColorFamilies = TrimToNull(model.ColorFamilies); + model.TransferEfficiency ??= DefaultTransferEfficiency; + } + + private static string? TrimToNull(string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } + + private static string? NullIfWhiteSpace(string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value; + } + + private static decimal? GetEffectiveTransferEfficiency(decimal? transferEfficiency) + { + return transferEfficiency ?? DefaultTransferEfficiency; + } } /// Extension helpers for reading nullable strings from JsonElement. diff --git a/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogFormViewModel.cs b/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogFormViewModel.cs new file mode 100644 index 0000000..d25165d --- /dev/null +++ b/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogFormViewModel.cs @@ -0,0 +1,85 @@ +using System.ComponentModel.DataAnnotations; + +namespace PowderCoating.Web.ViewModels.PowderCatalog; + +public class PowderCatalogFormViewModel +{ + public int Id { get; set; } + + [Required] + [StringLength(120)] + public string VendorName { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + [Display(Name = "SKU")] + public string Sku { get; set; } = string.Empty; + + [Required] + [StringLength(160)] + [Display(Name = "Color Name")] + public string ColorName { get; set; } = string.Empty; + + [StringLength(4000)] + public string? Description { get; set; } + + [Range(0, 999999)] + [Display(Name = "Unit Price")] + public decimal UnitPrice { get; set; } + + [Url] + [Display(Name = "Image URL")] + public string? ImageUrl { get; set; } + + [Url] + [Display(Name = "SDS URL")] + public string? SdsUrl { get; set; } + + [Url] + [Display(Name = "TDS URL")] + public string? TdsUrl { get; set; } + + [Url] + [Display(Name = "Application Guide URL")] + public string? ApplicationGuideUrl { get; set; } + + [Url] + [Display(Name = "Product URL")] + public string? ProductUrl { get; set; } + + [Range(0, 1000)] + [Display(Name = "Cure Temperature (F)")] + public decimal? CureTemperatureF { get; set; } + + [Range(0, 1000)] + [Display(Name = "Cure Time (Minutes)")] + public int? CureTimeMinutes { get; set; } + + [StringLength(100)] + public string? Finish { get; set; } + + [StringLength(300)] + [Display(Name = "Color Families")] + public string? ColorFamilies { get; set; } + + [Display(Name = "Requires Clear Coat")] + public bool? RequiresClearCoat { get; set; } + + [Range(0, 10000)] + [Display(Name = "Coverage (Sq Ft / Lb)")] + public decimal? CoverageSqFtPerLb { get; set; } + + [Range(0, 100)] + [Display(Name = "Transfer Efficiency (%)")] + public decimal? TransferEfficiency { get; set; } + + [Display(Name = "Discontinued")] + public bool IsDiscontinued { get; set; } + + [Display(Name = "User Contributed")] + public bool IsUserContributed { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public DateTime? LastSyncedAt { get; set; } +} diff --git a/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogIndexViewModel.cs b/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogIndexViewModel.cs new file mode 100644 index 0000000..175a67e --- /dev/null +++ b/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogIndexViewModel.cs @@ -0,0 +1,37 @@ +using PowderCoating.Application.DTOs.Common; +using PowderCoating.Application.DTOs.Inventory; + +namespace PowderCoating.Web.ViewModels.PowderCatalog; + +public class PowderCatalogIndexViewModel +{ + public PowderCatalogStatsDto Stats { get; set; } = new(); + public PagedResult Catalog { get; set; } = new(); + public IReadOnlyList Vendors { get; set; } = Array.Empty(); + + public string? SearchTerm { get; set; } + public string? VendorName { get; set; } + public string Status { get; set; } = "all"; + public string Source { get; set; } = "all"; + public string Completeness { get; set; } = "all"; + public string SortColumn { get; set; } = "VendorName"; + public string SortDirection { get; set; } = "asc"; +} + +public class PowderCatalogListItemViewModel +{ + public int Id { get; set; } + public string VendorName { get; set; } = string.Empty; + public string Sku { get; set; } = string.Empty; + public string ColorName { get; set; } = string.Empty; + public string? Finish { get; set; } + public decimal UnitPrice { get; set; } + public bool IsDiscontinued { get; set; } + public bool IsUserContributed { get; set; } + public bool HasImage { get; set; } + public bool HasCoreSpecs { get; set; } + public bool HasDocuments { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public DateTime? LastSyncedAt { get; set; } +} diff --git a/src/PowderCoating.Web/Views/PowderCatalog/Create.cshtml b/src/PowderCoating.Web/Views/PowderCatalog/Create.cshtml new file mode 100644 index 0000000..dc7f5db --- /dev/null +++ b/src/PowderCoating.Web/Views/PowderCatalog/Create.cshtml @@ -0,0 +1,42 @@ +@model PowderCoating.Web.ViewModels.PowderCatalog.PowderCatalogFormViewModel + +@{ + ViewData["Title"] = "Add Powder Catalog Item"; + ViewData["PageIcon"] = "bi-plus-circle"; +} + +
+
+ + + +
+

Add Powder Catalog Item

+ Create a platform-level powder record for inventory autofill and documentation links. +
+
+ +
+
+
+
+
+ @Html.AntiForgeryToken() + + +
+ + Cancel +
+ +
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/src/PowderCoating.Web/Views/PowderCatalog/Edit.cshtml b/src/PowderCoating.Web/Views/PowderCatalog/Edit.cshtml new file mode 100644 index 0000000..36535ca --- /dev/null +++ b/src/PowderCoating.Web/Views/PowderCatalog/Edit.cshtml @@ -0,0 +1,50 @@ +@model PowderCoating.Web.ViewModels.PowderCatalog.PowderCatalogFormViewModel + +@{ + ViewData["Title"] = "Edit Powder Catalog Item"; + ViewData["PageIcon"] = "bi-pencil-square"; +} + +
+
+ + + +
+

Edit Powder Catalog Item

+ @Model.VendorName - @Model.Sku +
+
+ +
+
+
+
+
+ @Html.AntiForgeryToken() + + + + + @{ + ViewData["EnableAiLookup"] = true; + } + + +
+ + Cancel +
+ +
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} + +} diff --git a/src/PowderCoating.Web/Views/PowderCatalog/Index.cshtml b/src/PowderCoating.Web/Views/PowderCatalog/Index.cshtml index 086b52c..37f0115 100644 --- a/src/PowderCoating.Web/Views/PowderCatalog/Index.cshtml +++ b/src/PowderCoating.Web/Views/PowderCatalog/Index.cshtml @@ -1,12 +1,94 @@ -@model PowderCoating.Application.DTOs.Inventory.PowderCatalogStatsDto +@model PowderCoating.Web.ViewModels.PowderCatalog.PowderCatalogIndexViewModel @{ ViewData["Title"] = "Powder Catalog"; ViewData["PageIcon"] = "bi-palette2"; - Layout = "_Layout"; + ViewData["PageHelpTitle"] = "Powder Catalog"; + ViewData["PageHelpContent"] = "Manage the platform-level powder master list used to auto-fill inventory. Filter for contributed records, missing specs, or discontinued powders, then edit entries directly from here."; } -
+@functions { + string SortLink(string column) + { + var route = new Dictionary + { + ["searchTerm"] = Model.SearchTerm, + ["vendorName"] = Model.VendorName, + ["status"] = Model.Status, + ["source"] = Model.Source, + ["completeness"] = Model.Completeness, + ["pageNumber"] = 1, + ["pageSize"] = Model.Catalog.PageSize, + ["sortColumn"] = column, + ["sortDirection"] = Model.SortColumn == column && Model.SortDirection == "asc" ? "desc" : "asc" + }; + return Url.Action("Index", route) ?? "#"; + } + + string SortIcon(string column) + { + if (!string.Equals(Model.SortColumn, column, StringComparison.OrdinalIgnoreCase)) + return "bi-arrow-down-up"; + + return Model.SortDirection == "asc" ? "bi-arrow-up" : "bi-arrow-down"; + } +} + +@section Styles { + +} + +
@if (TempData["Success"] != null) { } - -
-
-
+
+
+

Powder Catalog

+
Platform-level lookup library for inventory autofill, SDS/TDS links, and curing specs.
+
+ +
+ +
+
+
-
- +
+
-
@Model.TotalProducts.ToString("N0")
-
Total Products
+
@Model.Stats.TotalProducts.ToString("N0")
+
Total Products
-
-
+
+
-
- +
+
-
@Model.ActiveProducts.ToString("N0")
-
Active
+
@Model.Stats.ActiveProducts.ToString("N0")
+
Active
-
-
+
+
-
- +
+
-
@Model.DiscontinuedProducts.ToString("N0")
-
Discontinued
+
@Model.Stats.DiscontinuedProducts.ToString("N0")
+
Discontinued
-
-
+
+
-
- +
+
-
@Model.VendorCount
-
- @(Model.VendorCount == 1 ? "Vendor" : "Vendors") - @if (Model.LastImportedAt.HasValue) - { -
Last sync @Model.LastImportedAt.Value.ToString("MMM d, yyyy") - } -
+
@Model.Stats.VendorCount
+
Vendors
-
-
+
+
-
- +
+
-
@Model.UserContributedProducts.ToString("N0")
-
Tenant Contributed
+
@Model.Stats.UserContributedProducts.ToString("N0")
+
Contributed
+
+
+
+
+
+
+
+
+ +
+
+
@(Model.Stats.LastImportedAt?.ToString("MMM d, yyyy") ?? "Never")
+
Last Sync
@@ -98,50 +198,271 @@
- -
-
+
+
+
+
+
+
Manage Catalog Records
+
Search, filter, and edit the powders your inventory lookup depends on.
+
+
@Model.Catalog.TotalCount.ToString("N0") filtered result@(Model.Catalog.TotalCount == 1 ? "" : "s")
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + Clear +
+
+ +
+ + + + + + + + + + + + + + + + @if (!Model.Catalog.Items.Any()) + { + + + + } + else + { + @foreach (var item in Model.Catalog.Items) + { + + + + + + + + + + + + } + } + +
Vendor SKU Powder Finish Price StatusQualitySynced Actions
+ + No powder catalog records matched your filters. +
@item.VendorName@item.Sku +
@item.ColorName
+
Updated @(item.UpdatedAt?.ToString("MMM d, yyyy") ?? item.CreatedAt.ToString("MMM d, yyyy"))
+
@(string.IsNullOrWhiteSpace(item.Finish) ? "-" : item.Finish)@(item.UnitPrice > 0 ? item.UnitPrice.ToString("C") : "-") +
+ @if (item.IsDiscontinued) + { + Discontinued + } + else + { + Active + } + @if (item.IsUserContributed) + { + Contributed + } + else + { + Curated + } +
+
+
+ Specs + Docs + Image +
+
@(item.LastSyncedAt?.ToString("MMM d, yyyy") ?? "-") +
+ + + +
+ @Html.AntiForgeryToken() + +
+
+
+
+ +
+ @if (!Model.Catalog.Items.Any()) + { +
No powder catalog records matched your filters.
+ } + else + { +
+ @foreach (var item in Model.Catalog.Items) + { +
+
+
+ +
+
+
@item.ColorName
+ @item.VendorName - @item.Sku +
+
+
+
+ Finish + @(string.IsNullOrWhiteSpace(item.Finish) ? "-" : item.Finish) +
+
+ Price + @(item.UnitPrice > 0 ? item.UnitPrice.ToString("C") : "-") +
+
+ Status + + @if (item.IsDiscontinued) + { + Discontinued + } + else + { + Active + } + @if (item.IsUserContributed) + { + Contributed + } + +
+
+ Quality + @(item.HasCoreSpecs ? "Specs" : "Missing Specs"), @(item.HasDocuments ? "Docs" : "Missing Docs"), @(item.HasImage ? "Image" : "Missing Image") +
+
+ +
+ } +
+ } +
+
+ + @if (Model.Catalog.TotalPages > 1) + { + + } +
+
+ +
+
Import Catalog Data

- Upload a Prismatic Powders scrape JSON file (the prismatic_powders.json format with - a top-level results array). Existing SKUs are updated in-place; new ones are inserted. - Discontinued products remain in the catalog flagged as IsDiscontinued. + Upload a Prismatic-style scrape JSON file with a top-level results array. + Existing rows are updated by vendor + SKU and new powders are inserted automatically.

-
+
-
Must match exactly — used as the upsert key alongside SKU.
+
Used as part of the upsert key alongside SKU.
-
Max 50 MB. Must be the scraped format with results[] array.
+
Max 50 MB. Must contain the scraped results[] payload.
-
-
- -
-
+
-
How It Works
+
Management Notes
-
    -
  • Platform-level: One shared catalog, no per-tenant copies.
  • -
  • Catalog-first lookup: When a tenant adds inventory, the form searches here before calling the AI API.
  • -
  • Auto-fill: Selecting a result fills color name, manufacturer, part number, unit cost, SDS/TDS links, and product image.
  • -
  • Discontinued: Flagged IsDiscontinued = true — never hidden, always available for historical lookups.
  • -
  • Phase 2: Monthly price sync + push to tenant inventory items.
  • +
      +
    • Contributed powders are auto-added from tenant lookups when a catalog match does not exist.
    • +
    • Specs matter because inventory autofill uses finish, cure data, coverage, and transfer efficiency when available.
    • +
    • Documents matter because the inventory form surfaces product, SDS, and TDS links directly from this catalog.
    • +
    • Discontinued powders stay searchable so historical inventory and customer references still resolve.
@@ -149,10 +470,13 @@
- +@section Scripts { + +} diff --git a/src/PowderCoating.Web/Views/PowderCatalog/_Form.cshtml b/src/PowderCoating.Web/Views/PowderCatalog/_Form.cshtml new file mode 100644 index 0000000..53cc62f --- /dev/null +++ b/src/PowderCoating.Web/Views/PowderCatalog/_Form.cshtml @@ -0,0 +1,184 @@ +@model PowderCoating.Web.ViewModels.PowderCatalog.PowderCatalogFormViewModel + +@{ + var enableAiLookup = ViewData["EnableAiLookup"] as bool? == true; +} + +@if (enableAiLookup) +{ +
+
+
+
+ AI Lookup +
+ + +
+
+ Search the web for missing specs, cure data, and SDS/TDS links. Existing values are left alone unless the field is blank. +
+
+
+
+} + +
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + + +
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+ +
+ + + + +
+ +
+
+ + + +
+ +
+ +
+ + + + +
+ +
+
+ +
+ + + + +
+ +
+
+ +
+ + + + +
+ +
+ +
+ + + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +@if (Model.Id > 0) +{ +
+
+
+
Created
+
@Model.CreatedAt.ToString("MMM d, yyyy h:mm tt") UTC
+
+
+
Updated
+
@(Model.UpdatedAt?.ToString("MMM d, yyyy h:mm tt") ?? "Never")@(Model.UpdatedAt.HasValue ? " UTC" : string.Empty)
+
+
+
Last Synced
+
@(Model.LastSyncedAt?.ToString("MMM d, yyyy h:mm tt") ?? "Never")@(Model.LastSyncedAt.HasValue ? " UTC" : string.Empty)
+
+
+} diff --git a/src/PowderCoating.Web/wwwroot/js/powder-catalog-ai-lookup.js b/src/PowderCoating.Web/wwwroot/js/powder-catalog-ai-lookup.js new file mode 100644 index 0000000..616f49f --- /dev/null +++ b/src/PowderCoating.Web/wwwroot/js/powder-catalog-ai-lookup.js @@ -0,0 +1,175 @@ +(function () { + 'use strict'; + + const lookupBtn = document.getElementById('powder-ai-lookup-btn'); + const urlBtn = document.getElementById('powder-ai-url-btn'); + const statusEl = document.getElementById('ai-lookup-status'); + + if (!lookupBtn || !statusEl) return; + + const endpoints = { + lookup: '/PowderCatalog/AiLookup', + byUrl: '/PowderCatalog/AiAugmentFromUrl' + }; + + lookupBtn.addEventListener('click', async function () { + const vendorName = getValue('field-vendorname'); + const colorName = getValue('field-colorname'); + const sku = getValue('field-sku'); + + if (!vendorName && !colorName && !sku) { + showStatus('warning', 'Enter a vendor, color name, or SKU first.'); + return; + } + + await runLookup(endpoints.lookup, { + vendorName: vendorName, + colorName: colorName, + sku: sku + }, 'Searching the web for missing powder specs and documents...'); + }); + + urlBtn?.addEventListener('click', async function () { + const productUrl = getValue('field-producturl'); + const colorName = getValue('field-colorname'); + + if (!productUrl) { + showStatus('warning', 'Add a product URL first, then try AI From URL.'); + return; + } + + await runLookup(endpoints.byUrl, { + productUrl: productUrl, + colorName: colorName + }, 'Reading the product page for missing specs and document links...'); + }); + + async function runLookup(url, payload, loadingMessage) { + setButtonsDisabled(true); + showStatus('info', '' + loadingMessage); + + try { + const formData = new FormData(); + Object.entries(payload).forEach(([key, value]) => formData.append(key, value || '')); + + const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value; + if (token) formData.append('__RequestVerificationToken', token); + + const response = await fetch(url, { method: 'POST', body: formData }); + const data = await response.json(); + + if (!response.ok || !data.success) { + showStatus('danger', 'AI lookup failed: ' + (data.errorMessage || 'Unknown error.')); + return; + } + + const filled = applyLookupResult(data); + const reasoning = data.reasoning ? `
${escapeHtml(data.reasoning)}
` : ''; + + if (filled.length > 0) { + showStatus('success', `Filled missing fields: ${filled.join(', ')}.${reasoning}`); + } else { + showStatus('warning', 'AI found the product, but there were no empty specs or docs to fill.' + reasoning); + } + } catch (error) { + showStatus('danger', 'AI lookup request failed: ' + error.message); + } finally { + setButtonsDisabled(false); + } + } + + function applyLookupResult(data) { + const filled = []; + + fillIfEmpty('field-vendorname', data.manufacturer || data.vendorName, 'Vendor', filled); + fillIfEmpty('field-sku', data.manufacturerPartNumber, 'SKU', filled); + fillIfEmpty('field-colorname', data.colorName, 'Color Name', filled); + fillIfEmpty('field-description', data.description, 'Description', filled, true); + fillIfEmpty('field-finish', data.finish, 'Finish', filled); + fillIfEmpty('field-curetemp', data.cureTemperatureF, 'Cure Temp', filled); + fillIfEmpty('field-curetime', data.cureTimeMinutes, 'Cure Time', filled); + fillIfEmpty('field-coverage', data.coverageSqFtPerLb, 'Coverage', filled); + fillIfEmpty('field-transfer', data.transferEfficiency, 'Transfer Efficiency', filled); + fillIfEmpty('field-producturl', data.specPageUrl, 'Product URL', filled); + fillIfEmpty('field-imageurl', data.imageUrl, 'Image URL', filled); + fillIfEmpty('field-sdsurl', data.sdsUrl, 'SDS URL', filled); + fillIfEmpty('field-tdsurl', data.tdsUrl, 'TDS URL', filled); + fillIfEmpty('field-colorfamilies', data.colorFamilies, 'Color Families', filled); + + if (data.unitCostPerLb !== null && data.unitCostPerLb !== undefined) { + const unitPrice = document.getElementById('field-unitprice'); + const current = unitPrice ? parseFloat(unitPrice.value) || 0 : 0; + if (unitPrice && current === 0) { + unitPrice.value = String(data.unitCostPerLb).trim(); + filled.push('Unit Price'); + } + } + + const clearCoat = document.getElementById('field-clearcoat'); + if (clearCoat && isEmptyValue(clearCoat.value) && data.requiresClearCoat !== null && data.requiresClearCoat !== undefined) { + clearCoat.value = data.requiresClearCoat ? 'true' : 'false'; + filled.push('Requires Clear Coat'); + } + + syncLinkButton('field-producturl', 'field-producturl-link'); + syncLinkButton('field-sdsurl', 'field-sdsurl-link'); + syncLinkButton('field-tdsurl', 'field-tdsurl-link'); + syncLinkButton('field-applicationguideurl', 'field-applicationguideurl-link'); + + return filled; + } + + function fillIfEmpty(id, value, label, filled, isTextarea) { + const el = document.getElementById(id); + if (!el) return; + + const normalized = value !== null && value !== undefined ? String(value).trim() : ''; + const current = isTextarea ? (el.value || '').trim() : (el.value || '').trim(); + if (!normalized || current) return; + + el.value = normalized; + filled.push(label); + } + + function getValue(id) { + return document.getElementById(id)?.value?.trim() || ''; + } + + function syncLinkButton(inputId, linkId) { + const input = document.getElementById(inputId); + const link = document.getElementById(linkId); + if (!input || !link) return; + + if (input.value && input.value.trim()) { + link.href = input.value.trim(); + link.classList.remove('d-none'); + } else { + link.href = '#'; + link.classList.add('d-none'); + } + } + + function setButtonsDisabled(disabled) { + lookupBtn.disabled = disabled; + if (urlBtn) urlBtn.disabled = disabled; + } + + function isEmptyValue(value) { + return value === null || value === undefined || String(value).trim() === ''; + } + + function showStatus(type, message) { + statusEl.className = `alert alert-${type} py-2 small mb-3`; + statusEl.innerHTML = message; + statusEl.classList.remove('d-none'); + } + + function escapeHtml(value) { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + } +})(); From f383339465f59a1b5d28399a37df6325d89b1d62 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 08:46:41 -0400 Subject: [PATCH 02/15] Store powder specific gravity and fix coverage math --- .../DTOs/Inventory/InventoryDtos.cs | 5 + .../DTOs/Inventory/PowderCatalogDtos.cs | 1 + .../Entities/InventoryItem.cs | 1 + .../Entities/PowderCatalogItem.cs | 3 + ...ityToPowderCatalogAndInventory.Designer.cs | 9525 +++++++++++++++++ ...cificGravityToPowderCatalogAndInventory.cs | 81 + .../ApplicationDbContextModelSnapshot.cs | 12 +- .../Services/InventoryAiLookupService.cs | 5 +- .../Controllers/InventoryController.cs | 5 + .../Controllers/PowderCatalogController.cs | 4 + .../PowderCatalogFormViewModel.cs | 4 + .../Views/Inventory/Create.cshtml | 24 +- .../Views/Inventory/Edit.cshtml | 24 +- .../_InventoryColorFamilyScripts.cshtml | 3 +- .../Views/PowderCatalog/_Form.cshtml | 5 + .../wwwroot/js/inventory-catalog-lookup.js | 2 + .../wwwroot/js/inventory-label-scan.js | 1 + .../wwwroot/js/powder-catalog-ai-lookup.js | 1 + 18 files changed, 9690 insertions(+), 16 deletions(-) create mode 100644 src/PowderCoating.Infrastructure/Migrations/20260506123541_AddSpecificGravityToPowderCatalogAndInventory.Designer.cs create mode 100644 src/PowderCoating.Infrastructure/Migrations/20260506123541_AddSpecificGravityToPowderCatalogAndInventory.cs diff --git a/src/PowderCoating.Application/DTOs/Inventory/InventoryDtos.cs b/src/PowderCoating.Application/DTOs/Inventory/InventoryDtos.cs index 846703d..624d1ec 100644 --- a/src/PowderCoating.Application/DTOs/Inventory/InventoryDtos.cs +++ b/src/PowderCoating.Application/DTOs/Inventory/InventoryDtos.cs @@ -17,6 +17,7 @@ public class InventoryItemDto public string? Manufacturer { get; set; } public string? ManufacturerPartNumber { get; set; } public decimal? CoverageSqFtPerLb { get; set; } + public decimal? SpecificGravity { get; set; } public decimal? TransferEfficiency { get; set; } public int? CureTemperatureF { get; set; } public int? CureTimeMinutes { get; set; } @@ -125,6 +126,10 @@ public class CreateInventoryItemDto [Display(Name = "Coverage (Sq Ft/Lb)")] public decimal? CoverageSqFtPerLb { get; set; } + [Range(0, 100, ErrorMessage = "Specific gravity must be between 0 and 100")] + [Display(Name = "Specific Gravity")] + public decimal? SpecificGravity { get; set; } + [Range(0, 100, ErrorMessage = "Transfer efficiency must be between 0 and 100%")] [Display(Name = "Transfer Efficiency (%)")] public decimal? TransferEfficiency { get; set; } diff --git a/src/PowderCoating.Application/DTOs/Inventory/PowderCatalogDtos.cs b/src/PowderCoating.Application/DTOs/Inventory/PowderCatalogDtos.cs index f0b8994..03e5480 100644 --- a/src/PowderCoating.Application/DTOs/Inventory/PowderCatalogDtos.cs +++ b/src/PowderCoating.Application/DTOs/Inventory/PowderCatalogDtos.cs @@ -23,6 +23,7 @@ public class PowderCatalogLookupResult public string? ColorFamilies { get; set; } public bool? RequiresClearCoat { get; set; } public decimal? CoverageSqFtPerLb { get; set; } + public decimal? SpecificGravity { get; set; } public decimal? TransferEfficiency { get; set; } } diff --git a/src/PowderCoating.Core/Entities/InventoryItem.cs b/src/PowderCoating.Core/Entities/InventoryItem.cs index 40050e9..6f1f5e9 100644 --- a/src/PowderCoating.Core/Entities/InventoryItem.cs +++ b/src/PowderCoating.Core/Entities/InventoryItem.cs @@ -20,6 +20,7 @@ public class InventoryItem : BaseEntity public string? Manufacturer { get; set; } public string? ManufacturerPartNumber { get; set; } public decimal? CoverageSqFtPerLb { get; set; } // Square feet coverage per pound (default 30) + public decimal? SpecificGravity { get; set; } // Powder specific gravity from the technical data sheet public decimal? TransferEfficiency { get; set; } // Percentage of powder that sticks (default 65%) public decimal? CureTemperatureF { get; set; } // Required cure temperature in °F (recommended for oven scheduling) public int? CureTimeMinutes { get; set; } // Required hold time at cure temperature diff --git a/src/PowderCoating.Core/Entities/PowderCatalogItem.cs b/src/PowderCoating.Core/Entities/PowderCatalogItem.cs index faa04ce..22dab0c 100644 --- a/src/PowderCoating.Core/Entities/PowderCatalogItem.cs +++ b/src/PowderCoating.Core/Entities/PowderCatalogItem.cs @@ -52,6 +52,9 @@ public class PowderCatalogItem /// Theoretical coverage in sq ft per pound. Typical 80–120. public decimal? CoverageSqFtPerLb { get; set; } + /// Specific gravity from the TDS. Used to derive theoretical coverage when needed. + public decimal? SpecificGravity { get; set; } + /// Powder transfer efficiency percentage. Typical 60–75%. public decimal? TransferEfficiency { get; set; } diff --git a/src/PowderCoating.Infrastructure/Migrations/20260506123541_AddSpecificGravityToPowderCatalogAndInventory.Designer.cs b/src/PowderCoating.Infrastructure/Migrations/20260506123541_AddSpecificGravityToPowderCatalogAndInventory.Designer.cs new file mode 100644 index 0000000..c11fa9e --- /dev/null +++ b/src/PowderCoating.Infrastructure/Migrations/20260506123541_AddSpecificGravityToPowderCatalogAndInventory.Designer.cs @@ -0,0 +1,9525 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PowderCoating.Infrastructure.Data; + +#nullable disable + +namespace PowderCoating.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260506123541_AddSpecificGravityToPowderCatalogAndInventory")] + partial class AddSpecificGravityToPowderCatalogAndInventory + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("nvarchar(max)"); + + b.Property("Xml") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AccountSubType") + .HasColumnType("int"); + + b.Property("AccountType") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OpeningBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("OpeningBalanceDate") + .HasColumnType("datetime2"); + + b.Property("ParentAccountId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ParentAccountId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AiItemPrediction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AiTags") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConversationRounds") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PredictedComplexity") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PredictedMinutes") + .HasColumnType("int"); + + b.Property("PredictedSurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("PredictedUnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Reasoning") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UserOverrodeEstimate") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("AiItemPredictions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AiUsageLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CalledAt") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Feature") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InputLength") + .HasColumnType("int"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId", "CalledAt") + .HasDatabaseName("IX_AiUsageLogs_CompanyId_CalledAt"); + + b.ToTable("AiUsageLogs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Announcement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedByUserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDismissible") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartsAt") + .HasColumnType("datetime2"); + + b.Property("Target") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TargetCompanyId") + .HasColumnType("int"); + + b.Property("TargetPlan") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Announcements"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AnnouncementDismissal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnnouncementId") + .HasColumnType("int"); + + b.Property("DismissedAt") + .HasColumnType("datetime2"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("AnnouncementId", "UserId") + .IsUnique(); + + b.ToTable("AnnouncementDismissals"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("BanReason") + .HasColumnType("nvarchar(max)"); + + b.Property("BannedAt") + .HasColumnType("datetime2"); + + b.Property("BannedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("CanApproveQuotes") + .HasColumnType("bit"); + + b.Property("CanCreateQuotes") + .HasColumnType("bit"); + + b.Property("CanManageCalendar") + .HasColumnType("bit"); + + b.Property("CanManageCustomers") + .HasColumnType("bit"); + + b.Property("CanManageEquipment") + .HasColumnType("bit"); + + b.Property("CanManageInventory") + .HasColumnType("bit"); + + b.Property("CanManageInvoices") + .HasColumnType("bit"); + + b.Property("CanManageJobs") + .HasColumnType("bit"); + + b.Property("CanManageMaintenance") + .HasColumnType("bit"); + + b.Property("CanManageProducts") + .HasColumnType("bit"); + + b.Property("CanManageVendors") + .HasColumnType("bit"); + + b.Property("CanViewCalendar") + .HasColumnType("bit"); + + b.Property("CanViewProducts") + .HasColumnType("bit"); + + b.Property("CanViewReports") + .HasColumnType("bit"); + + b.Property("CanViewShopFloor") + .HasColumnType("bit"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyRole") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DashboardLayout") + .HasColumnType("int"); + + b.Property("DateFormat") + .HasColumnType("nvarchar(max)"); + + b.Property("Department") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("EmployeeNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("HireDate") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsBanned") + .HasColumnType("bit"); + + b.Property("LastLoginDate") + .HasColumnType("datetime2"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PasskeyPromptDismissed") + .HasColumnType("bit"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("Position") + .HasColumnType("nvarchar(max)"); + + b.Property("ProfilePictureFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("SidebarColor") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("TerminationDate") + .HasColumnType("datetime2"); + + b.Property("Theme") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeZone") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Appointment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualEndTime") + .HasColumnType("datetime2"); + + b.Property("ActualStartTime") + .HasColumnType("datetime2"); + + b.Property("AppointmentNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AppointmentStatusId") + .HasColumnType("int"); + + b.Property("AppointmentTypeId") + .HasColumnType("int"); + + b.Property("AssignedUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsAllDay") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsReminderEnabled") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Location") + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ReminderMinutesBefore") + .HasColumnType("int"); + + b.Property("ScheduledEndTime") + .HasColumnType("datetime2"); + + b.Property("ScheduledStartTime") + .HasColumnType("datetime2"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AppointmentStatusId"); + + b.HasIndex("AppointmentTypeId"); + + b.HasIndex("AssignedUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("JobId"); + + b.HasIndex("ScheduledStartTime"); + + b.HasIndex("CompanyId", "AppointmentStatusId") + .HasDatabaseName("IX_Appointments_CompanyId_AppointmentStatusId"); + + b.HasIndex("CompanyId", "ScheduledStartTime") + .HasDatabaseName("IX_Appointments_CompanyId_ScheduledStartTime"); + + b.ToTable("Appointments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentStatusLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("IsTerminalStatus") + .HasColumnType("bit"); + + b.Property("StatusCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AppointmentStatusLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentTypeLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("RequiresJobLink") + .HasColumnType("bit"); + + b.Property("TypeCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AppointmentTypeLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("EntityDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("EntityId") + .HasColumnType("nvarchar(450)"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("NewValues") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValues") + .HasColumnType("nvarchar(max)"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId", "Timestamp"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BannedIp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAt") + .HasColumnType("datetime2"); + + b.Property("BannedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Reason") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("BannedIps"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Bill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("APAccountId") + .HasColumnType("int"); + + b.Property("AmountPaid") + .HasColumnType("decimal(18,2)"); + + b.Property("BillDate") + .HasColumnType("datetime2"); + + b.Property("BillNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Memo") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiptFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Terms") + .HasColumnType("nvarchar(max)"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("VendorInvoiceNumber") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("APAccountId"); + + b.HasIndex("DueDate"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.HasIndex("CompanyId", "Status"); + + b.ToTable("Bills"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BillLineItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("BillId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("BillId"); + + b.HasIndex("JobId"); + + b.ToTable("BillLineItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BillPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("BankAccountId") + .HasColumnType("int"); + + b.Property("BillId") + .HasColumnType("int"); + + b.Property("CheckNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Memo") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentDate") + .HasColumnType("datetime2"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("PaymentNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BankAccountId"); + + b.HasIndex("BillId"); + + b.HasIndex("VendorId"); + + b.ToTable("BillPayments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BugReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("ResolutionNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedAt") + .HasColumnType("datetime2"); + + b.Property("ResolvedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedByUserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SubmittedByUserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("BugReports"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BugReportAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BlobPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BugReportId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BugReportId"); + + b.ToTable("BugReportAttachments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsMerchandise") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ParentCategoryId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("CatalogCategories"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApproximateArea") + .HasColumnType("decimal(18,2)"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("CogsAccountId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultEstimatedMinutes") + .HasColumnType("int"); + + b.Property("DefaultPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("DefaultRequiresMasking") + .HasColumnType("bit"); + + b.Property("DefaultRequiresSandblasting") + .HasColumnType("bit"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("ImagePath") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsMerchandise") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RevenueAccountId") + .HasColumnType("int"); + + b.Property("SKU") + .HasColumnType("nvarchar(max)"); + + b.Property("ThumbnailPath") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("CogsAccountId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("RevenueAccountId"); + + b.ToTable("CatalogItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogPriceCheckReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ItemsChecked") + .HasColumnType("int"); + + b.Property("OperatingCostsSummary") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RunAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CatalogPriceCheckReports"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountingOverride") + .HasColumnType("bit"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("AiCatalogPriceCheckEnabled") + .HasColumnType("bit"); + + b.Property("AiInventoryAssistEnabled") + .HasColumnType("bit"); + + b.Property("AiPhotoQuotesEnabled") + .HasColumnType("bit"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyCode") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsAnnualBilling") + .HasColumnType("bit"); + + b.Property("IsComped") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LogoContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("LogoData") + .HasColumnType("varbinary(max)"); + + b.Property("LogoFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("MarketingEmailOptOut") + .HasColumnType("bit"); + + b.Property("MarketingUnsubscribeToken") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MaxActiveJobsOverride") + .HasColumnType("int"); + + b.Property("MaxAiPhotoQuotesPerMonthOverride") + .HasColumnType("int"); + + b.Property("MaxCatalogItemsOverride") + .HasColumnType("int"); + + b.Property("MaxCustomersOverride") + .HasColumnType("int"); + + b.Property("MaxJobPhotosOverride") + .HasColumnType("int"); + + b.Property("MaxQuotePhotosOverride") + .HasColumnType("int"); + + b.Property("MaxQuotesOverride") + .HasColumnType("int"); + + b.Property("MaxUsersOverride") + .HasColumnType("int"); + + b.Property("OnlinePaymentSurchargeType") + .HasColumnType("int"); + + b.Property("OnlinePaymentSurchargeValue") + .HasColumnType("decimal(18,2)"); + + b.Property("OnlinePaymentsOverride") + .HasColumnType("bit"); + + b.Property("OnlineSurchargeAcknowledged") + .HasColumnType("bit"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryContactEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryContactName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SmsDisabledByAdmin") + .HasColumnType("bit"); + + b.Property("SmsEnabled") + .HasColumnType("bit"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeAccountId") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeConnectStatus") + .HasColumnType("int"); + + b.Property("StripeCustomerId") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeSubscriptionId") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionEndDate") + .HasColumnType("datetime2"); + + b.Property("SubscriptionNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionPlan") + .HasColumnType("int"); + + b.Property("SubscriptionStartDate") + .HasColumnType("datetime2"); + + b.Property("SubscriptionStatus") + .HasColumnType("int"); + + b.Property("TimeZone") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyCode") + .IsUnique() + .HasFilter("[CompanyCode] IS NOT NULL"); + + b.ToTable("Companies"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyBlastSetup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BlastNozzleSize") + .HasColumnType("int"); + + b.Property("BlastRateSqFtPerHourOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompressorCfm") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PrimarySubstrate") + .HasColumnType("int"); + + b.Property("SetupType") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("CompanyBlastSetups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyOperatingCosts", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdditionalCoatLaborPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("AiContextProfile") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("BlastNozzleSize") + .HasColumnType("int"); + + b.Property("BlastRateSqFtPerHourOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("BlastSetupType") + .HasColumnType("int"); + + b.Property("CoatingBoothCostPerHour") + .HasColumnType("decimal(18,2)"); + + b.Property("CoatingGunType") + .HasColumnType("int"); + + b.Property("CoatingRateSqFtPerHourOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ComplexityComplexPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("ComplexityExtremePercent") + .HasColumnType("decimal(18,2)"); + + b.Property("ComplexityModeratePercent") + .HasColumnType("decimal(18,2)"); + + b.Property("ComplexitySimplePercent") + .HasColumnType("decimal(18,2)"); + + b.Property("CompressorCfm") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultOvenCycleMinutes") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralMarkupPercentage") + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MonthlyBillableHours") + .HasColumnType("int"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("MonthlyUtilities") + .HasColumnType("decimal(18,2)"); + + b.Property("OvenOperatingCostPerHour") + .HasColumnType("decimal(18,2)"); + + b.Property("PowderCoatingCostPerSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("PricingMode") + .HasColumnType("int"); + + b.Property("PrimaryBlastSubstrate") + .HasColumnType("int"); + + b.Property("RushChargeFixedAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("RushChargePercentage") + .HasColumnType("decimal(18,2)"); + + b.Property("RushChargeType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SandblasterCostPerHour") + .HasColumnType("decimal(18,2)"); + + b.Property("ShopCapabilityTier") + .HasColumnType("int"); + + b.Property("ShopMinimumCharge") + .HasColumnType("decimal(18,2)"); + + b.Property("ShopSuppliesRate") + .HasColumnType("decimal(18,2)"); + + b.Property("StandardLaborRate") + .HasColumnType("decimal(18,2)"); + + b.Property("TargetMarginPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId") + .IsUnique(); + + b.ToTable("CompanyOperatingCosts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AllowCustomerApproval") + .HasColumnType("bit"); + + b.Property("AutoArchiveJobsDays") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultCurrency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultDateFormat") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultJobPriority") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultPaymentTerms") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultQuoteValidityDays") + .HasColumnType("int"); + + b.Property("DefaultTimeFormat") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultTurnaroundDays") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedRecordRetentionDays") + .HasColumnType("int"); + + b.Property("DueDateWarningDays") + .HasColumnType("int"); + + b.Property("EmailFromAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailFromName") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailNotificationsEnabled") + .HasColumnType("bit"); + + b.Property("FirstInvoiceCreatedAt") + .HasColumnType("datetime2"); + + b.Property("FirstJobCreatedAt") + .HasColumnType("datetime2"); + + b.Property("FirstQuoteCreatedAt") + .HasColumnType("datetime2"); + + b.Property("FirstWorkflowCompleted") + .HasColumnType("bit"); + + b.Property("FirstWorkflowCompletedAt") + .HasColumnType("datetime2"); + + b.Property("GuidedActivationDismissedAt") + .HasColumnType("datetime2"); + + b.Property("InAccentColor") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InDefaultTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("InFooterNote") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceNumberPrefix") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobNumberPrefix") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JobRetentionYears") + .HasColumnType("int"); + + b.Property("LogRetentionDays") + .HasColumnType("int"); + + b.Property("MaintenanceAlertDays") + .HasColumnType("int"); + + b.Property("MigratingFromQuickBooks") + .HasColumnType("bit"); + + b.Property("NotifyOnJobStatusChange") + .HasColumnType("bit"); + + b.Property("NotifyOnNewJob") + .HasColumnType("bit"); + + b.Property("NotifyOnNewQuote") + .HasColumnType("bit"); + + b.Property("NotifyOnPaymentReceived") + .HasColumnType("bit"); + + b.Property("NotifyOnQuoteApproval") + .HasColumnType("bit"); + + b.Property("OnboardingPath") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentReminderDays") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentRemindersEnabled") + .HasColumnType("bit"); + + b.Property("QbMigrationStateJson") + .HasColumnType("nvarchar(max)"); + + b.Property("QtAccentColor") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QtDefaultTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("QtFooterNote") + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteExpiryWarningDays") + .HasColumnType("int"); + + b.Property("QuoteNumberPrefix") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteRetentionYears") + .HasColumnType("int"); + + b.Property("RequireCustomerPO") + .HasColumnType("bit"); + + b.Property("SetupWizardCompleted") + .HasColumnType("bit"); + + b.Property("SetupWizardCompletedAt") + .HasColumnType("datetime2"); + + b.Property("SetupWizardCompletedByName") + .HasColumnType("nvarchar(max)"); + + b.Property("SetupWizardCompletedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("SetupWizardDoneSteps") + .HasColumnType("nvarchar(max)"); + + b.Property("SetupWizardSkippedSteps") + .HasColumnType("nvarchar(max)"); + + b.Property("SetupWizardStarted") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UseMetricSystem") + .HasColumnType("bit"); + + b.Property("WoAccentColor") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WoTerms") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId") + .IsUnique(); + + b.ToTable("CompanyPreferences"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanySmsAgreement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AgreedAt") + .HasColumnType("datetime2"); + + b.Property("AgreedByUserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AgreedByUserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("TermsVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UserAgent") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CompanySmsAgreements"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ContactSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReadAt") + .HasColumnType("datetime2"); + + b.Property("ReadByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("ReadByUserName") + .HasColumnType("nvarchar(max)"); + + b.Property("SenderEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SenderName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ContactSubmissions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("AmountApplied") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssueDate") + .HasColumnType("datetime2"); + + b.Property("IssuedById") + .HasColumnType("nvarchar(450)"); + + b.Property("MemoNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OriginalInvoiceId") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReworkRecordId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("IssuedById"); + + b.HasIndex("OriginalInvoiceId"); + + b.HasIndex("ReworkRecordId"); + + b.HasIndex("CompanyId", "MemoNumber") + .IsUnique() + .HasDatabaseName("IX_CreditMemos_CompanyId_MemoNumber"); + + b.ToTable("CreditMemos"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemoApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AmountApplied") + .HasColumnType("decimal(18,2)"); + + b.Property("AppliedById") + .HasColumnType("nvarchar(450)"); + + b.Property("AppliedDate") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditMemoId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AppliedById"); + + b.HasIndex("CreditMemoId"); + + b.HasIndex("InvoiceId"); + + b.ToTable("CreditMemoApplications"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(450)"); + + b.Property("ContactFirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactLastName") + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("CreditLimit") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(450)"); + + b.Property("GeneralNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsCommercial") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsTaxExempt") + .HasColumnType("bit"); + + b.Property("LastContactDate") + .HasColumnType("datetime2"); + + b.Property("MobilePhone") + .HasColumnType("nvarchar(max)"); + + b.Property("NotifyByEmail") + .HasColumnType("bit"); + + b.Property("NotifyBySms") + .HasColumnType("bit"); + + b.Property("PaymentTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("PricingTierId") + .HasColumnType("int"); + + b.Property("SmsConsentMethod") + .HasColumnType("nvarchar(max)"); + + b.Property("SmsConsentedAt") + .HasColumnType("datetime2"); + + b.Property("SmsOptedOutAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxExemptCertificateContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxExemptCertificateData") + .HasColumnType("varbinary(max)"); + + b.Property("TaxExemptCertificateFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxId") + .HasColumnType("nvarchar(max)"); + + b.Property("UnsubscribeToken") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)") + .HasDefaultValueSql("REPLACE(NEWID(),'-','')"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyName"); + + b.HasIndex("PricingTierId"); + + b.HasIndex("UnsubscribeToken") + .IsUnique() + .HasDatabaseName("IX_Customers_UnsubscribeToken"); + + b.HasIndex("CompanyId", "Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CustomerNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsImportant") + .HasColumnType("bit"); + + b.Property("Note") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId", "CreatedAt") + .HasDatabaseName("IX_CustomerNotes_CustomerId_CreatedAt"); + + b.ToTable("CustomerNotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.DashboardTip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("TipText") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DashboardTips"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Deposit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("AppliedDate") + .HasColumnType("datetime2"); + + b.Property("AppliedToInvoiceId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("ReceiptNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReceivedDate") + .HasColumnType("datetime2"); + + b.Property("RecordedById") + .HasColumnType("nvarchar(450)"); + + b.Property("Reference") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AppliedToInvoiceId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("JobId"); + + b.HasIndex("QuoteId"); + + b.HasIndex("RecordedById"); + + b.ToTable("Deposits"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Equipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastMaintenanceDate") + .HasColumnType("datetime2"); + + b.Property("Location") + .HasColumnType("nvarchar(max)"); + + b.Property("ManualContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("ManualFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("ManualFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("ManualFileSize") + .HasColumnType("bigint"); + + b.Property("ManualUploadedDate") + .HasColumnType("datetime2"); + + b.Property("Manufacturer") + .HasColumnType("nvarchar(max)"); + + b.Property("MaxLoadSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("NextScheduledMaintenance") + .HasColumnType("datetime2"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OvenCycleMinutes") + .HasColumnType("int"); + + b.Property("PurchaseDate") + .HasColumnType("datetime2"); + + b.Property("PurchasePrice") + .HasColumnType("decimal(18,2)"); + + b.Property("RecommendedMaintenanceIntervalDays") + .HasColumnType("int"); + + b.Property("SerialNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WarrantyExpiration") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "Status") + .HasDatabaseName("IX_Equipment_CompanyId_Status"); + + b.ToTable("Equipment"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Expense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpenseAccountId") + .HasColumnType("int"); + + b.Property("ExpenseNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Memo") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentAccountId") + .HasColumnType("int"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("ReceiptFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ExpenseAccountId"); + + b.HasIndex("JobId"); + + b.HasIndex("PaymentAccountId"); + + b.HasIndex("VendorId"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CertificateCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssueDate") + .HasColumnType("datetime2"); + + b.Property("IssuedById") + .HasColumnType("nvarchar(450)"); + + b.Property("IssuedReason") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OriginalAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("PurchasePrice") + .HasColumnType("decimal(18,2)"); + + b.Property("PurchasingCustomerId") + .HasColumnType("int"); + + b.Property("RecipientCustomerId") + .HasColumnType("int"); + + b.Property("RecipientEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientName") + .HasColumnType("nvarchar(max)"); + + b.Property("RedeemedAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceInvoiceItemId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("IssuedById"); + + b.HasIndex("PurchasingCustomerId"); + + b.HasIndex("RecipientCustomerId"); + + b.HasIndex("CompanyId", "CertificateCode") + .IsUnique(); + + b.ToTable("GiftCertificates"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificateRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AmountRedeemed") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("GiftCertificateId") + .HasColumnType("int"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("RedeemedById") + .HasColumnType("nvarchar(450)"); + + b.Property("RedeemedDate") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("GiftCertificateId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("RedeemedById"); + + b.ToTable("GiftCertificateRedemptions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InAppNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Link") + .HasColumnType("nvarchar(max)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NotificationType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("ReadAt") + .HasColumnType("datetime2"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("QuoteId"); + + b.ToTable("InAppNotifications"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryCategoryLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsCoating") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "CategoryCode") + .IsUnique(); + + b.ToTable("InventoryCategoryLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AverageCost") + .HasColumnType("decimal(18,2)"); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CogsAccountId") + .HasColumnType("int"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorFamilies") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CureTemperatureF") + .HasColumnType("decimal(18,2)"); + + b.Property("CureTimeMinutes") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscontinuedDate") + .HasColumnType("datetime2"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("HasSamplePanel") + .HasColumnType("bit"); + + b.Property("ImageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryAccountId") + .HasColumnType("int"); + + b.Property("InventoryCategoryId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastPurchaseDate") + .HasColumnType("datetime2"); + + b.Property("LastPurchasePrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Location") + .HasColumnType("nvarchar(max)"); + + b.Property("Manufacturer") + .HasColumnType("nvarchar(max)"); + + b.Property("ManufacturerPartNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("MaximumStock") + .HasColumnType("decimal(18,2)"); + + b.Property("MinimumStock") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryVendorId") + .HasColumnType("int"); + + b.Property("QuantityOnHand") + .HasColumnType("decimal(18,2)"); + + b.Property("ReorderPoint") + .HasColumnType("decimal(18,2)"); + + b.Property("ReorderQuantity") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiresClearCoat") + .HasColumnType("bit"); + + b.Property("SKU") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SdsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("SpecPageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("SpecificGravity") + .HasColumnType("decimal(18,2)"); + + b.Property("TdsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitOfMeasure") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorPartNumber") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CogsAccountId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("InventoryAccountId"); + + b.HasIndex("InventoryCategoryId"); + + b.HasIndex("IsActive"); + + b.HasIndex("PrimaryVendorId"); + + b.HasIndex("CompanyId", "IsActive"); + + b.HasIndex("CompanyId", "SKU") + .IsUnique() + .HasDatabaseName("IX_InventoryItems_CompanyId_SKU"); + + b.HasIndex("CompanyId", "QuantityOnHand", "ReorderPoint") + .HasDatabaseName("IX_InventoryItems_CompanyId_Quantity_Reorder"); + + b.ToTable("InventoryItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BalanceAfter") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PurchaseOrderId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("Reference") + .HasColumnType("nvarchar(max)"); + + b.Property("TotalCost") + .HasColumnType("decimal(18,2)"); + + b.Property("TransactionDate") + .HasColumnType("datetime2"); + + b.Property("TransactionType") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("JobId"); + + b.HasIndex("PurchaseOrderId"); + + b.HasIndex("TransactionType", "TransactionDate"); + + b.ToTable("InventoryTransactions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Invoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AmountPaid") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditApplied") + .HasColumnType("decimal(18,2)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("CustomerPO") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("ExternalReference") + .HasColumnType("nvarchar(450)"); + + b.Property("GiftCertificateRedeemed") + .HasColumnType("decimal(18,2)"); + + b.Property("InternalNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceDate") + .HasColumnType("datetime2"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OnlineAmountPaid") + .HasColumnType("decimal(18,2)"); + + b.Property("OnlinePaymentStatus") + .HasColumnType("int"); + + b.Property("OnlineSurchargeCollected") + .HasColumnType("decimal(18,2)"); + + b.Property("PaidDate") + .HasColumnType("datetime2"); + + b.Property("PaymentLinkExpiresAt") + .HasColumnType("datetime2"); + + b.Property("PaymentLinkToken") + .HasColumnType("nvarchar(max)"); + + b.Property("PreparedById") + .HasColumnType("nvarchar(450)"); + + b.Property("SalesTaxAccountId") + .HasColumnType("int"); + + b.Property("SentDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StripePaymentIntentId") + .HasColumnType("nvarchar(max)"); + + b.Property("SubTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Terms") + .HasColumnType("nvarchar(max)"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DueDate"); + + b.HasIndex("InvoiceDate"); + + b.HasIndex("JobId") + .IsUnique() + .HasFilter("[JobId] IS NOT NULL"); + + b.HasIndex("PreparedById"); + + b.HasIndex("SalesTaxAccountId"); + + b.HasIndex("Status"); + + b.HasIndex("CompanyId", "CustomerId") + .HasDatabaseName("IX_Invoices_CompanyId_CustomerId"); + + b.HasIndex("CompanyId", "DueDate") + .HasDatabaseName("IX_Invoices_CompanyId_DueDate"); + + b.HasIndex("CompanyId", "ExternalReference") + .HasDatabaseName("IX_Invoices_CompanyId_ExternalReference"); + + b.HasIndex("CompanyId", "InvoiceNumber") + .IsUnique() + .HasDatabaseName("IX_Invoices_CompanyId_InvoiceNumber"); + + b.HasIndex("CompanyId", "IsDeleted"); + + b.HasIndex("CompanyId", "JobId") + .IsUnique() + .HasDatabaseName("IX_Invoices_CompanyId_JobId") + .HasFilter("[JobId] IS NOT NULL"); + + b.HasIndex("CompanyId", "Status") + .HasDatabaseName("IX_Invoices_CompanyId_Status"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InvoiceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CatalogItemId") + .HasColumnType("int"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("GcExpiryDate") + .HasColumnType("datetime2"); + + b.Property("GcRecipientEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("GcRecipientName") + .HasColumnType("nvarchar(max)"); + + b.Property("GeneratedGiftCertificateId") + .HasColumnType("int"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsGiftCertificate") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("RevenueAccountId") + .HasColumnType("int"); + + b.Property("SourceJobItemId") + .HasColumnType("int"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CatalogItemId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("GeneratedGiftCertificateId"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("IX_InvoiceItems_InvoiceId"); + + b.HasIndex("RevenueAccountId"); + + b.HasIndex("SourceJobItemId"); + + b.ToTable("InvoiceItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualTimeSpentHours") + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("CustomerPO") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountReason") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountType") + .HasColumnType("int"); + + b.Property("DiscountValue") + .HasColumnType("decimal(18,2)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("FinalPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("IntakeCheckedByUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("IntakeConditionNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IntakeDate") + .HasColumnType("datetime2"); + + b.Property("IntakePartCount") + .HasColumnType("int"); + + b.Property("InternalNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IsCustomerApproved") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsReworkJob") + .HasColumnType("bit"); + + b.Property("IsRushJob") + .HasColumnType("bit"); + + b.Property("JobNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("JobPriorityId") + .HasColumnType("int"); + + b.Property("JobStatusId") + .HasColumnType("int"); + + b.Property("OriginalJobId") + .HasColumnType("int"); + + b.Property("OvenCostId") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("QuoteSnapshotUpdatedAt") + .HasColumnType("datetime2"); + + b.Property("QuotedPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiresCustomerApproval") + .HasColumnType("bit"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("ShopAccessCode") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("ShopWorkerId") + .HasColumnType("int"); + + b.Property("SpecialInstructions") + .HasColumnType("nvarchar(max)"); + + b.Property("StartedDate") + .HasColumnType("datetime2"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedUserId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DueDate"); + + b.HasIndex("IntakeCheckedByUserId"); + + b.HasIndex("JobPriorityId"); + + b.HasIndex("JobStatusId"); + + b.HasIndex("OriginalJobId"); + + b.HasIndex("OvenCostId"); + + b.HasIndex("QuoteId") + .IsUnique() + .HasFilter("[QuoteId] IS NOT NULL"); + + b.HasIndex("ScheduledDate"); + + b.HasIndex("ShopWorkerId"); + + b.HasIndex("CompanyId", "CustomerId") + .HasDatabaseName("IX_Jobs_CompanyId_CustomerId"); + + b.HasIndex("CompanyId", "DueDate") + .HasDatabaseName("IX_Jobs_CompanyId_DueDate"); + + b.HasIndex("CompanyId", "IsDeleted"); + + b.HasIndex("CompanyId", "JobNumber") + .IsUnique() + .HasDatabaseName("IX_Jobs_CompanyId_JobNumber"); + + b.HasIndex("CompanyId", "JobPriorityId") + .HasDatabaseName("IX_Jobs_CompanyId_JobPriorityId"); + + b.HasIndex("CompanyId", "JobStatusId") + .HasDatabaseName("IX_Jobs_CompanyId_JobStatusId"); + + b.HasIndex("CompanyId", "ScheduledDate") + .HasDatabaseName("IX_Jobs_CompanyId_ScheduledDate"); + + b.HasIndex("CompanyId", "ShopAccessCode") + .IsUnique() + .HasDatabaseName("IX_Jobs_CompanyId_ShopAccessCode"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobChangeHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChangeDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedAt") + .HasColumnType("datetime2"); + + b.Property("ChangedByUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FieldName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("NewValue") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ChangedByUserId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("JobId"); + + b.ToTable("JobChangeHistories"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobDailyPriority", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.ToTable("JobDailyPriorities"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AiPredictionId") + .HasColumnType("int"); + + b.Property("AiTags") + .HasColumnType("nvarchar(max)"); + + b.Property("CatalogItemId") + .HasColumnType("int"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Complexity") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("IncludePrepCost") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsGenericItem") + .HasColumnType("bit"); + + b.Property("IsLaborItem") + .HasColumnType("bit"); + + b.Property("IsSalesItem") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LaborCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ManualUnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiresMasking") + .HasColumnType("bit"); + + b.Property("RequiresSandblasting") + .HasColumnType("bit"); + + b.Property("Sku") + .HasColumnType("nvarchar(max)"); + + b.Property("SurfaceArea") + .HasColumnType("decimal(18,2)"); + + b.Property("SurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AiPredictionId"); + + b.HasIndex("CatalogItemId"); + + b.HasIndex("JobId") + .HasDatabaseName("IX_JobItems_JobId"); + + b.HasIndex("JobId", "IsDeleted") + .HasDatabaseName("IX_JobItems_JobId_IsDeleted"); + + b.ToTable("JobItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItemCoat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualPowderUsedLbs") + .HasColumnType("decimal(18,2)"); + + b.Property("CoatName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("PowderOrdered") + .HasColumnType("bit"); + + b.Property("PowderOrderedAt") + .HasColumnType("datetime2"); + + b.Property("PowderOrderedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderReceived") + .HasColumnType("bit"); + + b.Property("PowderReceivedAt") + .HasColumnType("datetime2"); + + b.Property("PowderReceivedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderReceivedLbs") + .HasColumnType("decimal(18,2)"); + + b.Property("PowderToOrder") + .HasColumnType("decimal(18,2)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("JobItemId"); + + b.HasIndex("VendorId"); + + b.ToTable("JobItemCoats"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItemPrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BlastSetupId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BlastSetupId"); + + b.HasIndex("CompanyId") + .HasDatabaseName("IX_JobItemPrepServices_CompanyId"); + + b.HasIndex("JobItemId") + .HasDatabaseName("IX_JobItemPrepServices_JobItemId"); + + b.HasIndex("PrepServiceId") + .HasDatabaseName("IX_JobItemPrepServices_PrepServiceId"); + + b.ToTable("JobItemPrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsImportant") + .HasColumnType("bit"); + + b.Property("IsInternal") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Note") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId", "CreatedAt") + .HasDatabaseName("IX_JobNotes_JobId_CreatedAt"); + + b.ToTable("JobNotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Caption") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsAiAnalysisPhoto") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("PhotoType") + .HasColumnType("int"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UploadedById") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UploadedDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("UploadedById"); + + b.HasIndex("JobId", "IsDeleted", "DisplayOrder") + .HasDatabaseName("IX_JobPhotos_JobId_IsDeleted_DisplayOrder"); + + b.ToTable("JobPhotos"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("PrepServiceId"); + + b.ToTable("JobPrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPriorityLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("PriorityCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "PriorityCode") + .IsUnique(); + + b.ToTable("JobPriorityLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChangedDate") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FromStatusId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ToStatusId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FromStatusId"); + + b.HasIndex("JobId"); + + b.HasIndex("ToStatusId"); + + b.ToTable("JobStatusHistory"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("IsTerminalStatus") + .HasColumnType("bit"); + + b.Property("IsWorkInProgressStatus") + .HasColumnType("bit"); + + b.Property("StatusCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkflowCategory") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "StatusCode") + .IsUnique(); + + b.ToTable("JobStatusLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SpecialInstructions") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UsageCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("JobTemplates"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CatalogItemId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Complexity") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IncludePrepCost") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsGenericItem") + .HasColumnType("bit"); + + b.Property("IsLaborItem") + .HasColumnType("bit"); + + b.Property("JobTemplateId") + .HasColumnType("int"); + + b.Property("ManualUnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiresMasking") + .HasColumnType("bit"); + + b.Property("RequiresSandblasting") + .HasColumnType("bit"); + + b.Property("SurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CatalogItemId"); + + b.HasIndex("JobTemplateId"); + + b.ToTable("JobTemplateItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItemCoat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CoatName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobTemplateItemId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("JobTemplateItemId"); + + b.HasIndex("VendorId"); + + b.ToTable("JobTemplateItemCoats"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItemPrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobTemplateItemId") + .HasColumnType("int"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobTemplateItemId"); + + b.HasIndex("PrepServiceId"); + + b.ToTable("JobTemplateItemPrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTimeEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("HoursWorked") + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ShopWorkerId") + .HasColumnType("int"); + + b.Property("Stage") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UserDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("ShopWorkerId"); + + b.ToTable("JobTimeEntries"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.MaintenanceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AssignedUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DowntimeHours") + .HasColumnType("decimal(18,2)"); + + b.Property("EquipmentId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsRecurring") + .HasColumnType("bit"); + + b.Property("LaborCost") + .HasColumnType("decimal(18,2)"); + + b.Property("MaintenanceType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PartsCost") + .HasColumnType("decimal(18,2)"); + + b.Property("PartsReplaced") + .HasColumnType("nvarchar(max)"); + + b.Property("PerformedById") + .HasColumnType("nvarchar(450)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("RecurrenceEndDate") + .HasColumnType("datetime2"); + + b.Property("RecurrenceFrequency") + .HasColumnType("int"); + + b.Property("RecurrenceGroupId") + .HasColumnType("nvarchar(max)"); + + b.Property("RecurrenceParentId") + .HasColumnType("int"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("ShopWorkerId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TechnicianNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("TotalCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkPerformed") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedUserId"); + + b.HasIndex("EquipmentId"); + + b.HasIndex("PerformedById"); + + b.HasIndex("RecurrenceParentId"); + + b.HasIndex("ScheduledDate"); + + b.HasIndex("ShopWorkerId"); + + b.HasIndex("Status"); + + b.HasIndex("CompanyId", "ScheduledDate") + .HasDatabaseName("IX_MaintenanceRecords_CompanyId_ScheduledDate"); + + b.HasIndex("CompanyId", "Status") + .HasDatabaseName("IX_MaintenanceRecords_CompanyId_Status"); + + b.ToTable("MaintenanceRecords"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ManufacturerLookupPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Domain") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ManufacturerName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductUrlTemplate") + .HasColumnType("nvarchar(max)"); + + b.Property("SlugTransform") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ManufacturerLookupPatterns"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.NotificationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Channel") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorMessage") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("Recipient") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("JobId"); + + b.HasIndex("QuoteId"); + + b.HasIndex("CompanyId", "SentAt") + .HasDatabaseName("IX_NotificationLogs_CompanyId_SentAt"); + + b.HasIndex("CompanyId", "Status") + .HasDatabaseName("IX_NotificationLogs_CompanyId_Status"); + + b.ToTable("NotificationLogs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.NotificationTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Channel") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId", "NotificationType", "Channel") + .IsUnique() + .HasDatabaseName("IX_NotificationTemplates_Company_Type_Channel"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualEndTime") + .HasColumnType("datetime2"); + + b.Property("ActualStartTime") + .HasColumnType("datetime2"); + + b.Property("AiReasoningJson") + .HasColumnType("nvarchar(max)"); + + b.Property("AiSuggested") + .HasColumnType("bit"); + + b.Property("BatchNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CureTemperatureF") + .HasColumnType("decimal(18,2)"); + + b.Property("CycleMinutes") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentId") + .HasColumnType("int"); + + b.Property("EstimatedEndTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OvenCostId") + .HasColumnType("int"); + + b.Property("PrimaryColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("ScheduledStartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalSurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EquipmentId"); + + b.HasIndex("OvenCostId"); + + b.HasIndex("ScheduledDate", "Status"); + + b.ToTable("OvenBatches"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatchItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CoatPassNumber") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("JobItemCoatId") + .HasColumnType("int"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OvenBatchId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SurfaceAreaContribution") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("JobItemCoatId"); + + b.HasIndex("JobItemId"); + + b.HasIndex("OvenBatchId"); + + b.ToTable("OvenBatchItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenCost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CostPerHour") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultCycleMinutes") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MaxLoadSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("OvenCosts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DepositAccountId") + .HasColumnType("int"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentDate") + .HasColumnType("datetime2"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("RecordedById") + .HasColumnType("nvarchar(450)"); + + b.Property("Reference") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DepositAccountId"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("IX_Payments_InvoiceId"); + + b.HasIndex("PaymentDate"); + + b.HasIndex("RecordedById"); + + b.HasIndex("CompanyId", "PaymentDate") + .HasDatabaseName("IX_Payments_CompanyId_PaymentDate"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PendingRegistrationSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsAnnual") + .HasColumnType("bit"); + + b.Property("IsCompleted") + .HasColumnType("bit"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Plan") + .HasColumnType("int"); + + b.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("PendingRegistrationSessions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("GroupName") + .HasColumnType("nvarchar(max)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Label") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PowderCatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApplicationGuideUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorFamilies") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CureTemperatureF") + .HasColumnType("decimal(18,2)"); + + b.Property("CureTimeMinutes") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("ImageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDiscontinued") + .HasColumnType("bit"); + + b.Property("IsUserContributed") + .HasColumnType("bit"); + + b.Property("LastSyncedAt") + .HasColumnType("datetime2"); + + b.Property("PriceTiersJson") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("RequiresClearCoat") + .HasColumnType("bit"); + + b.Property("SdsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SpecificGravity") + .HasColumnType("decimal(18,2)"); + + b.Property("TdsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("VendorName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("ColorName") + .HasDatabaseName("IX_PowderCatalogItems_ColorName"); + + b.HasIndex("VendorName", "Sku") + .IsUnique() + .HasDatabaseName("IX_PowderCatalogItems_Vendor_Sku"); + + b.ToTable("PowderCatalogItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PowderUsageLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualLbsUsed") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedLbs") + .HasColumnType("decimal(18,2)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("InventoryTransactionId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("JobItemCoatId") + .HasColumnType("int"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("RecordedAt") + .HasColumnType("datetime2"); + + b.Property("RecordedByUserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VarianceLbs") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("InventoryTransactionId"); + + b.HasIndex("JobId"); + + b.HasIndex("JobItemCoatId"); + + b.HasIndex("JobItemId"); + + b.ToTable("PowderUsageLogs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("RequiresBlastSetup") + .HasColumnType("bit"); + + b.Property("ServiceName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("PrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PricingTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("TierName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("PricingTiers"); + + b.HasData( + new + { + Id = 1, + CompanyId = 0, + CreatedAt = new DateTime(2026, 5, 6, 12, 35, 37, 694, DateTimeKind.Utc).AddTicks(5288), + Description = "Standard pricing for regular customers", + DiscountPercent = 0m, + IsActive = true, + IsDeleted = false, + TierName = "Standard" + }, + new + { + Id = 2, + CompanyId = 0, + CreatedAt = new DateTime(2026, 5, 6, 12, 35, 37, 694, DateTimeKind.Utc).AddTicks(5294), + Description = "5% discount for preferred customers", + DiscountPercent = 5m, + IsActive = true, + IsDeleted = false, + TierName = "Preferred" + }, + new + { + Id = 3, + CompanyId = 0, + CreatedAt = new DateTime(2026, 5, 6, 12, 35, 37, 694, DateTimeKind.Utc).AddTicks(5296), + Description = "10% discount for premium customers", + DiscountPercent = 10m, + IsActive = true, + IsDeleted = false, + TierName = "Premium" + }); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BillId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectedDeliveryDate") + .HasColumnType("datetime2"); + + b.Property("InternalNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OrderDate") + .HasColumnType("datetime2"); + + b.Property("PoNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReceivedDate") + .HasColumnType("datetime2"); + + b.Property("ShippingCost") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BillId"); + + b.HasIndex("VendorId"); + + b.ToTable("PurchaseOrders"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LineTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PurchaseOrderId") + .HasColumnType("int"); + + b.Property("QuantityOrdered") + .HasColumnType("decimal(18,2)"); + + b.Property("QuantityReceived") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitOfMeasure") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("PurchaseOrderId"); + + b.ToTable("PurchaseOrderItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApprovalToken") + .HasColumnType("nvarchar(450)"); + + b.Property("ApprovalTokenExpiresAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalTokenUsedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedDate") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ConvertedDate") + .HasColumnType("datetime2"); + + b.Property("ConvertedToJobId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("CustomerPO") + .HasColumnType("nvarchar(max)"); + + b.Property("DeclineReason") + .HasColumnType("nvarchar(max)"); + + b.Property("DeclinedByIp") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DepositAmountPaid") + .HasColumnType("decimal(18,2)"); + + b.Property("DepositPaymentIntentId") + .HasColumnType("nvarchar(max)"); + + b.Property("DepositPaymentLinkExpiresAt") + .HasColumnType("datetime2"); + + b.Property("DepositPaymentLinkToken") + .HasColumnType("nvarchar(max)"); + + b.Property("DepositPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("DiscountPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("DiscountReason") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountType") + .HasColumnType("int"); + + b.Property("DiscountValue") + .HasColumnType("decimal(18,2)"); + + b.Property("EquipmentCosts") + .HasColumnType("decimal(18,2)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("HideDiscountFromCustomer") + .HasColumnType("bit"); + + b.Property("IsCommercial") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsRushJob") + .HasColumnType("bit"); + + b.Property("ItemsSubtotal") + .HasColumnType("decimal(18,2)"); + + b.Property("LaborCosts") + .HasColumnType("decimal(18,2)"); + + b.Property("MaterialCosts") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OvenBatchCost") + .HasColumnType("decimal(18,2)"); + + b.Property("OvenBatches") + .HasColumnType("int"); + + b.Property("OvenCostId") + .HasColumnType("int"); + + b.Property("OvenCycleMinutes") + .HasColumnType("int"); + + b.Property("OverheadAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("OverheadPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("PreparedById") + .HasColumnType("nvarchar(450)"); + + b.Property("ProfitMargin") + .HasColumnType("decimal(18,2)"); + + b.Property("ProfitPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("ProspectAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectCity") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectCompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectState") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectZipCode") + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteDate") + .HasColumnType("datetime2"); + + b.Property("QuoteNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("QuoteStatusId") + .HasColumnType("int"); + + b.Property("RequiresDeposit") + .HasColumnType("bit"); + + b.Property("RushFee") + .HasColumnType("decimal(18,2)"); + + b.Property("SentDate") + .HasColumnType("datetime2"); + + b.Property("ShopSuppliesAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("ShopSuppliesPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("SubTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Terms") + .HasColumnType("nvarchar(max)"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalToken") + .IsUnique() + .HasDatabaseName("IX_Quotes_ApprovalToken") + .HasFilter("[ApprovalToken] IS NOT NULL"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("OvenCostId"); + + b.HasIndex("PreparedById"); + + b.HasIndex("QuoteStatusId"); + + b.HasIndex("CompanyId", "ExpirationDate") + .HasDatabaseName("IX_Quotes_CompanyId_ExpirationDate"); + + b.HasIndex("CompanyId", "IsDeleted"); + + b.HasIndex("CompanyId", "QuoteNumber") + .IsUnique() + .HasDatabaseName("IX_Quotes_CompanyId_QuoteNumber"); + + b.HasIndex("CompanyId", "QuoteStatusId") + .HasDatabaseName("IX_Quotes_CompanyId_QuoteStatusId"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteChangeHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChangeDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedAt") + .HasColumnType("datetime2"); + + b.Property("ChangedByUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FieldName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("NewValue") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValue") + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ChangedByUserId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("QuoteId"); + + b.ToTable("QuoteChangeHistories"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AiPredictionId") + .HasColumnType("int"); + + b.Property("AiTags") + .HasColumnType("nvarchar(max)"); + + b.Property("CatalogItemId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Complexity") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IsAiItem") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsGenericItem") + .HasColumnType("bit"); + + b.Property("IsLaborItem") + .HasColumnType("bit"); + + b.Property("IsSalesItem") + .HasColumnType("bit"); + + b.Property("ItemEquipmentCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ItemLaborCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ItemMaterialCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ManualUnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("RequiresMasking") + .HasColumnType("bit"); + + b.Property("RequiresSandblasting") + .HasColumnType("bit"); + + b.Property("Sku") + .HasColumnType("nvarchar(max)"); + + b.Property("SurfaceArea") + .HasColumnType("decimal(18,2)"); + + b.Property("SurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AiPredictionId"); + + b.HasIndex("CatalogItemId"); + + b.HasIndex("QuoteId") + .HasDatabaseName("IX_QuoteItems_QuoteId"); + + b.ToTable("QuoteItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItemCoat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CoatLaborCost") + .HasColumnType("decimal(18,2)"); + + b.Property("CoatMaterialCost") + .HasColumnType("decimal(18,2)"); + + b.Property("CoatName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CoatTotalCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("PowderToOrder") + .HasColumnType("decimal(18,2)"); + + b.Property("QuoteItemId") + .HasColumnType("int"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("IX_QuoteItemCoats_CompanyId"); + + b.HasIndex("InventoryItemId") + .HasDatabaseName("IX_QuoteItemCoats_InventoryItemId"); + + b.HasIndex("QuoteItemId") + .HasDatabaseName("IX_QuoteItemCoats_QuoteItemId"); + + b.HasIndex("VendorId"); + + b.ToTable("QuoteItemCoats"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItemPrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BlastSetupId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("QuoteItemId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BlastSetupId"); + + b.HasIndex("CompanyId") + .HasDatabaseName("IX_QuoteItemPrepServices_CompanyId"); + + b.HasIndex("PrepServiceId") + .HasDatabaseName("IX_QuoteItemPrepServices_PrepServiceId"); + + b.HasIndex("QuoteItemId") + .HasDatabaseName("IX_QuoteItemPrepServices_QuoteItemId"); + + b.ToTable("QuoteItemPrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuotePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Caption") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsAiAnalysisPhoto") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("TempId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UploadedById") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("QuoteId"); + + b.HasIndex("UploadedById"); + + b.ToTable("QuotePhotos"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuotePrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PrepServiceId"); + + b.HasIndex("QuoteId"); + + b.ToTable("QuotePrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteStatusLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsApprovedStatus") + .HasColumnType("bit"); + + b.Property("IsConvertedStatus") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsDraftStatus") + .HasColumnType("bit"); + + b.Property("IsRejectedStatus") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("StatusCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "StatusCode") + .IsUnique(); + + b.ToTable("QuoteStatusLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditMemoId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssuedById") + .HasColumnType("nvarchar(450)"); + + b.Property("IssuedDate") + .HasColumnType("datetime2"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentId") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reference") + .HasColumnType("nvarchar(max)"); + + b.Property("RefundDate") + .HasColumnType("datetime2"); + + b.Property("RefundMethod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreditMemoId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("IssuedById"); + + b.HasIndex("PaymentId"); + + b.ToTable("Refunds"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ReleaseNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedByUserName") + .HasColumnType("nvarchar(max)"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("ReleasedAt") + .HasColumnType("datetime2"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ReleaseNotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ReworkRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualReworkCost") + .HasColumnType("decimal(18,2)"); + + b.Property("BillingNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefectDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscoveredBy") + .HasColumnType("int"); + + b.Property("DiscoveredDate") + .HasColumnType("datetime2"); + + b.Property("EstimatedReworkCost") + .HasColumnType("decimal(18,2)"); + + b.Property("IsBillableToCustomer") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("Reason") + .HasColumnType("int"); + + b.Property("ReportedByName") + .HasColumnType("nvarchar(max)"); + + b.Property("Resolution") + .HasColumnType("int"); + + b.Property("ResolutionNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedDate") + .HasColumnType("datetime2"); + + b.Property("ReworkJobId") + .HasColumnType("int"); + + b.Property("ReworkType") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("JobItemId"); + + b.HasIndex("ReworkJobId"); + + b.ToTable("ReworkRecords"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ShopWorker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("ShopWorkers"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ShopWorkerRoleCost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("HourlyRate") + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId", "Role") + .IsUnique() + .HasDatabaseName("IX_ShopWorkerRoleCosts_CompanyId_Role"); + + b.ToTable("ShopWorkerRoleCosts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.StripeWebhookEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("RawJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("StripeWebhookEvents"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.SubscriptionPlanConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AllowAccounting") + .HasColumnType("bit"); + + b.Property("AllowAiCatalogPriceCheck") + .HasColumnType("bit"); + + b.Property("AllowAiInventoryAssist") + .HasColumnType("bit"); + + b.Property("AllowAiPhotoQuotes") + .HasColumnType("bit"); + + b.Property("AllowOnlinePayments") + .HasColumnType("bit"); + + b.Property("AllowSms") + .HasColumnType("bit"); + + b.Property("AnnualPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaxActiveJobs") + .HasColumnType("int"); + + b.Property("MaxAiPhotoQuotesPerMonth") + .HasColumnType("int"); + + b.Property("MaxCatalogItems") + .HasColumnType("int"); + + b.Property("MaxCustomers") + .HasColumnType("int"); + + b.Property("MaxJobPhotos") + .HasColumnType("int"); + + b.Property("MaxQuotePhotos") + .HasColumnType("int"); + + b.Property("MaxQuotes") + .HasColumnType("int"); + + b.Property("MaxUsers") + .HasColumnType("int"); + + b.Property("MonthlyPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Plan") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("StripePriceIdAnnual") + .HasColumnType("nvarchar(max)"); + + b.Property("StripePriceIdMonthly") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionPlanConfigs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.TermsAcceptance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AcceptedAt") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("TosVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserAgent") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TermsAcceptances"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.UserPasskey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CredentialId") + .IsRequired() + .HasColumnType("varbinary(900)"); + + b.Property("DeviceFriendlyName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastUsedAt") + .HasColumnType("datetime2"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("SignCount") + .HasColumnType("bigint"); + + b.Property("UserHandle") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CredentialId") + .IsUnique(); + + b.ToTable("UserPasskeys"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Vendor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditLimit") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("DefaultExpenseAccountId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPreferred") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OpeningBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("OpeningBalanceDate") + .HasColumnType("datetime2"); + + b.Property("PaymentTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxId") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Website") + .HasColumnType("nvarchar(max)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("DefaultExpenseAccountId"); + + b.ToTable("Vendors"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Account", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "ParentAccount") + .WithMany("SubAccounts") + .HasForeignKey("ParentAccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ParentAccount"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AiUsageLog", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AnnouncementDismissal", b => + { + b.HasOne("PowderCoating.Core.Entities.Announcement", "Announcement") + .WithMany("Dismissals") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Announcement"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ApplicationUser", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany("Users") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Appointment", b => + { + b.HasOne("PowderCoating.Core.Entities.AppointmentStatusLookup", "AppointmentStatus") + .WithMany("Appointments") + .HasForeignKey("AppointmentStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.AppointmentTypeLookup", "AppointmentType") + .WithMany("Appointments") + .HasForeignKey("AppointmentTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "AssignedUser") + .WithMany() + .HasForeignKey("AssignedUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId"); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.Navigation("AppointmentStatus"); + + b.Navigation("AppointmentType"); + + b.Navigation("AssignedUser"); + + b.Navigation("Customer"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Bill", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "APAccount") + .WithMany("Bills") + .HasForeignKey("APAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany("Bills") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("APAccount"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BillLineItem", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "Account") + .WithMany("BillLineItems") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Bill", "Bill") + .WithMany("LineItems") + .HasForeignKey("BillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.Navigation("Account"); + + b.Navigation("Bill"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BillPayment", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "BankAccount") + .WithMany("BillPayments") + .HasForeignKey("BankAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Bill", "Bill") + .WithMany("Payments") + .HasForeignKey("BillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany("BillPayments") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BankAccount"); + + b.Navigation("Bill"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BugReportAttachment", b => + { + b.HasOne("PowderCoating.Core.Entities.BugReport", "BugReport") + .WithMany("Attachments") + .HasForeignKey("BugReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BugReport"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogCategory", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.CatalogCategory", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogItem", b => + { + b.HasOne("PowderCoating.Core.Entities.CatalogCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "CogsAccount") + .WithMany() + .HasForeignKey("CogsAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId"); + + b.HasOne("PowderCoating.Core.Entities.Account", "RevenueAccount") + .WithMany() + .HasForeignKey("RevenueAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Category"); + + b.Navigation("CogsAccount"); + + b.Navigation("InventoryItem"); + + b.Navigation("RevenueAccount"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyBlastSetup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyOperatingCosts", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithOne("OperatingCosts") + .HasForeignKey("PowderCoating.Core.Entities.CompanyOperatingCosts", "CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyPreferences", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithOne("Preferences") + .HasForeignKey("PowderCoating.Core.Entities.CompanyPreferences", "CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemo", b => + { + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "IssuedBy") + .WithMany() + .HasForeignKey("IssuedById"); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "OriginalInvoice") + .WithMany() + .HasForeignKey("OriginalInvoiceId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.ReworkRecord", "ReworkRecord") + .WithMany() + .HasForeignKey("ReworkRecordId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Customer"); + + b.Navigation("IssuedBy"); + + b.Navigation("OriginalInvoice"); + + b.Navigation("ReworkRecord"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemoApplication", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "AppliedBy") + .WithMany() + .HasForeignKey("AppliedById"); + + b.HasOne("PowderCoating.Core.Entities.CreditMemo", "CreditMemo") + .WithMany("Applications") + .HasForeignKey("CreditMemoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("CreditApplications") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AppliedBy"); + + b.Navigation("CreditMemo"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Customer", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Customers") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PricingTier", "PricingTier") + .WithMany("Customers") + .HasForeignKey("PricingTierId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("PricingTier"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CustomerNote", b => + { + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("CustomerNotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Deposit", b => + { + b.HasOne("PowderCoating.Core.Entities.Invoice", "AppliedToInvoice") + .WithMany() + .HasForeignKey("AppliedToInvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany() + .HasForeignKey("QuoteId"); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "RecordedBy") + .WithMany() + .HasForeignKey("RecordedById"); + + b.Navigation("AppliedToInvoice"); + + b.Navigation("Customer"); + + b.Navigation("Job"); + + b.Navigation("Quote"); + + b.Navigation("RecordedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Equipment", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Equipment") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Expense", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "ExpenseAccount") + .WithMany("Expenses") + .HasForeignKey("ExpenseAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.HasOne("PowderCoating.Core.Entities.Account", "PaymentAccount") + .WithMany("ExpensePaymentAccounts") + .HasForeignKey("PaymentAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany("Expenses") + .HasForeignKey("VendorId"); + + b.Navigation("ExpenseAccount"); + + b.Navigation("Job"); + + b.Navigation("PaymentAccount"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificate", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "IssuedBy") + .WithMany() + .HasForeignKey("IssuedById"); + + b.HasOne("PowderCoating.Core.Entities.Customer", "PurchasingCustomer") + .WithMany() + .HasForeignKey("PurchasingCustomerId"); + + b.HasOne("PowderCoating.Core.Entities.Customer", "RecipientCustomer") + .WithMany() + .HasForeignKey("RecipientCustomerId"); + + b.Navigation("IssuedBy"); + + b.Navigation("PurchasingCustomer"); + + b.Navigation("RecipientCustomer"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificateRedemption", b => + { + b.HasOne("PowderCoating.Core.Entities.GiftCertificate", "GiftCertificate") + .WithMany("Redemptions") + .HasForeignKey("GiftCertificateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("GiftCertificateRedemptions") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "RedeemedBy") + .WithMany() + .HasForeignKey("RedeemedById"); + + b.Navigation("GiftCertificate"); + + b.Navigation("Invoice"); + + b.Navigation("RedeemedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InAppNotification", b => + { + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId"); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId"); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany() + .HasForeignKey("QuoteId"); + + b.Navigation("Customer"); + + b.Navigation("Invoice"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryCategoryLookup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryItem", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "CogsAccount") + .WithMany() + .HasForeignKey("CogsAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("InventoryItems") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "InventoryAccount") + .WithMany() + .HasForeignKey("InventoryAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.InventoryCategoryLookup", "InventoryCategory") + .WithMany("InventoryItems") + .HasForeignKey("InventoryCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "PrimaryVendor") + .WithMany("InventoryItems") + .HasForeignKey("PrimaryVendorId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("CogsAccount"); + + b.Navigation("InventoryAccount"); + + b.Navigation("InventoryCategory"); + + b.Navigation("PrimaryVendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryTransaction", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany("Transactions") + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("PurchaseOrderId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("InventoryItem"); + + b.Navigation("Job"); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Invoice", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("Invoices") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithOne("Invoice") + .HasForeignKey("PowderCoating.Core.Entities.Invoice", "JobId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "PreparedBy") + .WithMany() + .HasForeignKey("PreparedById") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Account", "SalesTaxAccount") + .WithMany() + .HasForeignKey("SalesTaxAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Customer"); + + b.Navigation("Job"); + + b.Navigation("PreparedBy"); + + b.Navigation("SalesTaxAccount"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InvoiceItem", b => + { + b.HasOne("PowderCoating.Core.Entities.CatalogItem", "CatalogItem") + .WithMany() + .HasForeignKey("CatalogItemId"); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.GiftCertificate", "GeneratedGiftCertificate") + .WithMany() + .HasForeignKey("GeneratedGiftCertificateId"); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("InvoiceItems") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "RevenueAccount") + .WithMany() + .HasForeignKey("RevenueAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "SourceJobItem") + .WithMany() + .HasForeignKey("SourceJobItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("CatalogItem"); + + b.Navigation("GeneratedGiftCertificate"); + + b.Navigation("Invoice"); + + b.Navigation("RevenueAccount"); + + b.Navigation("SourceJobItem"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Job", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "AssignedUser") + .WithMany() + .HasForeignKey("AssignedUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Jobs") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("Jobs") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "IntakeCheckedBy") + .WithMany() + .HasForeignKey("IntakeCheckedByUserId"); + + b.HasOne("PowderCoating.Core.Entities.JobPriorityLookup", "JobPriority") + .WithMany("Jobs") + .HasForeignKey("JobPriorityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobStatusLookup", "JobStatus") + .WithMany("Jobs") + .HasForeignKey("JobStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "OriginalJob") + .WithMany() + .HasForeignKey("OriginalJobId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.OvenCost", "OvenCost") + .WithMany("Jobs") + .HasForeignKey("OvenCostId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithOne("ConvertedToJob") + .HasForeignKey("PowderCoating.Core.Entities.Job", "QuoteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.ShopWorker", null) + .WithMany("AssignedJobs") + .HasForeignKey("ShopWorkerId"); + + b.Navigation("AssignedUser"); + + b.Navigation("Customer"); + + b.Navigation("IntakeCheckedBy"); + + b.Navigation("JobPriority"); + + b.Navigation("JobStatus"); + + b.Navigation("OriginalJob"); + + b.Navigation("OvenCost"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobChangeHistory", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "ChangedBy") + .WithMany() + .HasForeignKey("ChangedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChangedBy"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobDailyPriority", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItem", b => + { + b.HasOne("PowderCoating.Core.Entities.AiItemPrediction", "AiPrediction") + .WithMany() + .HasForeignKey("AiPredictionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.CatalogItem", "CatalogItem") + .WithMany() + .HasForeignKey("CatalogItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("JobItems") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AiPrediction"); + + b.Navigation("CatalogItem"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItemCoat", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId"); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany("Coats") + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId"); + + b.Navigation("InventoryItem"); + + b.Navigation("JobItem"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItemPrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.CompanyBlastSetup", "BlastSetup") + .WithMany() + .HasForeignKey("BlastSetupId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany("PrepServices") + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BlastSetup"); + + b.Navigation("JobItem"); + + b.Navigation("PrepService"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobNote", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("Notes") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPhoto", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("Photos") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "UploadedBy") + .WithMany() + .HasForeignKey("UploadedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("UploadedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("JobPrepServices") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("PrepService"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPriorityLookup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusHistory", b => + { + b.HasOne("PowderCoating.Core.Entities.JobStatusLookup", "FromStatus") + .WithMany("FromStatusHistory") + .HasForeignKey("FromStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("StatusHistory") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobStatusLookup", "ToStatus") + .WithMany("ToStatusHistory") + .HasForeignKey("ToStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("FromStatus"); + + b.Navigation("Job"); + + b.Navigation("ToStatus"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusLookup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplate", b => + { + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItem", b => + { + b.HasOne("PowderCoating.Core.Entities.CatalogItem", "CatalogItem") + .WithMany() + .HasForeignKey("CatalogItemId"); + + b.HasOne("PowderCoating.Core.Entities.JobTemplate", "JobTemplate") + .WithMany("Items") + .HasForeignKey("JobTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CatalogItem"); + + b.Navigation("JobTemplate"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItemCoat", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId"); + + b.HasOne("PowderCoating.Core.Entities.JobTemplateItem", "JobTemplateItem") + .WithMany("Coats") + .HasForeignKey("JobTemplateItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId"); + + b.Navigation("InventoryItem"); + + b.Navigation("JobTemplateItem"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItemPrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.JobTemplateItem", "JobTemplateItem") + .WithMany("PrepServices") + .HasForeignKey("JobTemplateItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobTemplateItem"); + + b.Navigation("PrepService"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTimeEntry", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("TimeEntries") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ShopWorker", "Worker") + .WithMany("TimeEntries") + .HasForeignKey("ShopWorkerId"); + + b.Navigation("Job"); + + b.Navigation("Worker"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.MaintenanceRecord", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "AssignedUser") + .WithMany() + .HasForeignKey("AssignedUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Equipment", "Equipment") + .WithMany("MaintenanceRecords") + .HasForeignKey("EquipmentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "PerformedBy") + .WithMany("PerformedMaintenances") + .HasForeignKey("PerformedById") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.MaintenanceRecord", "RecurrenceParent") + .WithMany() + .HasForeignKey("RecurrenceParentId"); + + b.HasOne("PowderCoating.Core.Entities.ShopWorker", null) + .WithMany("AssignedMaintenanceTasks") + .HasForeignKey("ShopWorkerId"); + + b.Navigation("AssignedUser"); + + b.Navigation("Equipment"); + + b.Navigation("PerformedBy"); + + b.Navigation("RecurrenceParent"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.NotificationLog", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("NotificationLogs") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany() + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Invoice"); + + b.Navigation("Job"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.NotificationTemplate", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatch", b => + { + b.HasOne("PowderCoating.Core.Entities.Equipment", "Equipment") + .WithMany("OvenBatches") + .HasForeignKey("EquipmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.OvenCost", "OvenCost") + .WithMany() + .HasForeignKey("OvenCostId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Equipment"); + + b.Navigation("OvenCost"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatchItem", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItemCoat", "JobItemCoat") + .WithMany() + .HasForeignKey("JobItemCoatId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany() + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.OvenBatch", "Batch") + .WithMany("Items") + .HasForeignKey("OvenBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Batch"); + + b.Navigation("Job"); + + b.Navigation("JobItem"); + + b.Navigation("JobItemCoat"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenCost", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Payment", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "DepositAccount") + .WithMany() + .HasForeignKey("DepositAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "RecordedBy") + .WithMany() + .HasForeignKey("RecordedById") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DepositAccount"); + + b.Navigation("Invoice"); + + b.Navigation("RecordedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PowderUsageLog", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.InventoryTransaction", "InventoryTransaction") + .WithMany() + .HasForeignKey("InventoryTransactionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItemCoat", "JobItemCoat") + .WithMany() + .HasForeignKey("JobItemCoatId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany() + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("InventoryItem"); + + b.Navigation("InventoryTransaction"); + + b.Navigation("Job"); + + b.Navigation("JobItem"); + + b.Navigation("JobItemCoat"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PricingTier", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("PricingTiers") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrder", b => + { + b.HasOne("PowderCoating.Core.Entities.Bill", "Bill") + .WithMany() + .HasForeignKey("BillId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Bill"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrderItem", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany("Items") + .HasForeignKey("PurchaseOrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InventoryItem"); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Quote", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Quotes") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("Quotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.OvenCost", "OvenCost") + .WithMany("Quotes") + .HasForeignKey("OvenCostId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "PreparedBy") + .WithMany("PreparedQuotes") + .HasForeignKey("PreparedById") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.QuoteStatusLookup", "QuoteStatus") + .WithMany("Quotes") + .HasForeignKey("QuoteStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("OvenCost"); + + b.Navigation("PreparedBy"); + + b.Navigation("QuoteStatus"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteChangeHistory", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "ChangedBy") + .WithMany() + .HasForeignKey("ChangedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany() + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChangedBy"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItem", b => + { + b.HasOne("PowderCoating.Core.Entities.AiItemPrediction", "AiPrediction") + .WithMany() + .HasForeignKey("AiPredictionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.CatalogItem", "CatalogItem") + .WithMany() + .HasForeignKey("CatalogItemId"); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany("QuoteItems") + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AiPrediction"); + + b.Navigation("CatalogItem"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItemCoat", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.QuoteItem", "QuoteItem") + .WithMany("Coats") + .HasForeignKey("QuoteItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId"); + + b.Navigation("InventoryItem"); + + b.Navigation("QuoteItem"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItemPrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.CompanyBlastSetup", "BlastSetup") + .WithMany() + .HasForeignKey("BlastSetupId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.QuoteItem", "QuoteItem") + .WithMany("PrepServices") + .HasForeignKey("QuoteItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlastSetup"); + + b.Navigation("PrepService"); + + b.Navigation("QuoteItem"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuotePhoto", b => + { + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany("QuotePhotos") + .HasForeignKey("QuoteId"); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "UploadedBy") + .WithMany() + .HasForeignKey("UploadedById"); + + b.Navigation("Quote"); + + b.Navigation("UploadedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuotePrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany("QuotePrepServices") + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PrepService"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteStatusLookup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Refund", b => + { + b.HasOne("PowderCoating.Core.Entities.CreditMemo", "CreditMemo") + .WithMany() + .HasForeignKey("CreditMemoId"); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("Refunds") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "IssuedBy") + .WithMany() + .HasForeignKey("IssuedById"); + + b.HasOne("PowderCoating.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("CreditMemo"); + + b.Navigation("Invoice"); + + b.Navigation("IssuedBy"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ReworkRecord", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("ReworkRecords") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany() + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Job", "ReworkJob") + .WithMany() + .HasForeignKey("ReworkJobId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Job"); + + b.Navigation("JobItem"); + + b.Navigation("ReworkJob"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ShopWorker", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("ShopWorkers") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Vendor", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Vendors") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "DefaultExpenseAccount") + .WithMany() + .HasForeignKey("DefaultExpenseAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DefaultExpenseAccount"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Account", b => + { + b.Navigation("BillLineItems"); + + b.Navigation("BillPayments"); + + b.Navigation("Bills"); + + b.Navigation("ExpensePaymentAccounts"); + + b.Navigation("Expenses"); + + b.Navigation("SubAccounts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Announcement", b => + { + b.Navigation("Dismissals"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ApplicationUser", b => + { + b.Navigation("PerformedMaintenances"); + + b.Navigation("PreparedQuotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentStatusLookup", b => + { + b.Navigation("Appointments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentTypeLookup", b => + { + b.Navigation("Appointments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Bill", b => + { + b.Navigation("LineItems"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BugReport", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogCategory", b => + { + b.Navigation("Items"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Company", b => + { + b.Navigation("Customers"); + + b.Navigation("Equipment"); + + b.Navigation("InventoryItems"); + + b.Navigation("Jobs"); + + b.Navigation("OperatingCosts"); + + b.Navigation("Preferences"); + + b.Navigation("PricingTiers"); + + b.Navigation("Quotes"); + + b.Navigation("ShopWorkers"); + + b.Navigation("Users"); + + b.Navigation("Vendors"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemo", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Customer", b => + { + b.Navigation("CustomerNotes"); + + b.Navigation("Invoices"); + + b.Navigation("Jobs"); + + b.Navigation("NotificationLogs"); + + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Equipment", b => + { + b.Navigation("MaintenanceRecords"); + + b.Navigation("OvenBatches"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificate", b => + { + b.Navigation("Redemptions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryCategoryLookup", b => + { + b.Navigation("InventoryItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryItem", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Invoice", b => + { + b.Navigation("CreditApplications"); + + b.Navigation("GiftCertificateRedemptions"); + + b.Navigation("InvoiceItems"); + + b.Navigation("Payments"); + + b.Navigation("Refunds"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Job", b => + { + b.Navigation("Invoice"); + + b.Navigation("JobItems"); + + b.Navigation("JobPrepServices"); + + b.Navigation("Notes"); + + b.Navigation("Photos"); + + b.Navigation("ReworkRecords"); + + b.Navigation("StatusHistory"); + + b.Navigation("TimeEntries"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItem", b => + { + b.Navigation("Coats"); + + b.Navigation("PrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPriorityLookup", b => + { + b.Navigation("Jobs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusLookup", b => + { + b.Navigation("FromStatusHistory"); + + b.Navigation("Jobs"); + + b.Navigation("ToStatusHistory"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplate", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItem", b => + { + b.Navigation("Coats"); + + b.Navigation("PrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatch", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenCost", b => + { + b.Navigation("Jobs"); + + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PricingTier", b => + { + b.Navigation("Customers"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrder", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Quote", b => + { + b.Navigation("ConvertedToJob"); + + b.Navigation("QuoteItems"); + + b.Navigation("QuotePhotos"); + + b.Navigation("QuotePrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItem", b => + { + b.Navigation("Coats"); + + b.Navigation("PrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteStatusLookup", b => + { + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ShopWorker", b => + { + b.Navigation("AssignedJobs"); + + b.Navigation("AssignedMaintenanceTasks"); + + b.Navigation("TimeEntries"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Vendor", b => + { + b.Navigation("BillPayments"); + + b.Navigation("Bills"); + + b.Navigation("Expenses"); + + b.Navigation("InventoryItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/PowderCoating.Infrastructure/Migrations/20260506123541_AddSpecificGravityToPowderCatalogAndInventory.cs b/src/PowderCoating.Infrastructure/Migrations/20260506123541_AddSpecificGravityToPowderCatalogAndInventory.cs new file mode 100644 index 0000000..692624f --- /dev/null +++ b/src/PowderCoating.Infrastructure/Migrations/20260506123541_AddSpecificGravityToPowderCatalogAndInventory.cs @@ -0,0 +1,81 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PowderCoating.Infrastructure.Migrations +{ + /// + public partial class AddSpecificGravityToPowderCatalogAndInventory : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SpecificGravity", + table: "PowderCatalogItems", + type: "decimal(18,2)", + nullable: true); + + migrationBuilder.AddColumn( + name: "SpecificGravity", + table: "InventoryItems", + type: "decimal(18,2)", + nullable: true); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 1, + column: "CreatedAt", + value: new DateTime(2026, 5, 6, 12, 35, 37, 694, DateTimeKind.Utc).AddTicks(5288)); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 2, + column: "CreatedAt", + value: new DateTime(2026, 5, 6, 12, 35, 37, 694, DateTimeKind.Utc).AddTicks(5294)); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 3, + column: "CreatedAt", + value: new DateTime(2026, 5, 6, 12, 35, 37, 694, DateTimeKind.Utc).AddTicks(5296)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SpecificGravity", + table: "PowderCatalogItems"); + + migrationBuilder.DropColumn( + name: "SpecificGravity", + table: "InventoryItems"); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 1, + column: "CreatedAt", + value: new DateTime(2026, 5, 6, 2, 7, 22, 625, DateTimeKind.Utc).AddTicks(2199)); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 2, + column: "CreatedAt", + value: new DateTime(2026, 5, 6, 2, 7, 22, 625, DateTimeKind.Utc).AddTicks(2206)); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 3, + column: "CreatedAt", + value: new DateTime(2026, 5, 6, 2, 7, 22, 625, DateTimeKind.Utc).AddTicks(2208)); + } + } +} diff --git a/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index a3a2fd2..023fc51 100644 --- a/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -3332,6 +3332,9 @@ namespace PowderCoating.Infrastructure.Migrations b.Property("SpecPageUrl") .HasColumnType("nvarchar(max)"); + b.Property("SpecificGravity") + .HasColumnType("decimal(18,2)"); + b.Property("TdsUrl") .HasColumnType("nvarchar(max)"); @@ -5835,6 +5838,9 @@ namespace PowderCoating.Infrastructure.Migrations .IsRequired() .HasColumnType("nvarchar(450)"); + b.Property("SpecificGravity") + .HasColumnType("decimal(18,2)"); + b.Property("TdsUrl") .HasColumnType("nvarchar(max)"); @@ -6053,7 +6059,7 @@ namespace PowderCoating.Infrastructure.Migrations { Id = 1, CompanyId = 0, - CreatedAt = new DateTime(2026, 5, 6, 2, 7, 22, 625, DateTimeKind.Utc).AddTicks(2199), + CreatedAt = new DateTime(2026, 5, 6, 12, 35, 37, 694, DateTimeKind.Utc).AddTicks(5288), Description = "Standard pricing for regular customers", DiscountPercent = 0m, IsActive = true, @@ -6064,7 +6070,7 @@ namespace PowderCoating.Infrastructure.Migrations { Id = 2, CompanyId = 0, - CreatedAt = new DateTime(2026, 5, 6, 2, 7, 22, 625, DateTimeKind.Utc).AddTicks(2206), + CreatedAt = new DateTime(2026, 5, 6, 12, 35, 37, 694, DateTimeKind.Utc).AddTicks(5294), Description = "5% discount for preferred customers", DiscountPercent = 5m, IsActive = true, @@ -6075,7 +6081,7 @@ namespace PowderCoating.Infrastructure.Migrations { Id = 3, CompanyId = 0, - CreatedAt = new DateTime(2026, 5, 6, 2, 7, 22, 625, DateTimeKind.Utc).AddTicks(2208), + CreatedAt = new DateTime(2026, 5, 6, 12, 35, 37, 694, DateTimeKind.Utc).AddTicks(5296), Description = "10% discount for premium customers", DiscountPercent = 10m, IsActive = true, diff --git a/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs b/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs index a49bb70..4515c9c 100644 --- a/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs +++ b/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs @@ -28,7 +28,8 @@ namespace PowderCoating.Infrastructure.Services; public class InventoryAiLookupService : IInventoryAiLookupService { private const decimal DefaultTransferEfficiency = 65m; - private const decimal TheoreticalCoverageAtOneMilFactor = 192.3m; + private const decimal TheoreticalCoverageConstant = 192.3m; + private const decimal DefaultCoverageThicknessMils = 1.5m; private readonly IConfiguration _config; private readonly IHttpClientFactory _httpClientFactory; @@ -1247,7 +1248,7 @@ Rules: if (!result.CoverageSqFtPerLb.HasValue && result.SpecificGravity is > 0) { - var calculatedCoverage = TheoreticalCoverageAtOneMilFactor / result.SpecificGravity.Value; + var calculatedCoverage = TheoreticalCoverageConstant / (result.SpecificGravity.Value * DefaultCoverageThicknessMils); result.CoverageSqFtPerLb = Math.Round(calculatedCoverage, 2, MidpointRounding.AwayFromZero); } } diff --git a/src/PowderCoating.Web/Controllers/InventoryController.cs b/src/PowderCoating.Web/Controllers/InventoryController.cs index 7afdf85..02dcce8 100644 --- a/src/PowderCoating.Web/Controllers/InventoryController.cs +++ b/src/PowderCoating.Web/Controllers/InventoryController.cs @@ -747,6 +747,7 @@ public class InventoryController : Controller if (match.ColorFamilies != null) result.ColorFamilies = match.ColorFamilies; if (match.RequiresClearCoat != null) result.RequiresClearCoat = match.RequiresClearCoat; if (match.CoverageSqFtPerLb != null) result.CoverageSqFtPerLb = match.CoverageSqFtPerLb; + if (match.SpecificGravity != null) result.SpecificGravity = match.SpecificGravity; result.TransferEfficiency ??= GetEffectiveTransferEfficiency(match.TransferEfficiency); // URL / price fields: fill gaps only — AI may have found something better result.ImageUrl ??= match.ImageUrl; @@ -777,6 +778,7 @@ public class InventoryController : Controller ColorFamilies = result.ColorFamilies, RequiresClearCoat = result.RequiresClearCoat, CoverageSqFtPerLb = result.CoverageSqFtPerLb, + SpecificGravity = result.SpecificGravity, TransferEfficiency = GetEffectiveTransferEfficiency(result.TransferEfficiency), ImageUrl = result.ImageUrl, ProductUrl = result.SpecPageUrl, @@ -875,6 +877,7 @@ public class InventoryController : Controller aiResult.CureTimeMinutes ??= full.CureTimeMinutes; aiResult.RequiresClearCoat ??= full.RequiresClearCoat; aiResult.CoverageSqFtPerLb ??= full.CoverageSqFtPerLb; + aiResult.SpecificGravity ??= full.SpecificGravity; aiResult.TransferEfficiency ??= GetEffectiveTransferEfficiency(full.TransferEfficiency); aiResult.ManufacturerPartNumber ??= full.ManufacturerPartNumber; aiResult.ColorName ??= full.ColorName; @@ -954,6 +957,7 @@ public class InventoryController : Controller colorFamilies = aiResult.ColorFamilies, requiresClearCoat = aiResult.RequiresClearCoat, coverageSqFtPerLb = aiResult.CoverageSqFtPerLb, + specificGravity = aiResult.SpecificGravity, transferEfficiency = aiResult.TransferEfficiency ?? DefaultTransferEfficiency, unitPrice = aiResult.UnitCostPerLb ?? 0m, imageUrl = aiResult.ImageUrl, @@ -1106,6 +1110,7 @@ public class InventoryController : Controller colorFamilies = p.ColorFamilies, requiresClearCoat = p.RequiresClearCoat, coverageSqFtPerLb = p.CoverageSqFtPerLb, + specificGravity = p.SpecificGravity, transferEfficiency = GetEffectiveTransferEfficiency(p.TransferEfficiency) }) .ToList(); diff --git a/src/PowderCoating.Web/Controllers/PowderCatalogController.cs b/src/PowderCoating.Web/Controllers/PowderCatalogController.cs index 9160d3c..d29e840 100644 --- a/src/PowderCoating.Web/Controllers/PowderCatalogController.cs +++ b/src/PowderCoating.Web/Controllers/PowderCatalogController.cs @@ -181,6 +181,7 @@ public class PowderCatalogController : Controller ColorFamilies = NullIfWhiteSpace(model.ColorFamilies), RequiresClearCoat = model.RequiresClearCoat, CoverageSqFtPerLb = model.CoverageSqFtPerLb, + SpecificGravity = model.SpecificGravity, TransferEfficiency = model.TransferEfficiency, IsDiscontinued = model.IsDiscontinued, IsUserContributed = model.IsUserContributed, @@ -250,6 +251,7 @@ public class PowderCatalogController : Controller entity.ColorFamilies = NullIfWhiteSpace(model.ColorFamilies); entity.RequiresClearCoat = model.RequiresClearCoat; entity.CoverageSqFtPerLb = model.CoverageSqFtPerLb; + entity.SpecificGravity = model.SpecificGravity; entity.TransferEfficiency = model.TransferEfficiency; entity.IsDiscontinued = model.IsDiscontinued; entity.IsUserContributed = model.IsUserContributed; @@ -412,6 +414,7 @@ public class PowderCatalogController : Controller TdsUrl = p.TdsUrl, ApplicationGuideUrl = p.ApplicationGuideUrl, ProductUrl = p.ProductUrl, + SpecificGravity = p.SpecificGravity, IsDiscontinued = p.IsDiscontinued }) .ToList(); @@ -661,6 +664,7 @@ public class PowderCatalogController : Controller ColorFamilies = item.ColorFamilies, RequiresClearCoat = item.RequiresClearCoat, CoverageSqFtPerLb = item.CoverageSqFtPerLb, + SpecificGravity = item.SpecificGravity, TransferEfficiency = GetEffectiveTransferEfficiency(item.TransferEfficiency), IsDiscontinued = item.IsDiscontinued, IsUserContributed = item.IsUserContributed, diff --git a/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogFormViewModel.cs b/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogFormViewModel.cs index d25165d..f2a72ec 100644 --- a/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogFormViewModel.cs +++ b/src/PowderCoating.Web/ViewModels/PowderCatalog/PowderCatalogFormViewModel.cs @@ -69,6 +69,10 @@ public class PowderCatalogFormViewModel [Display(Name = "Coverage (Sq Ft / Lb)")] public decimal? CoverageSqFtPerLb { get; set; } + [Range(0, 100)] + [Display(Name = "Specific Gravity")] + public decimal? SpecificGravity { get; set; } + [Range(0, 100)] [Display(Name = "Transfer Efficiency (%)")] public decimal? TransferEfficiency { get; set; } diff --git a/src/PowderCoating.Web/Views/Inventory/Create.cshtml b/src/PowderCoating.Web/Views/Inventory/Create.cshtml index 7ccfe82..b19492d 100644 --- a/src/PowderCoating.Web/Views/Inventory/Create.cshtml +++ b/src/PowderCoating.Web/Views/Inventory/Create.cshtml @@ -159,21 +159,35 @@
-
+ -
+
+
+ + + + +
+ + + Store the TDS specific gravity for future reference and calculations +
+
-
+ -
+
+
+ + + + +
+ + + Store the TDS specific gravity for future reference and calculations +
+
+
+ + + +
diff --git a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js index b3234f3..6c845d3 100644 --- a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js +++ b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js @@ -144,6 +144,7 @@ setIfEmpty('field-curetemp', item.cureTemperatureF, 'Cure Temp'); setIfEmpty('field-curetime', item.cureTimeMinutes, 'Cure Time'); setIfEmpty('field-coverage', item.coverageSqFtPerLb, 'Coverage'); + setIfEmpty('field-specificgravity', item.specificGravity, 'Specific Gravity'); setIfEmpty('field-transfer', item.transferEfficiency,'Transfer Efficiency'); if (item.requiresClearCoat != null) { @@ -246,6 +247,7 @@ setIfEmpty('field-finish', data.finish, 'Finish'); setIfEmpty('field-coverage', data.coverageSqFtPerLb, 'Coverage'); + setIfEmpty('field-specificgravity', data.specificGravity, 'Specific Gravity'); setIfEmpty('field-transfer', data.transferEfficiency, 'Transfer Efficiency'); setIfEmpty('field-curetemp', data.cureTemperatureF, 'Cure Temp'); setIfEmpty('field-curetime', data.cureTimeMinutes, 'Cure Time'); diff --git a/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js b/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js index c3d6a48..91d9ce2 100644 --- a/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js +++ b/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js @@ -446,6 +446,7 @@ setIfEmpty('field-curetemp', data.cureTemperatureF, 'Cure Temp'); setIfEmpty('field-curetime', data.cureTimeMinutes, 'Cure Time'); setIfEmpty('field-coverage', data.coverageSqFtPerLb, 'Coverage'); + setIfEmpty('field-specificgravity', data.specificGravity, 'Specific Gravity'); setIfEmpty('field-transfer', data.transferEfficiency, 'Transfer Efficiency'); if (data.unitPrice > 0) { diff --git a/src/PowderCoating.Web/wwwroot/js/powder-catalog-ai-lookup.js b/src/PowderCoating.Web/wwwroot/js/powder-catalog-ai-lookup.js index 616f49f..127930b 100644 --- a/src/PowderCoating.Web/wwwroot/js/powder-catalog-ai-lookup.js +++ b/src/PowderCoating.Web/wwwroot/js/powder-catalog-ai-lookup.js @@ -89,6 +89,7 @@ fillIfEmpty('field-curetemp', data.cureTemperatureF, 'Cure Temp', filled); fillIfEmpty('field-curetime', data.cureTimeMinutes, 'Cure Time', filled); fillIfEmpty('field-coverage', data.coverageSqFtPerLb, 'Coverage', filled); + fillIfEmpty('field-specificgravity', data.specificGravity, 'Specific Gravity', filled); fillIfEmpty('field-transfer', data.transferEfficiency, 'Transfer Efficiency', filled); fillIfEmpty('field-producturl', data.specPageUrl, 'Product URL', filled); fillIfEmpty('field-imageurl', data.imageUrl, 'Image URL', filled); From 7e0699d5bd2a1682f7b889b1ab121bec0d9b9e1a Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 09:05:00 -0400 Subject: [PATCH 03/15] Add smart install prompt for supported browsers --- .../Views/Shared/_Layout.cshtml | 47 ++++++++ .../wwwroot/js/install-app.js | 107 ++++++++++++++++++ src/PowderCoating.Web/wwwroot/manifest.json | 2 + 3 files changed, 156 insertions(+) create mode 100644 src/PowderCoating.Web/wwwroot/js/install-app.js diff --git a/src/PowderCoating.Web/Views/Shared/_Layout.cshtml b/src/PowderCoating.Web/Views/Shared/_Layout.cshtml index fc7d47e..4a4b72e 100644 --- a/src/PowderCoating.Web/Views/Shared/_Layout.cshtml +++ b/src/PowderCoating.Web/Views/Shared/_Layout.cshtml @@ -351,6 +351,17 @@ gap: 1rem; } + .install-app-btn { + display: inline-flex; + align-items: center; + gap: 0.45rem; + white-space: nowrap; + } + + .install-app-btn i { + font-size: 0.95rem; + } + .user-avatar { width: 40px; height: 40px; @@ -627,6 +638,10 @@ .stat-card i { font-size: 1.25rem !important; } + + .install-app-btn .install-label { + display: none; + } } /* Loading States */ @@ -1434,6 +1449,12 @@
+ +
+ + +
diff --git a/src/PowderCoating.Web/Views/Reports/Landing.cshtml b/src/PowderCoating.Web/Views/Reports/Landing.cshtml index ab58ba7..45422d1 100644 --- a/src/PowderCoating.Web/Views/Reports/Landing.cshtml +++ b/src/PowderCoating.Web/Views/Reports/Landing.cshtml @@ -212,6 +212,14 @@

Detailed breakdown of sales by customer, job type, and period.

Open report
+ +
+ +
+
Sales Tax Report
+

Invoice-basis tax liability: taxable vs non-taxable sales, tax billed by account and month, full invoice detail.

+
Open report
+
diff --git a/src/PowderCoating.Web/Views/Reports/SalesTax.cshtml b/src/PowderCoating.Web/Views/Reports/SalesTax.cshtml new file mode 100644 index 0000000..a7545cd --- /dev/null +++ b/src/PowderCoating.Web/Views/Reports/SalesTax.cshtml @@ -0,0 +1,369 @@ +@model PowderCoating.Application.DTOs.Accounting.SalesTaxReportDto +@{ + ViewData["Title"] = "Sales Tax Report"; + ViewData["PageIcon"] = "bi-percent"; + var today = DateTime.Today; + var ytdFrom = new DateTime(today.Year, 1, 1).ToString("yyyy-MM-dd"); + var ytdTo = today.ToString("yyyy-MM-dd"); + var lastYrFrom = new DateTime(today.Year - 1, 1, 1).ToString("yyyy-MM-dd"); + var lastYrTo = new DateTime(today.Year - 1, 12, 31).ToString("yyyy-MM-dd"); + var thisMonthFrom = new DateTime(today.Year, today.Month, 1).ToString("yyyy-MM-dd"); + var thisMonthTo = today.ToString("yyyy-MM-dd"); + var lastMonthFrom = new DateTime(today.Year, today.Month, 1).AddMonths(-1).ToString("yyyy-MM-dd"); + var lastMonthTo = new DateTime(today.Year, today.Month, 1).AddDays(-1).ToString("yyyy-MM-dd"); + + var monthLabels = Model.ByMonth.Select(m => m.Label).ToList(); + var monthTaxable = Model.ByMonth.Select(m => m.TaxableSales).ToList(); + var monthTaxBilled = Model.ByMonth.Select(m => m.TaxBilled).ToList(); +} + + + + +
+ +

@Model.From.ToString("MMM d") – @Model.To.ToString("MMM d, yyyy") · @(Model.TaxableInvoiceCount + Model.NonTaxableInvoiceCount) invoices

+ +
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + +
+

@Model.CompanyName

+
Sales Tax Liability Report
+

@Model.From.ToString("MMMM d, yyyy") – @Model.To.ToString("MMMM d, yyyy") · Invoice Basis

+
+ + +
+
+
+
+
@Model.TotalTaxBilled.ToString("C")
+
Total Tax Billed
+
+
+
+
+
+
+
@Model.TotalTaxableSales.ToString("C")
+
Taxable Sales
+
@Model.TaxableInvoiceCount invoices
+
+
+
+
+
+
+
@Model.TotalNonTaxableSales.ToString("C")
+
Non-Taxable Sales
+
@Model.NonTaxableInvoiceCount invoices
+
+
+
+
+
+
+
@Model.EffectiveTaxRate.ToString("F2")%
+
Effective Tax Rate
+
on taxable sales
+
+
+
+
+ +@if (!Model.Invoices.Any()) +{ +
+
+ +

No invoices found for this period.

+
+
+} +else +{ +
+ + @if (Model.ByMonth.Count > 1) + { +
+
+
+ Monthly Tax Trend +
+
+ +
+
+
+ } + + +
+
+
By Month
+
+ + + + + + + + + + + @foreach (var m in Model.ByMonth) + { + + + + + + + } + + + + + + + + + +
MonthTaxable SalesTax Billed#
@m.Label@m.TaxableSales.ToString("C")@m.TaxBilled.ToString("C")@m.InvoiceCount
Total@Model.TotalTaxableSales.ToString("C")@Model.TotalTaxBilled.ToString("C")@Model.TaxableInvoiceCount
+
+
+
+
+ + + @if (Model.ByAccount.Any()) + { +
+
By Tax Account
+
+ + + + + + + + + + + + @foreach (var a in Model.ByAccount) + { + var rate = a.TaxableSales == 0 ? 0m : Math.Round(a.TaxBilled / a.TaxableSales * 100, 2); + + + + + + + + } + + + + + + + + + + +
AccountTaxable SalesTax BilledInvoicesEffective Rate
+ @if (!string.IsNullOrEmpty(a.AccountNumber)) + { + @a.AccountNumber + } + @a.AccountName + @a.TaxableSales.ToString("C")@a.TaxBilled.ToString("C")@a.InvoiceCount@rate.ToString("F2")%
Total@Model.TotalTaxableSales.ToString("C")@Model.TotalTaxBilled.ToString("C")@Model.TaxableInvoiceCount
+
+
+ } + + +
+
+ Invoice Detail +
+ @(Model.TaxableInvoiceCount + Model.NonTaxableInvoiceCount) invoices + + + Non-taxable rows shaded + +
+
+
+ + + + + + + + + + + + + + + + + @foreach (var inv in Model.Invoices) + { + bool isTaxable = inv.TaxAmount > 0; + string statusBadge = inv.Status switch + { + "Paid" => "bg-success-subtle text-success", + "PartiallyPaid" => "bg-warning-subtle text-warning", + "Sent" => "bg-info-subtle text-info", + "Overdue" => "bg-danger-subtle text-danger", + _ => "bg-secondary-subtle text-secondary" + }; + + + + + + + + + + + + + } + + + + + + + + + + + + +
InvoiceCustomerDateStatusSubtotalTax %Tax AmountTotalPaidTax Account
+ + @inv.InvoiceNumber + + @inv.CustomerName@inv.InvoiceDate.ToString("MM/dd/yyyy")@inv.Status@inv.SubTotal.ToString("C")@(isTaxable ? inv.TaxPercent.ToString("F2") + "%" : "—")@(isTaxable ? inv.TaxAmount.ToString("C") : "—")@inv.Total.ToString("C")@inv.AmountPaid.ToString("C")@(string.IsNullOrEmpty(inv.TaxAccountName) ? "—" : inv.TaxAccountName)
Totals@(Model.TotalTaxableSales + Model.TotalNonTaxableSales).ToString("C")@Model.TotalTaxBilled.ToString("C")@Model.Invoices.Sum(i => i.Total).ToString("C")@Model.Invoices.Sum(i => i.AmountPaid).ToString("C")
+
+
+} + +
+ + Generated @DateTime.Now.ToString("MMM d, yyyy h:mm tt") · Invoice basis — tax liability is recognized when invoiced, not when collected. Excludes Draft and Voided invoices. +
+ +@if (Model.ByMonth.Count > 1) +{ + @section Scripts { + + + } +} From a8fb56e8ec87ccb8048eafab057fe705c67d79d9 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 12:27:18 -0400 Subject: [PATCH 05/15] Fix company logo missing from PDFs and add AI photo save logging When a tenant uploads a logo it is stored in Azure Blob Storage and LogoData (the legacy DB byte[]) is cleared. All PDF controllers were still reading the now-null LogoData, so logos never appeared on any PDF after upload. Fixed by injecting ICompanyLogoService into all six affected controllers (Quotes, Invoices, Deposits, GiftCertificates, PurchaseOrders, CatalogItems) and loading the blob-stored logo first before falling back to the legacy DB field. Also added structured logging to the AI photo promotion path in QuotesController Create/Edit POST so upload failures are visible in production logs instead of silently swallowed. Added onclick safety net to the Create and Edit quote submit buttons so dynamically-injected hidden fields (AiPhotoTempIds) are written before iOS Safari collects the form data on submit. Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/CatalogItemsController.cs | 21 ++++- .../Controllers/DepositsController.cs | 20 ++++- .../Controllers/GiftCertificatesController.cs | 19 ++++- .../Controllers/InvoicesController.cs | 23 +++++- .../Controllers/PurchaseOrdersController.cs | 19 ++++- .../Controllers/QuotesController.cs | 80 ++++++++++++++++--- .../Views/Quotes/Create.cshtml | 2 +- .../Views/Quotes/Edit.cshtml | 69 +++++++++++++++- 8 files changed, 231 insertions(+), 22 deletions(-) diff --git a/src/PowderCoating.Web/Controllers/CatalogItemsController.cs b/src/PowderCoating.Web/Controllers/CatalogItemsController.cs index 2054ee9..190c64a 100644 --- a/src/PowderCoating.Web/Controllers/CatalogItemsController.cs +++ b/src/PowderCoating.Web/Controllers/CatalogItemsController.cs @@ -40,6 +40,7 @@ namespace PowderCoating.Web.Controllers private readonly ICatalogImageService _catalogImageService; private readonly IAiCatalogPriceCheckService _priceCheckService; private readonly IPlatformSettingsService _platformSettings; + private readonly ICompanyLogoService _logoService; public CatalogItemsController( IUnitOfWork unitOfWork, @@ -52,7 +53,8 @@ namespace PowderCoating.Web.Controllers ISubscriptionService subscriptionService, ICatalogImageService catalogImageService, IAiCatalogPriceCheckService priceCheckService, - IPlatformSettingsService platformSettings) + IPlatformSettingsService platformSettings, + ICompanyLogoService logoService) { _unitOfWork = unitOfWork; _mapper = mapper; @@ -65,6 +67,7 @@ namespace PowderCoating.Web.Controllers _catalogImageService = catalogImageService; _priceCheckService = priceCheckService; _platformSettings = platformSettings; + _logoService = logoService; } /// @@ -906,11 +909,12 @@ namespace PowderCoating.Web.Controllers .ToList(); // Generate PDF + var (logoData, logoContentType) = await LoadCompanyLogoAsync(company); var pdfBytes = await _pdfService.GenerateCatalogPdfAsync( itemsByCategory, company.CompanyName, - company.LogoData, - company.LogoContentType + logoData, + logoContentType ); // Return PDF file @@ -1146,6 +1150,17 @@ namespace PowderCoating.Web.Controllers } return parts.Count > 0 ? string.Join(" > ", parts) : "Uncategorized"; } + + private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company? company) + { + if (company == null) return (null, null); + if (!string.IsNullOrEmpty(company.LogoFilePath)) + { + var (ok, content, contentType, _) = await _logoService.GetCompanyLogoAsync(company.LogoFilePath); + if (ok) return (content, contentType); + } + return (company.LogoData, company.LogoContentType); + } } // Helper class for hierarchical display diff --git a/src/PowderCoating.Web/Controllers/DepositsController.cs b/src/PowderCoating.Web/Controllers/DepositsController.cs index 4d26ab0..b547beb 100644 --- a/src/PowderCoating.Web/Controllers/DepositsController.cs +++ b/src/PowderCoating.Web/Controllers/DepositsController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using PowderCoating.Application.DTOs.Company; +using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Core.Interfaces; @@ -20,15 +21,18 @@ public class DepositsController : Controller private readonly IUnitOfWork _unitOfWork; private readonly UserManager _userManager; private readonly ILogger _logger; + private readonly ICompanyLogoService _logoService; public DepositsController( IUnitOfWork unitOfWork, UserManager userManager, - ILogger logger) + ILogger logger, + ICompanyLogoService logoService) { _unitOfWork = unitOfWork; _userManager = userManager; _logger = logger; + _logoService = logoService; } // ----------------------------------------------------------------------- @@ -191,7 +195,8 @@ public class DepositsController : Controller PrimaryContactEmail = company?.PrimaryContactEmail }; - var pdfBytes = GenerateReceiptPdf(deposit, company?.LogoData, company?.LogoContentType, companyInfo, prefs?.InAccentColor); + var (logoData, logoContentType) = await LoadCompanyLogoAsync(company); + var pdfBytes = GenerateReceiptPdf(deposit, logoData, logoContentType, companyInfo, prefs?.InAccentColor); Response.Headers["Content-Disposition"] = $"inline; filename=\"Deposit-Receipt-{deposit.ReceiptNumber}.pdf\""; return File(pdfBytes, "application/pdf"); } @@ -413,4 +418,15 @@ public class DepositsController : Controller if (string.IsNullOrWhiteSpace(hex)) return fallback; return hex.StartsWith("#") ? hex : fallback; } + + private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company? company) + { + if (company == null) return (null, null); + if (!string.IsNullOrEmpty(company.LogoFilePath)) + { + var (ok, content, contentType, _) = await _logoService.GetCompanyLogoAsync(company.LogoFilePath); + if (ok) return (content, contentType); + } + return (company.LogoData, company.LogoContentType); + } } diff --git a/src/PowderCoating.Web/Controllers/GiftCertificatesController.cs b/src/PowderCoating.Web/Controllers/GiftCertificatesController.cs index 613d97d..3e9e469 100644 --- a/src/PowderCoating.Web/Controllers/GiftCertificatesController.cs +++ b/src/PowderCoating.Web/Controllers/GiftCertificatesController.cs @@ -30,19 +30,22 @@ public class GiftCertificatesController : Controller private readonly ILogger _logger; private readonly UserManager _userManager; private readonly IPdfService _pdfService; + private readonly ICompanyLogoService _logoService; public GiftCertificatesController( IUnitOfWork unitOfWork, IMapper mapper, ILogger logger, UserManager userManager, - IPdfService pdfService) + IPdfService pdfService, + ICompanyLogoService logoService) { _unitOfWork = unitOfWork; _mapper = mapper; _logger = logger; _userManager = userManager; _pdfService = pdfService; + _logoService = logoService; } /// @@ -340,7 +343,8 @@ public class GiftCertificatesController : Controller try { - var pdfBytes = await _pdfService.GenerateGiftCertificatePdfAsync(dto, company?.LogoData, company?.LogoContentType, companyInfo); + var (logoData, logoContentType) = await LoadCompanyLogoAsync(company); + var pdfBytes = await _pdfService.GenerateGiftCertificatePdfAsync(dto, logoData, logoContentType, companyInfo); return File(pdfBytes, "application/pdf", $"GiftCertificate-{cert.CertificateCode}.pdf"); } catch (Exception ex) @@ -390,4 +394,15 @@ public class GiftCertificatesController : Controller list.Insert(0, new SelectListItem { Value = "", Text = "— None (non-customer recipient) —" }); ViewBag.Customers = list; } + + private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company? company) + { + if (company == null) return (null, null); + if (!string.IsNullOrEmpty(company.LogoFilePath)) + { + var (ok, content, contentType, _) = await _logoService.GetCompanyLogoAsync(company.LogoFilePath); + if (ok) return (content, contentType); + } + return (company.LogoData, company.LogoContentType); + } } diff --git a/src/PowderCoating.Web/Controllers/InvoicesController.cs b/src/PowderCoating.Web/Controllers/InvoicesController.cs index 120642e..9c06386 100644 --- a/src/PowderCoating.Web/Controllers/InvoicesController.cs +++ b/src/PowderCoating.Web/Controllers/InvoicesController.cs @@ -27,6 +27,7 @@ public class InvoicesController : Controller private readonly ITenantContext _tenantContext; private readonly INotificationService _notificationService; private readonly IAccountBalanceService _accountBalanceService; + private readonly ICompanyLogoService _logoService; public InvoicesController( IUnitOfWork unitOfWork, @@ -36,7 +37,8 @@ public class InvoicesController : Controller IPdfService pdfService, ITenantContext tenantContext, INotificationService notificationService, - IAccountBalanceService accountBalanceService) + IAccountBalanceService accountBalanceService, + ICompanyLogoService logoService) { _unitOfWork = unitOfWork; _mapper = mapper; @@ -46,6 +48,7 @@ public class InvoicesController : Controller _tenantContext = tenantContext; _notificationService = notificationService; _accountBalanceService = accountBalanceService; + _logoService = logoService; } // ----------------------------------------------------------------------- @@ -1624,8 +1627,9 @@ public class InvoicesController : Controller DefaultTerms = prefs?.InDefaultTerms }; + var (logoData, logoContentType) = await LoadCompanyLogoAsync(company); var dto = await BuildInvoiceDtoAsync(invoice); - return await _pdfService.GenerateInvoicePdfAsync(dto, company?.LogoData, company?.LogoContentType, companyInfo, template); + return await _pdfService.GenerateInvoicePdfAsync(dto, logoData, logoContentType, companyInfo, template); } // ----------------------------------------------------------------------- @@ -2444,4 +2448,19 @@ public class InvoicesController : Controller return false; } + + /// + /// Returns logo bytes and content type for PDF generation. + /// Prefers blob-stored logos (LogoFilePath) over the legacy DB column (LogoData). + /// + private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company? company) + { + if (company == null) return (null, null); + if (!string.IsNullOrEmpty(company.LogoFilePath)) + { + var (ok, content, contentType, _) = await _logoService.GetCompanyLogoAsync(company.LogoFilePath); + if (ok) return (content, contentType); + } + return (company.LogoData, company.LogoContentType); + } } diff --git a/src/PowderCoating.Web/Controllers/PurchaseOrdersController.cs b/src/PowderCoating.Web/Controllers/PurchaseOrdersController.cs index 9e74070..04a0730 100644 --- a/src/PowderCoating.Web/Controllers/PurchaseOrdersController.cs +++ b/src/PowderCoating.Web/Controllers/PurchaseOrdersController.cs @@ -23,19 +23,22 @@ public class PurchaseOrdersController : Controller private readonly UserManager _userManager; private readonly ILogger _logger; private readonly IPdfService _pdfService; + private readonly ICompanyLogoService _logoService; public PurchaseOrdersController( IUnitOfWork unitOfWork, IMapper mapper, UserManager userManager, ILogger logger, - IPdfService pdfService) + IPdfService pdfService, + ICompanyLogoService logoService) { _unitOfWork = unitOfWork; _mapper = mapper; _userManager = userManager; _logger = logger; _pdfService = pdfService; + _logoService = logoService; } // ----------------------------------------------------------------------- @@ -684,8 +687,9 @@ public class PurchaseOrdersController : Controller PrimaryContactEmail = company?.PrimaryContactEmail }; + var (logoData, logoContentType) = await LoadCompanyLogoAsync(company); var pdfBytes = await _pdfService.GeneratePurchaseOrderPdfAsync( - dto, company?.LogoData, company?.LogoContentType, companyInfo); + dto, logoData, logoContentType, companyInfo); return File(pdfBytes, "application/pdf", $"{po.PoNumber}.pdf"); } @@ -847,4 +851,15 @@ public class PurchaseOrdersController : Controller vendors.Insert(0, new SelectListItem("All Vendors", "")); ViewBag.VendorList = vendors; } + + private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company? company) + { + if (company == null) return (null, null); + if (!string.IsNullOrEmpty(company.LogoFilePath)) + { + var (ok, content, contentType, _) = await _logoService.GetCompanyLogoAsync(company.LogoFilePath); + if (ok) return (content, contentType); + } + return (company.LogoData, company.LogoContentType); + } } diff --git a/src/PowderCoating.Web/Controllers/QuotesController.cs b/src/PowderCoating.Web/Controllers/QuotesController.cs index ded5df7..610fff7 100644 --- a/src/PowderCoating.Web/Controllers/QuotesController.cs +++ b/src/PowderCoating.Web/Controllers/QuotesController.cs @@ -40,6 +40,7 @@ public class QuotesController : Controller private readonly IWebHostEnvironment _env; private readonly IJobPhotoService _jobPhotoService; private readonly IAiUsageLogger _usageLogger; + private readonly ICompanyLogoService _logoService; public QuotesController( IUnitOfWork unitOfWork, @@ -59,7 +60,8 @@ public class QuotesController : Controller IAiQuoteService aiService, IWebHostEnvironment env, IJobPhotoService jobPhotoService, - IAiUsageLogger usageLogger) + IAiUsageLogger usageLogger, + ICompanyLogoService logoService) { _unitOfWork = unitOfWork; _mapper = mapper; @@ -79,6 +81,7 @@ public class QuotesController : Controller _env = env; _jobPhotoService = jobPhotoService; _usageLogger = usageLogger; + _logoService = logoService; } /// @@ -604,10 +607,11 @@ public class QuotesController : Controller }; // Generate PDF + var (logoData, logoContentType) = await LoadCompanyLogoAsync(company); var pdfBytes = await _pdfService.GenerateQuotePdfAsync( quoteDto, - company.LogoData, - company.LogoContentType, + logoData, + logoContentType, companyInfo, template: template ); @@ -1037,13 +1041,22 @@ public class QuotesController : Controller await _unitOfWork.CompleteAsync(); // Promote AI temp photos to permanent storage and create QuotePhoto records + _logger.LogInformation("CREATE AI photo promotion: AiPhotoTempIds count={Count}, raw values=[{Values}]", + dto.AiPhotoTempIds?.Count ?? 0, + dto.AiPhotoTempIds == null ? "" : string.Join(",", dto.AiPhotoTempIds)); if (dto.AiPhotoTempIds?.Count > 0) { foreach (var rawTempId in dto.AiPhotoTempIds.Where(t => !string.IsNullOrWhiteSpace(t))) { - if (!Guid.TryParse(rawTempId, out var tempGuid)) continue; + if (!Guid.TryParse(rawTempId, out var tempGuid)) + { + _logger.LogWarning("CREATE AI photo: Guid.TryParse failed for rawTempId={RawTempId}", rawTempId); + continue; + } var tempId = tempGuid.ToString("N"); - var (promoted, photoPath, _) = await _photoService.PromoteTempPhotoAsync(tempId, quote.Id, currentUser.CompanyId); + _logger.LogInformation("CREATE AI photo: promoting tempId={TempId} for quoteId={QuoteId}", tempId, quote.Id); + var (promoted, photoPath, promoteError) = await _photoService.PromoteTempPhotoAsync(tempId, quote.Id, currentUser.CompanyId); + _logger.LogInformation("CREATE AI photo: promoted={Promoted}, path={Path}, error={Error}", promoted, photoPath, promoteError); if (promoted) { var ext = Path.GetExtension(photoPath).ToLowerInvariant(); @@ -1895,13 +1908,22 @@ public class QuotesController : Controller await _unitOfWork.CompleteAsync(); // Promote any new AI temp photos and create QuotePhoto records + _logger.LogInformation("EDIT AI photo promotion: AiPhotoTempIds count={Count}, raw values=[{Values}]", + dto.AiPhotoTempIds?.Count ?? 0, + dto.AiPhotoTempIds == null ? "" : string.Join(",", dto.AiPhotoTempIds)); if (dto.AiPhotoTempIds?.Count > 0) { foreach (var rawTempId in dto.AiPhotoTempIds.Where(t => !string.IsNullOrWhiteSpace(t))) { - if (!Guid.TryParse(rawTempId, out var tempGuid)) continue; + if (!Guid.TryParse(rawTempId, out var tempGuid)) + { + _logger.LogWarning("EDIT AI photo: Guid.TryParse failed for rawTempId={RawTempId}", rawTempId); + continue; + } var tempId = tempGuid.ToString("N"); - var (promoted, photoPath, _) = await _photoService.PromoteTempPhotoAsync(tempId, quote.Id, currentUser.CompanyId); + _logger.LogInformation("EDIT AI photo: promoting tempId={TempId} for quoteId={QuoteId}", tempId, quote.Id); + var (promoted, photoPath, promoteError) = await _photoService.PromoteTempPhotoAsync(tempId, quote.Id, currentUser.CompanyId); + _logger.LogInformation("EDIT AI photo: promoted={Promoted}, path={Path}, error={Error}", promoted, photoPath, promoteError); if (promoted) { var ext = Path.GetExtension(photoPath).ToLowerInvariant(); @@ -2826,10 +2848,11 @@ public class QuotesController : Controller DefaultTerms = prefs?.QtDefaultTerms }; + var (logoData, logoContentType) = await LoadCompanyLogoAsync(company); return await _pdfService.GenerateQuotePdfAsync( quoteDto, - company.LogoData, - company.LogoContentType, + logoData, + logoContentType, companyInfo, template: template); } @@ -3908,6 +3931,30 @@ public class QuotesController : Controller return Json(new { success = true }); } + /// Updates the caption of a non-AI quote photo. + [HttpPost] + [ValidateAntiForgeryToken] + public async Task UpdateQuotePhoto(int id, string? caption) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) return Json(new { success = false, error = "Not authenticated." }); + + var photo = await _unitOfWork.QuotePhotos.GetByIdAsync(id); + if (photo == null || photo.CompanyId != user.CompanyId) + return Json(new { success = false, error = "Photo not found." }); + + if (photo.IsAiAnalysisPhoto) + return Json(new { success = false, error = "AI analysis photos cannot be edited." }); + + photo.Caption = string.IsNullOrWhiteSpace(caption) ? null : caption.Trim(); + photo.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.QuotePhotos.UpdateAsync(photo); + await _unitOfWork.CompleteAsync(); + + return Json(new { success = true }); + } + private async Task GetCompanyPreferencesAsync(int companyId) { return await _unitOfWork.CompanyPreferences.FirstOrDefaultAsync(p => p.CompanyId == companyId && !p.IsDeleted); @@ -3936,6 +3983,21 @@ public class QuotesController : Controller _logger.LogInformation("Recorded first job creation for company {CompanyId}", companyId); } + + /// + /// Returns logo bytes and content type for PDF generation. + /// Prefers blob-stored logos (LogoFilePath) over the legacy DB column (LogoData) + /// so that companies that uploaded a new logo after initial setup see it in PDFs. + /// + private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company company) + { + if (!string.IsNullOrEmpty(company.LogoFilePath)) + { + var (ok, content, contentType, _) = await _logoService.GetCompanyLogoAsync(company.LogoFilePath); + if (ok) return (content, contentType); + } + return (company.LogoData, company.LogoContentType); + } } // Request model for AJAX pricing calculation diff --git a/src/PowderCoating.Web/Views/Quotes/Create.cshtml b/src/PowderCoating.Web/Views/Quotes/Create.cshtml index c936832..7592de9 100644 --- a/src/PowderCoating.Web/Views/Quotes/Create.cshtml +++ b/src/PowderCoating.Web/Views/Quotes/Create.cshtml @@ -398,7 +398,7 @@ Cancel -
diff --git a/src/PowderCoating.Web/Views/Quotes/Edit.cshtml b/src/PowderCoating.Web/Views/Quotes/Edit.cshtml index d1973f0..c2d0c10 100644 --- a/src/PowderCoating.Web/Views/Quotes/Edit.cshtml +++ b/src/PowderCoating.Web/Views/Quotes/Edit.cshtml @@ -363,6 +363,10 @@ style="z-index:1;font-size:.7rem;" data-photo-id="@photo.Id" title="Delete"> + }

@photo.FileName

@photo.CreatedAt.ToString("MMM d, yyyy")

+ @if (!photo.IsAiAnalysisPhoto) + { +

@photo.Caption

+
+ +
+ + +
+
+ }
@@ -398,7 +413,7 @@ Cancel -
@@ -708,6 +723,7 @@ const quoteId = @Model.Id; const uploadUrl = '@Url.Action("UploadQuotePhoto", "Quotes")'; const deleteUrl = '@Url.Action("DeleteQuotePhoto", "Quotes")'; + const updateUrl = '@Url.Action("UpdateQuotePhoto", "Quotes")'; const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? ''; const fileInput = document.getElementById('editPhotoFileInput'); @@ -744,12 +760,24 @@ style="z-index:1;font-size:.7rem;" data-photo-id="${data.id}" title="Delete"> +

${data.fileName}

Just now

+

+
+ +
+ + +
+
`; grid.appendChild(col); @@ -771,6 +799,45 @@ updateCount(-1); }); + // Show inline caption editor + document.addEventListener('click', (e) => { + const btn = e.target.closest('.edit-caption-btn'); + if (!btn) return; + const card = btn.closest('.photo-item'); + card.querySelector('.caption-display')?.classList.add('d-none'); + card.querySelector('.caption-edit-form')?.classList.remove('d-none'); + }); + + // Cancel inline caption edit + document.addEventListener('click', (e) => { + const btn = e.target.closest('.cancel-caption-btn'); + if (!btn) return; + const card = btn.closest('.photo-item'); + card.querySelector('.caption-edit-form')?.classList.add('d-none'); + card.querySelector('.caption-display')?.classList.remove('d-none'); + }); + + // Save caption + document.addEventListener('click', async (e) => { + const btn = e.target.closest('.save-caption-btn'); + if (!btn) return; + const card = btn.closest('.photo-item'); + const editForm = card.querySelector('.caption-edit-form'); + const photoId = editForm?.dataset.photoId; + const caption = card.querySelector('.caption-textarea')?.value.trim() ?? ''; + const fd = new FormData(); + fd.append('id', photoId); + fd.append('caption', caption); + fd.append('__RequestVerificationToken', token); + const resp = await fetch(updateUrl, { method: 'POST', body: fd }); + const data = await resp.json(); + if (!data.success) { alert(data.error || 'Update failed.'); return; } + const displayEl = card.querySelector('.caption-display'); + if (displayEl) { displayEl.textContent = caption; displayEl.title = caption; } + editForm?.classList.add('d-none'); + card.querySelector('.caption-display')?.classList.remove('d-none'); + }); + function updateCount(delta) { const badge = document.getElementById('photoCount'); if (badge) badge.textContent = parseInt(badge.textContent || '0') + delta; From 74414c6c7144dd22ad543514aa43458284da0024 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 12:27:27 -0400 Subject: [PATCH 06/15] Add AI overload retry with model fallback and consolidate wizard errors Anthropic returns overloaded_error (HTTP 529) during high-demand periods. Previously this failed immediately with a generic error. Now the service retries Sonnet once after 5s, then falls back to Haiku (a separate capacity pool) after another 3s before giving up. If all three attempts are overloaded the user sees a clear "high demand" message rather than a generic error. Non-overload errors still log at Error level. Also consolidated AI wizard error display in item-wizard.js: photo upload failures were using browser alert() while analyze failures used the inline red alert bar. All errors now go through aiShowError() so they always appear consistently as the red bar below the Analyze button. Removed the alert() fallback from aiShowError() itself. Co-Authored-By: Claude Sonnet 4.6 --- .../Services/AiQuoteService.cs | 49 ++++++++++- .../wwwroot/js/item-wizard.js | 81 ++++++++++++++----- 2 files changed, 107 insertions(+), 23 deletions(-) diff --git a/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs b/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs index fb79bf0..a75d13c 100644 --- a/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs +++ b/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs @@ -254,8 +254,44 @@ Only ask follow-up questions if truly needed — prefer to make reasonable assum Messages = messages }; - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var response = await client.Messages.GetClaudeMessageAsync(messageRequest, cts.Token); + // On overloaded_error (HTTP 529): retry Sonnet once after a short delay, then + // fall back to Haiku (separate capacity pool). If Haiku is also overloaded, give up. + // Total worst-case added latency before fallback: ~5s. + MessageResponse response; + var modelsToTry = new[] { "claude-sonnet-4-6", "claude-sonnet-4-6", "claude-haiku-4-5-20251001" }; + HttpRequestException? lastOverloadEx = null; + response = null!; + for (int attempt = 0; attempt < modelsToTry.Length; attempt++) + { + messageRequest.Model = modelsToTry[attempt]; + if (attempt > 0) + { + var delay = attempt == 1 ? TimeSpan.FromSeconds(5) : TimeSpan.FromSeconds(3); + _logger.LogWarning("Claude API overloaded on {Model} (attempt {Attempt}); retrying with {NextModel} in {Delay}s", + modelsToTry[attempt - 1], attempt, modelsToTry[attempt], delay.TotalSeconds); + await Task.Delay(delay); + } + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + try + { + response = await client.Messages.GetClaudeMessageAsync(messageRequest, cts.Token); + lastOverloadEx = null; + break; + } + catch (HttpRequestException hex) when (hex.Message.Contains("overloaded_error")) + { + lastOverloadEx = hex; + } + } + if (lastOverloadEx != null) + { + _logger.LogWarning(lastOverloadEx, "Claude API overloaded on all models including fallback"); + return new AiAnalyzeItemResult + { + Success = false, + ErrorMessage = "The AI service is experiencing high demand right now. Please wait a minute and try again." + }; + } var rawText = response.FirstMessage?.Text ?? response.Content.OfType().FirstOrDefault()?.Text ?? ""; @@ -329,6 +365,15 @@ Only ask follow-up questions if truly needed — prefer to make reasonable assum ErrorMessage = "The AI service did not respond in time. Please try again." }; } + catch (HttpRequestException hex) when (hex.Message.Contains("overloaded_error")) + { + _logger.LogWarning(hex, "Claude API overloaded (outer catch — unexpected path)"); + return new AiAnalyzeItemResult + { + Success = false, + ErrorMessage = "The AI service is experiencing high demand right now. Please wait a minute and try again." + }; + } catch (Exception ex) { _logger.LogError(ex, "Error calling Claude AI for quote analysis"); diff --git a/src/PowderCoating.Web/wwwroot/js/item-wizard.js b/src/PowderCoating.Web/wwwroot/js/item-wizard.js index 510c8fa..c97549e 100644 --- a/src/PowderCoating.Web/wwwroot/js/item-wizard.js +++ b/src/PowderCoating.Web/wwwroot/js/item-wizard.js @@ -1184,10 +1184,10 @@ async function aiUploadFile(file) { aiRefreshPhotoList(); document.getElementById('ai_photoError')?.classList.add('d-none'); } else { - alert('Upload failed: ' + (result.error || 'Unknown error')); + aiShowError('Upload failed: ' + (result.error || 'Unknown error')); } } catch (err) { - alert('Upload error: ' + err.message); + aiShowError('Upload error: ' + err.message); } } @@ -1454,24 +1454,49 @@ function aiShowError(message) { el.textContent = message; el.classList.remove('d-none'); el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } else { - // Fallback if element not found - alert('AI Error: ' + message); } } // Step 3: Coating layers function renderStep3Html() { + const isSandblastOnly = !!wz.data.sandblastOnly; return ` -

- - Add one or more coating layers. The first coat uses 100% of the labor estimate; - each additional coat adds 30%. -

-
- `; +
+ + +
+
+

+ + Add one or more coating layers. The first coat uses 100% of the labor estimate; + each additional coat adds 30%. +

+
+ +
+ ${isSandblastOnly ? `
+ + No powder coating — no oven or powder costs will be applied. +
` : ''}`; +} + +function onSandblastOnlyToggle() { + const checked = document.getElementById('sandblastOnlyToggle')?.checked; + wz.data.sandblastOnly = checked; + if (checked) { + wz.data.coats = []; + // AI price was estimated with coating in mind — clear it so pricing recalculates from prep labor + if (wz.itemType === 'ai') { + wz.data.manualUnitPrice = null; + } + } + renderStep(3); } function renderCoatsList() { @@ -1893,8 +1918,9 @@ function renderStep4Html() {
`; } + const isSandblastOnly = !!wz.data.sandblastOnly; const isCatalog = wz.itemType === 'product'; - const isAi = wz.itemType === 'ai'; + const isAi = wz.itemType === 'ai' && !isSandblastOnly; const includePrepCost = wz.data.includePrepCost ?? !isCatalog; // default ON for calculated, OFF for catalog const current = wz.data.prepServices || []; @@ -1917,6 +1943,12 @@ function renderStep4Html() { Select the services below for shop floor reference — they will not add to the item price.
` : ''; + const sandblastBanner = isSandblastOnly ? ` +
+ + Sandblast / Prep Only: estimated minutes will be billed as labor — no powder or oven costs. +
` : ''; + const blastOptions = blastSetupData.length > 0 ? blastSetupData.map(s => ``).join('') : ''; @@ -1960,7 +1992,7 @@ function renderStep4Html() { ? '' : `
Check each prep step needed and enter an estimated time. Labor cost is added to this item's total.
`; - return `${catalogBanner}${aiBanner}${hint}${rows}`; + return `${catalogBanner}${aiBanner}${sandblastBanner}${hint}${rows}`; } function onPrepIncludeCostToggle() { @@ -2274,14 +2306,17 @@ function preFillStep2() { function buildItemFromWizard() { const d = wz.data; - const isAi = wz.itemType === 'ai'; + const isSandblastOnly = !!d.sandblastOnly; + // Sandblast-only AI items lose the AI pricing flag — the AI price included coating costs + // that no longer apply, so the server prices from prep labor instead. + const isAi = wz.itemType === 'ai' && !isSandblastOnly; return { description: d.description || null, quantity: d.quantity || 1, surfaceAreaSqFt: d.surfaceAreaSqFt || 0, estimatedMinutes: d.estimatedMinutes || 0, catalogItemId: d.catalogItemId || null, - manualUnitPrice: d.manualUnitPrice ?? null, + manualUnitPrice: isAi ? (d.manualUnitPrice ?? null) : (d.isGenericItem || d.isSalesItem ? (d.manualUnitPrice ?? null) : null), powderCostOverride: d.powderCostOverride ?? null, isGenericItem: !!d.isGenericItem, isLaborItem: !!d.isLaborItem, @@ -2296,8 +2331,9 @@ function buildItemFromWizard() { prepServices: d.prepServices || [], includePrepCost: d.includePrepCost ?? (wz.itemType !== 'product'), complexity: d.complexity || 'Simple', - aiPhotoTempIds: isAi ? (d.aiPhotoTempIds || []) : [], - aiPhotoFileNames: isAi ? (d.aiPhotoFileNames || []) : [], + // Keep AI photos even for sandblast-only so they get promoted to permanent storage + aiPhotoTempIds: wz.itemType === 'ai' ? (d.aiPhotoTempIds || []) : [], + aiPhotoFileNames: wz.itemType === 'ai' ? (d.aiPhotoFileNames || []) : [], aiTags: isAi ? ((d.aiTags || []).join ? (d.aiTags || []).join(',') : d.aiTags) || null : null, aiPredictionId: isAi ? (d.aiPredictionId ?? null) : null }; @@ -2336,8 +2372,11 @@ function buildCardHtml(item, i) { : { label: 'Custom', cls: 'success', icon: 'bi-rulers' }; const coatCount = item.coats?.length || 0; - const coatBadge = (coatCount > 0) + const isPrepOnly = coatCount === 0 && !item.isGenericItem && !item.isLaborItem && !item.isSalesItem && !item.catalogItemId; + const coatBadge = coatCount > 0 ? `${coatCount} coat${coatCount > 1 ? 's' : ''}` + : isPrepOnly + ? `Prep Only` : ''; const complexityBadge = (!item.isGenericItem && !item.isLaborItem && !item.catalogItemId && item.complexity && item.complexity !== 'Simple') From 2e73cfab549eacf35ac7750d9004955f784259b1 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 12:27:37 -0400 Subject: [PATCH 07/15] Miscellaneous UI and pricing updates from prior sessions - PricingCalculationService: powder coverage and specific gravity math fixes - Dashboard/Index: minor widget updates - Jobs/Details, Jobs/Intake: shop floor and intake view improvements - Quotes/Details: detail view updates - GiftCertificates/Details: detail view update - job-photos.js: photo gallery improvements Co-Authored-By: Claude Sonnet 4.6 --- .../Services/PricingCalculationService.cs | 33 ++++--- .../Views/Dashboard/Index.cshtml | 4 +- .../Views/GiftCertificates/Details.cshtml | 2 +- .../Views/Jobs/Details.cshtml | 54 +++++++++--- .../Views/Jobs/Intake.cshtml | 2 +- .../Views/Quotes/Details.cshtml | 85 ++++++++++++++++--- .../wwwroot/js/job-photos.js | 84 ++++++++++++++++++ 7 files changed, 226 insertions(+), 38 deletions(-) diff --git a/src/PowderCoating.Application/Services/PricingCalculationService.cs b/src/PowderCoating.Application/Services/PricingCalculationService.cs index 8c8e2a1..ff16d20 100644 --- a/src/PowderCoating.Application/Services/PricingCalculationService.cs +++ b/src/PowderCoating.Application/Services/PricingCalculationService.cs @@ -422,12 +422,14 @@ public class PricingCalculationService : IPricingCalculationService else { // Non-catalog: derive base from first coat's material + labor + equipment + markup + decimal coatLaborCost = 0m; // coat-only labor, used for coating booth (not prep/sandblast) if (item.Coats != null && item.Coats.Count > 0) { var firstCoatResult = await CalculateCoatPriceAsync( item.Coats[0], item.SurfaceAreaSqFt, item.Quantity, 0, item.EstimatedMinutes, companyId); totalMaterialCost = firstCoatResult.CoatMaterialCost; - totalLaborCost = firstCoatResult.CoatLaborCost; + coatLaborCost = firstCoatResult.CoatLaborCost; + totalLaborCost = coatLaborCost; } // Prep service labor (done once per item batch) @@ -443,9 +445,10 @@ public class PricingCalculationService : IPricingCalculationService // Consumables surcharge (5% of material) totalMaterialCost += totalMaterialCost * ConsumablesSurchargePercent; - // Equipment cost: coating booth only (oven cost moved to quote-level batch calculation) - var totalLaborHours = totalLaborCost / costs.StandardLaborRate; - totalEquipmentCost = totalLaborHours * costs.CoatingBoothCostPerHour; + // Equipment cost: coating booth only — use coat labor hours, not prep/sandblast hours + // (sandblasting happens in a blast cabinet, not the powder coating booth) + var coatLaborHours = costs.StandardLaborRate > 0 ? coatLaborCost / costs.StandardLaborRate : 0m; + totalEquipmentCost = coatLaborHours * costs.CoatingBoothCostPerHour; // Apply pricing mode: markup on material only, or target margin on total cost if (costs.PricingMode == PowderCoating.Core.Enums.PricingMode.MarginOnTotalCost) @@ -675,22 +678,24 @@ public class PricingCalculationService : IPricingCalculationService var effectiveBatches = Math.Max(1, ovenBatches); var fullOvenBatchCost = effectiveBatches * (effectiveCycleMinutes / 60m) * effectiveOvenRate; - // Scale oven cost by the fraction of total surface area coming from non-AI items. - // Use item count as a fallback when surface areas are all zero. - var totalSqFt = items.Sum(i => i.SurfaceAreaSqFt * i.Quantity); - var aiSqFt = items.Where(i => i.IsAiItem).Sum(i => i.SurfaceAreaSqFt * i.Quantity); - var nonAiSqFt = totalSqFt - aiSqFt; + // Only items with coating layers go in the oven — sandblast/prep-only items (zero coats) don't. + // Of those coating items, AI items already have oven cost baked into their AI price. + var coatingItems = items.Where(i => i.Coats != null && i.Coats.Any()).ToList(); + var nonAiCoatItems = coatingItems.Where(i => !i.IsAiItem).ToList(); decimal nonAiFraction; - if (totalSqFt > 0) + if (!coatingItems.Any()) { - nonAiFraction = nonAiSqFt / totalSqFt; + nonAiFraction = 0m; // No coated items — no oven charge } else { - var totalCount = items.Count; - var aiCount = items.Count(i => i.IsAiItem); - nonAiFraction = totalCount > 0 ? (decimal)(totalCount - aiCount) / totalCount : 1m; + var totalCoatSqFt = coatingItems.Sum(i => i.SurfaceAreaSqFt * i.Quantity); + var nonAiCoatSqFt = nonAiCoatItems.Sum(i => i.SurfaceAreaSqFt * i.Quantity); + if (totalCoatSqFt > 0) + nonAiFraction = nonAiCoatSqFt / totalCoatSqFt; + else + nonAiFraction = coatingItems.Count > 0 ? (decimal)nonAiCoatItems.Count / coatingItems.Count : 1m; } var ovenBatchCost = fullOvenBatchCost * nonAiFraction; diff --git a/src/PowderCoating.Web/Views/Dashboard/Index.cshtml b/src/PowderCoating.Web/Views/Dashboard/Index.cshtml index 46cdb0e..69dc8bd 100644 --- a/src/PowderCoating.Web/Views/Dashboard/Index.cshtml +++ b/src/PowderCoating.Web/Views/Dashboard/Index.cshtml @@ -479,7 +479,7 @@ Powder in Queue to be Ordered @Model.PowderOrdersNeededCount item@(Model.PowderOrdersNeededCount == 1 ? "" : "s") - Grouped by vendor · Mark lines as ordered to remove them + Grouped by vendor · Mark lines as ordered to remove them
@foreach (var vendorGroup in Model.PowderOrdersNeeded) @@ -574,7 +574,7 @@ Powder Ordered — Awaiting Receipt @Model.PowderOrdersPlacedCount item@(Model.PowderOrdersPlacedCount == 1 ? "" : "s") - Grouped by vendor · Enter lbs received to update inventory + Grouped by vendor · Enter lbs received to update inventory
@if (Model.PowderOrdersPlaced.Any()) diff --git a/src/PowderCoating.Web/Views/GiftCertificates/Details.cshtml b/src/PowderCoating.Web/Views/GiftCertificates/Details.cshtml index d4b1ff9..e6a42d5 100644 --- a/src/PowderCoating.Web/Views/GiftCertificates/Details.cshtml +++ b/src/PowderCoating.Web/Views/GiftCertificates/Details.cshtml @@ -38,7 +38,7 @@ } @if (Model.ExpiryDate.HasValue) { - · Expires @Model.ExpiryDate.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMMM d, yyyy") + · Expires @Model.ExpiryDate.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMMM d, yyyy") }
diff --git a/src/PowderCoating.Web/Views/Jobs/Details.cshtml b/src/PowderCoating.Web/Views/Jobs/Details.cshtml index 99f97e2..9729cde 100644 --- a/src/PowderCoating.Web/Views/Jobs/Details.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/Details.cshtml @@ -366,7 +366,7 @@ } @if (!string.IsNullOrEmpty(coat.Finish)) { - · @coat.Finish + · @coat.Finish } @if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0) { @@ -493,7 +493,7 @@ } @if (!string.IsNullOrEmpty(coat.Finish)) { - · @coat.Finish + · @coat.Finish } @if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0) { @@ -1680,12 +1680,46 @@

Uploaded:

By:

+ +
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -1747,7 +1781,7 @@ @item.Description @if (item.Quantity > 1) { - ×@item.Quantity + ×@item.Quantity } @@ -1940,7 +1974,7 @@
- Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144") + Formula: L × W ÷ @(ViewBag.UseMetric == true ? "10,000" : "144")
@@ -782,13 +801,15 @@ url = Url.Action("Photo", "Quotes", new { id = p.Id }), fileName = p.FileName, date = p.CreatedAt.ToString("MMM d, yyyy"), - isAi = p.IsAiAnalysisPhoto + isAi = p.IsAiAnalysisPhoto, + caption = p.Caption }))); let currentIndex = 0; const quoteId = @Model.Id; const uploadUrl = '@Url.Action("UploadQuotePhoto", "Quotes")'; const deleteUrl = '@Url.Action("DeleteQuotePhoto", "Quotes")'; + const updateUrl = '@Url.Action("UpdateQuotePhoto", "Quotes")'; const token = () => document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? ''; function render() { @@ -799,8 +820,14 @@ document.getElementById('qpFileName').textContent = p.fileName; document.getElementById('qpPosition').textContent = `Photo ${currentIndex + 1} of ${photos.length}`; document.getElementById('qpDeleteBtn').style.display = p.isAi ? 'none' : ''; + document.getElementById('qpEditBtn').style.display = p.isAi ? 'none' : ''; document.getElementById('qpPrev').disabled = photos.length <= 1; document.getElementById('qpNext').disabled = photos.length <= 1; + const captionRow = document.getElementById('qpCaptionRow'); + if (captionRow) { + document.getElementById('qpCaption').textContent = p.caption || ''; + captionRow.style.display = p.caption ? '' : 'none'; + } } function open(index) { @@ -810,10 +837,42 @@ } function navigate(dir) { + const editPanel = document.getElementById('qpEditPanel'); + if (editPanel && !editPanel.classList.contains('d-none')) cancelEdit(); currentIndex = (currentIndex + dir + photos.length) % photos.length; render(); } + function editPhoto() { + document.getElementById('qpEditCaption').value = photos[currentIndex].caption || ''; + document.getElementById('qpCaptionRow').style.display = 'none'; + document.getElementById('qpEditPanel').classList.remove('d-none'); + document.getElementById('qpViewButtons').classList.add('d-none'); + document.getElementById('qpEditButtons').classList.remove('d-none'); + } + + function cancelEdit() { + document.getElementById('qpEditPanel').classList.add('d-none'); + document.getElementById('qpCaptionRow').style.display = photos[currentIndex].caption ? '' : 'none'; + document.getElementById('qpEditButtons').classList.add('d-none'); + document.getElementById('qpViewButtons').classList.remove('d-none'); + } + + async function saveEdit() { + const p = photos[currentIndex]; + const caption = document.getElementById('qpEditCaption').value.trim(); + const fd = new FormData(); + fd.append('id', p.id); + fd.append('caption', caption); + fd.append('__RequestVerificationToken', token()); + const resp = await fetch(updateUrl, { method: 'POST', body: fd }); + const data = await resp.json(); + if (!data.success) { alert(data.error || 'Update failed.'); return; } + p.caption = caption || null; + cancelEdit(); + render(); + } + async function deletePhoto() { if (!confirm('Delete this photo?')) return; const p = photos[currentIndex]; @@ -864,7 +923,7 @@ if (!data.success) { alert(data.error || 'Upload failed.'); return; } const newIndex = photos.length; - photos.push({ id: data.id, url: data.url, fileName: data.fileName, date: 'Just now', isAi: false }); + photos.push({ id: data.id, url: data.url, fileName: data.fileName, date: 'Just now', isAi: false, caption: null }); document.getElementById('noPhotosMsg')?.remove(); const grid = document.getElementById('photoGrid'); @@ -884,7 +943,13 @@ if (badge) badge.textContent = parseInt(badge.textContent || '0') + delta; } - return { open, navigate, deletePhoto }; + // Reset to view mode whenever the modal is closed + document.getElementById('qpModal')?.addEventListener('hidden.bs.modal', () => { + const editPanel = document.getElementById('qpEditPanel'); + if (editPanel && !editPanel.classList.contains('d-none')) cancelEdit(); + }); + + return { open, navigate, deletePhoto, editPhoto, cancelEdit, saveEdit }; })(); } @@ -920,7 +985,7 @@ Oven (@Model.PricingBreakdown.OvenBatches batch@(Model.PricingBreakdown.OvenBatches != 1 ? "es" : "") - × @Model.PricingBreakdown.OvenCycleMinutes min): + × @Model.PricingBreakdown.OvenCycleMinutes min): @Model.PricingBreakdown.OvenBatchCost.ToString("C")
@@ -1225,7 +1290,7 @@ { @if (item.SurfaceAreaSqFt > 0) { @item.SurfaceAreaSqFt.ToString("F2") sqft } - @if (item.SurfaceAreaSqFt > 0 && item.EstimatedMinutes > 0) { · } + @if (item.SurfaceAreaSqFt > 0 && item.EstimatedMinutes > 0) { · } @if (item.EstimatedMinutes > 0) { @item.EstimatedMinutes min } } @@ -1233,7 +1298,7 @@
@if (item.Quantity > 1) { - ×@item.Quantity.ToString("G29") + ×@item.Quantity.ToString("G29") } @item.TotalPrice.ToString("C")
diff --git a/src/PowderCoating.Web/wwwroot/js/job-photos.js b/src/PowderCoating.Web/wwwroot/js/job-photos.js index 632a36b..f0c71bf 100644 --- a/src/PowderCoating.Web/wwwroot/js/job-photos.js +++ b/src/PowderCoating.Web/wwwroot/js/job-photos.js @@ -4,6 +4,7 @@ const jobPhotoModule = { allPhotos: [], currentPhotoIndex: 0, _tagApi: null, + _editTagApi: null, init: function(jobId, tagSuggestions) { this.jobId = jobId; @@ -21,6 +22,17 @@ const jobPhotoModule = { }); }); } + + // Reset to view mode when the view modal closes + const viewModal = document.getElementById('viewPhotoModal'); + if (viewModal) { + viewModal.addEventListener('hidden.bs.modal', () => { + const editPanel = document.getElementById('photoEditPanel'); + if (editPanel && !editPanel.classList.contains('d-none')) { + this.cancelPhotoEdit(); + } + }); + } }, loadJobPhotos: function() { @@ -119,6 +131,11 @@ const jobPhotoModule = { }, navigatePhoto: function(direction) { + // Exit edit mode before navigating to another photo + const editPanel = document.getElementById('photoEditPanel'); + if (editPanel && !editPanel.classList.contains('d-none')) { + this.cancelPhotoEdit(); + } this.currentPhotoIndex += direction; if (this.currentPhotoIndex < 0) this.currentPhotoIndex = this.allPhotos.length - 1; if (this.currentPhotoIndex >= this.allPhotos.length) this.currentPhotoIndex = 0; @@ -212,6 +229,73 @@ const jobPhotoModule = { }); }, + editPhoto: function() { + const photo = this.allPhotos[this.currentPhotoIndex]; + document.getElementById('editPhotoType').value = photo.photoType; + document.getElementById('editPhotoCaption').value = photo.caption || ''; + // Pre-populate tag hidden field so initTagInput picks it up + document.getElementById('editPhotoTagsHidden').value = photo.tags || ''; + this._editTagApi = initTagInput('editPhotoTagsHidden', 'editPhotoTagsContainer', { + suggestions: this._tagSuggestions + }); + document.getElementById('photoDetails').classList.add('d-none'); + document.getElementById('photoEditPanel').classList.remove('d-none'); + document.getElementById('viewModeButtons').classList.add('d-none'); + document.getElementById('editModeButtons').classList.remove('d-none'); + }, + + cancelPhotoEdit: function() { + document.getElementById('photoEditPanel').classList.add('d-none'); + document.getElementById('photoDetails').classList.remove('d-none'); + document.getElementById('editModeButtons').classList.add('d-none'); + document.getElementById('viewModeButtons').classList.remove('d-none'); + }, + + savePhotoEdit: function() { + const photo = this.allPhotos[this.currentPhotoIndex]; + const photoType = parseInt(document.getElementById('editPhotoType').value, 10); + const caption = document.getElementById('editPhotoCaption').value.trim(); + const tags = document.getElementById('editPhotoTagsHidden').value; + const token = document.querySelector('input[name="__RequestVerificationToken"]').value; + + fetch('/Jobs/UpdatePhoto', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'RequestVerificationToken': token + }, + body: JSON.stringify({ + id: photo.id, + caption: caption, + photoType: photoType, + tags: tags, + displayOrder: photo.displayOrder || 0 + }) + }) + .then(r => r.json()) + .then(data => { + if (data.success) { + photo.caption = caption || null; + photo.photoType = photoType; + photo.photoTypeDisplay = this.getPhotoTypeLabel(photoType); + photo.tags = tags || null; + photo.tagsList = tags ? tags.split(',').map(t => t.trim()).filter(t => t) : []; + this.cancelPhotoEdit(); + this.viewPhoto(this.currentPhotoIndex, true); + this.renderPhotoGallery(this.allPhotos); + this.showToast('Photo updated successfully', 'success'); + } else { + this.showToast(data.message || 'Error updating photo', 'danger'); + } + }) + .catch(() => this.showToast('An error occurred while updating', 'danger')); + }, + + getPhotoTypeLabel: function(type) { + const labels = { 0: 'Before', 1: 'Progress', 2: 'After', 3: 'Quality Check', 4: 'Issue', 5: 'Completed' }; + return labels[type] || 'Unknown'; + }, + setupDragDrop: function() { const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('photoFile'); From 71caa934616568e81bfa6bba3f3e1915da6658f5 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 13:38:18 -0400 Subject: [PATCH 08/15] Fix unit test build failures after logo service and pricing changes DepositsController and GiftCertificatesController gained a required ICompanyLogoService constructor parameter in the PDF logo fix; their test factories were not updated and failed to compile on Jenkins. Added Mock.Of() to both factory methods and the missing using directive to DepositsControllerTests. PricingCalculationService now only charges oven cost for items that have explicit coating layers (Coats collection non-empty), because sandblast/prep-only and labor items do not go in the oven. Two tests that tested the old "all items count toward oven fraction" logic were updated to include a single coat entry on each item, which restores the expected oven fraction math without changing the tested behaviour. Co-Authored-By: Claude Sonnet 4.6 --- .../DepositsControllerTests.cs | 4 +++- .../GiftCertificatesControllerTests.cs | 3 ++- .../PricingCalculationServiceTests.cs | 12 ++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/PowderCoating.UnitTests/DepositsControllerTests.cs b/tests/PowderCoating.UnitTests/DepositsControllerTests.cs index a2931bd..64b08b0 100644 --- a/tests/PowderCoating.UnitTests/DepositsControllerTests.cs +++ b/tests/PowderCoating.UnitTests/DepositsControllerTests.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; +using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Infrastructure.Data; @@ -290,7 +291,8 @@ public class DepositsControllerTests var controller = new DepositsController( uow, userManager.Object, - Mock.Of>()); + Mock.Of>(), + Mock.Of()); controller.ControllerContext = new ControllerContext { diff --git a/tests/PowderCoating.UnitTests/GiftCertificatesControllerTests.cs b/tests/PowderCoating.UnitTests/GiftCertificatesControllerTests.cs index 8b05d6f..d6f5ed2 100644 --- a/tests/PowderCoating.UnitTests/GiftCertificatesControllerTests.cs +++ b/tests/PowderCoating.UnitTests/GiftCertificatesControllerTests.cs @@ -253,7 +253,8 @@ public class GiftCertificatesControllerTests Mock.Of(), Mock.Of>(), userManager.Object, - Mock.Of()); + Mock.Of(), + Mock.Of()); controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; controller.TempData = new TempDataDictionary(httpContext, Mock.Of()); diff --git a/tests/PowderCoating.UnitTests/PricingCalculationServiceTests.cs b/tests/PowderCoating.UnitTests/PricingCalculationServiceTests.cs index 55a1cec..86fcea1 100644 --- a/tests/PowderCoating.UnitTests/PricingCalculationServiceTests.cs +++ b/tests/PowderCoating.UnitTests/PricingCalculationServiceTests.cs @@ -417,7 +417,8 @@ public class PricingCalculationServiceTests IsAiItem = true, ManualUnitPrice = 200m, Quantity = 1m, - SurfaceAreaSqFt = 50m + SurfaceAreaSqFt = 50m, + Coats = new List { new() { CoatName = "Base Coat", Sequence = 1 } } }, new() { @@ -425,7 +426,8 @@ public class PricingCalculationServiceTests IsLaborItem = true, Quantity = 1m, SurfaceAreaSqFt = 50m, - EstimatedMinutes = 60 + EstimatedMinutes = 60, + Coats = new List { new() { CoatName = "Base Coat", Sequence = 1 } } } }; @@ -470,14 +472,16 @@ public class PricingCalculationServiceTests Description = "AI item", IsAiItem = true, ManualUnitPrice = 100m, - Quantity = 1m + Quantity = 1m, + Coats = new List { new() { CoatName = "Base Coat", Sequence = 1 } } }, new() { Description = "Shelf item", IsSalesItem = true, ManualUnitPrice = 40m, - Quantity = 1m + Quantity = 1m, + Coats = new List { new() { CoatName = "Base Coat", Sequence = 1 } } } }; From 63a85b6ce9830f519925b6650b80b8da4e15f002 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 13:56:20 -0400 Subject: [PATCH 09/15] Fix sandblast-only oven charge and wizard overflow; fix Jenkins test failures Sandblast-only oven charge root cause: renderCoatsList() is called on every step-3 render. When the sandblast-only toggle was checked it cleared wz.data.coats to [], but renderCoatsList() then saw an empty list and auto-called addCoatRow(), silently pushing a Base Coat back into wz.data.coats. The server saw Coats.Any() = true and included the item in the oven fraction calculation, producing an unexpected oven batch charge. Fixed by bailing out of renderCoatsList() when sandblastOnly is active, and added a matching safety net in buildItemFromWizard() that forces coats:[] when sandblastOnly is true. Also fixed sandblast-only toggle label overflow: subtitle span changed to d-block so it wraps beneath the bold label instead of running inline. Test fixes: - DepositsController and GiftCertificatesController tests updated with the required ICompanyLogoService mock parameter added to the logo fix commit. - Two PricingCalculationServiceTests updated to include a coat entry on each item, matching the service's updated requirement that only items with coating layers are considered for oven fraction calculation. Co-Authored-By: Claude Sonnet 4.6 --- src/PowderCoating.Web/wwwroot/js/item-wizard.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PowderCoating.Web/wwwroot/js/item-wizard.js b/src/PowderCoating.Web/wwwroot/js/item-wizard.js index c97549e..6e76048 100644 --- a/src/PowderCoating.Web/wwwroot/js/item-wizard.js +++ b/src/PowderCoating.Web/wwwroot/js/item-wizard.js @@ -1464,9 +1464,9 @@ function renderStep3Html() {
-
@@ -1500,6 +1500,8 @@ function onSandblastOnlyToggle() { } function renderCoatsList() { + // Sandblast-only items have no coating layers — don't auto-add a coat + if (wz.data.sandblastOnly) return; const coats = wz.data.coats || []; if (coats.length === 0) { addCoatRow(); // auto-add a Base Coat @@ -2327,7 +2329,7 @@ function buildItemFromWizard() { requiresSandblasting: false, requiresMasking: false, notes: d.notes || null, - coats: d.coats || [], + coats: isSandblastOnly ? [] : (d.coats || []), prepServices: d.prepServices || [], includePrepCost: d.includePrepCost ?? (wz.itemType !== 'product'), complexity: d.complexity || 'Simple', From 0054b7d10811e4d0db8d679045490ae9b4620927 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 14:16:24 -0400 Subject: [PATCH 10/15] Fix NullReferenceException on Quote Details when quote total is zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PricingBreakdown was only populated when quote.Total > 0, but the Details view unconditionally dereferences PricingBreakdown.ItemsSubtotal. Sandblast- only quotes can legitimately have a $0 total (no powder/oven costs), leaving PricingBreakdown null and crashing the Details render. Removed the Total > 0 guard from both Details action overloads — always populate PricingBreakdown from the stored snapshot fields (all values are 0 for an unpriced or sandblast-only quote, which is safe for display). Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/QuotesController.cs | 98 +++++++++---------- 1 file changed, 46 insertions(+), 52 deletions(-) diff --git a/src/PowderCoating.Web/Controllers/QuotesController.cs b/src/PowderCoating.Web/Controllers/QuotesController.cs index 610fff7..17e5d17 100644 --- a/src/PowderCoating.Web/Controllers/QuotesController.cs +++ b/src/PowderCoating.Web/Controllers/QuotesController.cs @@ -365,33 +365,30 @@ public class QuotesController : Controller } // Build pricing breakdown from stored snapshot values — never recalculate on load - if (quote.Total > 0) + quoteDto.PricingBreakdown = new QuotePricingBreakdownDto { - quoteDto.PricingBreakdown = new QuotePricingBreakdownDto - { - MaterialCosts = quote.MaterialCosts, - LaborCosts = quote.LaborCosts, - EquipmentCosts = quote.EquipmentCosts, - ItemsSubtotal = quote.ItemsSubtotal, - OvenBatchCost = quote.OvenBatchCost, - OvenBatches = quote.OvenBatches, - OvenCycleMinutes = quote.OvenCycleMinutes ?? 0, - ShopSuppliesAmount = quote.ShopSuppliesAmount, - ShopSuppliesPercent = quote.ShopSuppliesPercent, - OverheadCosts = quote.OverheadAmount, - OverheadPercent = quote.OverheadPercent, - ProfitMargin = quote.ProfitMargin, - ProfitPercent = quote.ProfitPercent, - SubtotalBeforeDiscount = quote.SubTotal, - DiscountAmount = quote.DiscountAmount, - DiscountPercent = quote.DiscountPercent, - SubtotalAfterDiscount = quote.SubTotal - quote.DiscountAmount, - RushFee = quote.RushFee, - TaxPercent = quote.TaxPercent, - TaxAmount = quote.TaxAmount, - Total = quote.Total - }; - } + MaterialCosts = quote.MaterialCosts, + LaborCosts = quote.LaborCosts, + EquipmentCosts = quote.EquipmentCosts, + ItemsSubtotal = quote.ItemsSubtotal, + OvenBatchCost = quote.OvenBatchCost, + OvenBatches = quote.OvenBatches, + OvenCycleMinutes = quote.OvenCycleMinutes ?? 0, + ShopSuppliesAmount = quote.ShopSuppliesAmount, + ShopSuppliesPercent = quote.ShopSuppliesPercent, + OverheadCosts = quote.OverheadAmount, + OverheadPercent = quote.OverheadPercent, + ProfitMargin = quote.ProfitMargin, + ProfitPercent = quote.ProfitPercent, + SubtotalBeforeDiscount = quote.SubTotal, + DiscountAmount = quote.DiscountAmount, + DiscountPercent = quote.DiscountPercent, + SubtotalAfterDiscount = quote.SubTotal - quote.DiscountAmount, + RushFee = quote.RushFee, + TaxPercent = quote.TaxPercent, + TaxAmount = quote.TaxAmount, + Total = quote.Total + }; // Load change history var changeHistories = await _unitOfWork.Quotes.GetChangeHistoryAsync(id.Value); @@ -544,33 +541,30 @@ public class QuotesController : Controller var currentUser = await _userManager.GetUserAsync(User); // Populate pricing breakdown from stored snapshot values — never recalculate on load - if (quote.Total > 0) + quoteDto.PricingBreakdown = new QuotePricingBreakdownDto { - quoteDto.PricingBreakdown = new QuotePricingBreakdownDto - { - MaterialCosts = quote.MaterialCosts, - LaborCosts = quote.LaborCosts, - EquipmentCosts = quote.EquipmentCosts, - ItemsSubtotal = quote.ItemsSubtotal, - OvenBatchCost = quote.OvenBatchCost, - OvenBatches = quote.OvenBatches, - OvenCycleMinutes = quote.OvenCycleMinutes ?? 0, - ShopSuppliesAmount = quote.ShopSuppliesAmount, - ShopSuppliesPercent = quote.ShopSuppliesPercent, - OverheadCosts = quote.OverheadAmount, - OverheadPercent = quote.OverheadPercent, - ProfitMargin = quote.ProfitMargin, - ProfitPercent = quote.ProfitPercent, - SubtotalBeforeDiscount = quote.SubTotal, - DiscountAmount = quote.DiscountAmount, - DiscountPercent = quote.DiscountPercent, - SubtotalAfterDiscount = quote.SubTotal - quote.DiscountAmount, - RushFee = quote.RushFee, - TaxPercent = quote.TaxPercent, - TaxAmount = quote.TaxAmount, - Total = quote.Total - }; - } + MaterialCosts = quote.MaterialCosts, + LaborCosts = quote.LaborCosts, + EquipmentCosts = quote.EquipmentCosts, + ItemsSubtotal = quote.ItemsSubtotal, + OvenBatchCost = quote.OvenBatchCost, + OvenBatches = quote.OvenBatches, + OvenCycleMinutes = quote.OvenCycleMinutes ?? 0, + ShopSuppliesAmount = quote.ShopSuppliesAmount, + ShopSuppliesPercent = quote.ShopSuppliesPercent, + OverheadCosts = quote.OverheadAmount, + OverheadPercent = quote.OverheadPercent, + ProfitMargin = quote.ProfitMargin, + ProfitPercent = quote.ProfitPercent, + SubtotalBeforeDiscount = quote.SubTotal, + DiscountAmount = quote.DiscountAmount, + DiscountPercent = quote.DiscountPercent, + SubtotalAfterDiscount = quote.SubTotal - quote.DiscountAmount, + RushFee = quote.RushFee, + TaxPercent = quote.TaxPercent, + TaxAmount = quote.TaxAmount, + Total = quote.Total + }; if (currentUser?.CompanyId == null) { TempData["Error"] = "Company information not found."; From ecb285657a8dca313d87a574b666117399ae36d6 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 6 May 2026 14:29:03 -0400 Subject: [PATCH 11/15] Fix sandblast-only toggle overflow and $0 AI quote pricing Overflow: replaced Bootstrap form-check with an explicit flex row so the two-line label (title + subtitle) never bleeds outside the card boundary. $0 pricing: when sandblast-only was toggled on an AI item, manualUnitPrice was cleared and isAiItem set to false. The pricing engine then returned $0 because no prep services with minutes were configured. Fix: preserve the AI price when toggling sandblast-only, and keep isAiItem=true so the server routes through the AI-price path (manualUnitPrice) rather than trying to recalculate from prep labor. Co-Authored-By: Claude Sonnet 4.6 --- .../wwwroot/js/item-wizard.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/PowderCoating.Web/wwwroot/js/item-wizard.js b/src/PowderCoating.Web/wwwroot/js/item-wizard.js index 6e76048..bacfc82 100644 --- a/src/PowderCoating.Web/wwwroot/js/item-wizard.js +++ b/src/PowderCoating.Web/wwwroot/js/item-wizard.js @@ -1461,10 +1461,12 @@ function aiShowError(message) { function renderStep3Html() { const isSandblastOnly = !!wz.data.sandblastOnly; return ` -
- -