- Store complete PricingBreakdownJson snapshot on Job at every save point so
the Details page reads stored data rather than re-running the pricing engine
- Add 7 missing fields to Quote entity (FacilityOverheadCost, tier/quote discounts,
SubtotalAfterDiscount) and persist them via ApplyPricingSnapshot
- Fix OvenCostId-as-rate bug in JobsController (FK was passed as decimal $/hr)
- Fix hardcoded LaborCost * 0.4 multiplier in two JobItemAssemblyService overloads
- Fix FacilityOverheadCost dropped from invoices in both quote and direct-job paths
- Fix RushFee missing from direct-job invoices (read from PricingBreakdownJson)
- Fix Notes and CatalogItemId not copied to InvoiceItem
- Add 14 unit tests in PricingStageFlowTests covering all three pricing stages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Carry OvenBatches/OvenCycleMinutes from Quote → Job entity (was missing fields; all job pricing recalcs hardcoded 1/null)
- Fix invoice creation from job always showing Quantity=1 (was using TotalPrice as UnitPrice with qty 1)
- Add IsAiItem to JobItem + migration; map in all 3 JobItemAssemblyService.CreateJobItem overloads so AI photo jobs no longer double-price on first edit after quote→job conversion
- Propagate IsAiItem through all existingItemsData JSON blocks in Jobs views (Edit, EditItems, Create) so the wizard preserves AI routing on re-edit
- Add PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem structural test + 3 behavioral IsAiItem tests to JobItemAssemblyServiceTests
- Consolidate item wizard partials (_ItemWizardModal, _SqFtCalculatorModal) and item-wizard.css into shared locations
- Document pricing flag propagation checklist in CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1 — Invoice total didn't match job total for direct jobs:
- Root cause: all three item-save paths in JobsController passed null for
ovenCostId, so FinalPrice/ShopSuppliesAmount were stored without oven cost
while the Details page recalculated live with OvenCostId and showed higher.
- Add OvenBatchCost stored field to Job entity (migration AddJobOvenBatchCost,
default 0 for existing rows).
- Fix Create, Edit, and UpdateItems to pass job.OvenCostId and save OvenBatchCost.
- Fix InvoicesController.Create GET for direct jobs to use stored OvenBatchCost
and ShopSuppliesAmount as separate labeled lines instead of recalculating
shop supplies from scratch (which excluded the oven cost base).
Bug 2 — Quote status stayed Draft after "Send Quote via Email":
- ResendQuote advanced the approval token and sent the email but never
updated the status. Added Draft → Sent advancement (same guard used by
the SMS send path) so the status updates on successful email send.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Voided invoices no longer block creating a new invoice for the same job: voided invoice's
JobId FK is cleared so the unique index slot is freed for the replacement
- Invoice Details view shows voided invoices as history rather than hiding them
- Payment terms: standardized SelectList (Due on Receipt, Net 15/30/45/60/90, 2% 10 Net 30,
COD) with custom-term preservation; invoice-due-date.js auto-updates Due Date on term change
- Shop supplies on direct (no-quote) jobs: InvoicesController derives the shop supplies line
from the company rate when the job has no source quote to read the pre-agreed amount from
- Job entity: ShopSuppliesAmount + ShopSuppliesPercent fields preserved through job lifecycle
- Migration: AddShopSuppliesAmountToJob
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a source quote is edited after a job was created from it, the job
details page now shows a warning banner with the date of the change and
a link to the quote. Two actions are offered:
- Re-sync from Quote: replaces all job items, coats, prep services, and
pricing from the current quote. Only available while the job is still
in a pre-production status (Pending, Quoted, Approved); hidden once
shop work has started (InPreparation or beyond).
- Dismiss: acknowledges the change without altering the job, clearing
the banner by advancing the stored snapshot timestamp.
Implemented via Job.QuoteSnapshotUpdatedAt (new nullable column), set at
quote→job conversion time. The banner fires when quote.UpdatedAt exceeds
this baseline. Migration: AddJobQuoteSnapshotUpdatedAt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>