Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

386 lines
69 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Scratchpad — Quoting System
- Plan slug: `quoting-system`
- Created: `2026-03-13`
## What This Is
Keep a lightweight, continuously-updated log of discoveries and decisions made while implementing the quoting system.
## Decisions
- (2026-03-13) Place quotes in `packages/billing/src/` — cross-package imports aren't supported by the layer architecture, and quotes share tax, PDF, template, discount, and service catalog infrastructure with billing.
- (2026-03-13) Separate `quote_templates` table from `invoice_templates` — shared AST engine but different content needs (optional items, validity dates, scope of work, accept CTA, T&C section).
- (2026-03-13) Use `superseded` status distinct from `cancelled` — when a quote is revised, the old version is superseded (system action), not cancelled (user action). Preserves intent.
- (2026-03-13) `quantity` field as BIGINT (integer units) matching `invoice_charges` — not decimal. The migration `20250225165701` changed invoice_charges from decimal to integer.
- (2026-03-13) Auto-expiration via on-access check in model layer for Phase 1 — simpler than cron. Background job deferred to Phase 6.
- (2026-03-13) Reuse `billing:*` permissions for most operations. Only add `quotes:approve` for the approval workflow in Phase 6.
- (2026-03-13) `is_optional` line items in Phase 1 — key differentiator from invoices, core to MSP quoting workflow.
- (2026-03-13) Simple accept/reject with comment for client portal — no e-signatures in any phase.
- (2026-03-13) `contract_line_id` removed from quote_items — quotes are pre-sale, contract lines don't exist yet. Mapping happens during conversion.
- (2026-03-13) Match `service_item_kind` column name from invoice_charges (not `item_kind`) for consistency.
- (2026-03-13) Quote business templates via `is_template` boolean on quotes table — following contract template pattern. Separate from PDF document templates.
- (2026-03-13) Naming: "quote templates" = reusable business configurations (is_template on quotes). "Quote document templates" = PDF rendering templates (quote_document_templates table).
- (2026-03-13) Deletion uses `deleteEntityWithValidation()` with `supportsArchive: true`. Hard delete only for drafts with no business history.
- (2026-03-13) Client portal access: all client users with billing permissions can see quotes, not just the primary contact.
- (2026-03-13) Email: supports multiple addresses (array). Contact_id remains as primary/default recipient, but MSP can add additional recipients.
- (2026-03-13) Optional item selections: client toggles persisted server-side via `is_selected` on quote_items. On accept, selections sent to MSP for review before conversion. MSP detail view highlights client's choices.
- (2026-03-13) "Save as Template" action on existing quotes (Phase 6) — creates a business template from a quote, stripping client-specific data.
- (2026-03-13) `tax_source` lives on `quotes` (not `quote_items`) to mirror the invoice model: internal quotes calculate tax locally, while external/pending-external quotes keep item tax amounts at zero until a later integration supplies them.
## Parallel Work
- (2026-03-13) Billing cutover DONE (commit 28ced3ad9 on cleanup/billing branch). `server/src/lib/billing/` fully removed (-5,695 lines). All billing code now canonical in `packages/billing/src/`. Circular billingEngine↔invoiceService dependency eliminated. NumberingService at `@shared/services/numberingService`. All server callers updated to package imports.
## Discoveries / Constraints
- (2026-03-13) Client portal optional-item toggles can reuse the quote financial recalculation service on the server, but the portal still needs a mirrored lightweight totals calculation client-side for immediate feedback before the persisted response returns.
- (2026-03-13) Billing package Vitest config needed additional aliases for `@alga-psa/auth`, `@alga-psa/core`, `@alga-psa/db`, and `@alga-psa/ui` so quote action tests can import package-local server-action dependencies.
- (2026-03-13) `invoice_charges.quantity` is BIGINT (integer), not decimal — changed in migration `20250225165701`.
- (2026-03-13) All billing server actions use `withAuth()` wrapper from `packages/auth/src/lib/withAuth.ts`.
- (2026-03-13) Invoice template AST system is data-agnostic — evaluator takes bindings + data, not invoice-specific. Reusable for quotes by defining new bindings and QuoteViewModel.
- (2026-03-13) Template tables: `invoice_templates` (tenant-scoped custom), `standard_invoice_templates` (system-wide), `invoice_template_assignments` (selection mapping). Quote templates need parallel structure.
- (2026-03-13) PDF pipeline: fetch data → map to ViewModel → evaluate AST → render to HTML → Puppeteer to PDF. Need `QuoteViewModel` + `mapDbQuoteToViewModel()`.
- (2026-03-13) Contract system has templates vs active contracts with snapshot mechanism (`ensureTemplateLineSnapshot`). Quote→Contract conversion creates a direct contract (not template).
- (2026-03-13) Contract line service configurations: `_fixed_config`, `_hourly_config`, `_usage_config`, `_rate_tiers`. Conversion must create the right config type per billing method.
- (2026-03-13) `client_contracts` is an M:N assignment table between contracts and clients.
- (2026-03-13) Billing dashboard tabs defined in `billingTabsConfig.ts` — add "Quotes" tab here.
- (2026-03-13) Client portal billing: `BillingOverview.tsx` with lazy-loaded tabs. Add QuotesTab following InvoicesTab pattern.
- (2026-03-13) Email logged in `email_sending_logs` with `entity_type` field.
- (2026-03-13) Discount model: `is_discount`, `discount_type` ('percentage'/'fixed'), `discount_percentage`, `applies_to_item_id`, `applies_to_service_id`. Same on quote_items.
- (2026-03-13) Standard invoice templates: 'standard-default' (simple) and 'standard-detailed' (full branding). Need equivalent standard quote templates.
- (2026-03-13) Migration naming: `YYYYMMDDHHmmss_description.cjs` in `server/migrations/`.
## Commands / Runbooks
- (2026-03-13) **Feature branch**: `feature/quoting_the_beginnig` (branched off `cleanup/billing` at `28ced3ad9`)
- (2026-03-13) **Parent branch**: `cleanup/billing` (billing cutover, not yet merged to main)
- (2026-03-13) Run migrations: `cd server && npx knex migrate:latest`
- (2026-03-13) Migration files: `server/migrations/YYYYMMDDHHmmss_description.cjs`
- (2026-03-13) Billing package typecheck: `npm --prefix packages/billing run typecheck`
## Testing References
- Test framework: Vitest v4.0.18, sequential execution (`maxConcurrency: 1`, `singleFork: true`)
- TestContext: `server/test-utils/testContext.ts` — transaction-based rollback, `setupContext/resetContext/rollbackContext/cleanupContext`
- Data factories: `server/test-utils/testDataFactory.ts``createTenant()`, `createClient()`, `createUser()`
- DB config: `.env.localtest`, direct PostgreSQL port 5432 (not pgbouncer)
- Billing unit tests: `packages/billing/tests/` (own vitest.config.ts, 10s timeout)
- Billing infra tests: `server/src/test/infrastructure/billing/` (invoices: 17+ files, credits: 7, tax: 3)
- Billing integration tests: `server/src/test/integration/billing/`
- Playwright config: `server/playwright.config.ts`, pattern `**/*.playwright.test.ts`
- No existing Playwright e2e tests for billing — quoting will be first
- Run infra tests: `cd server && dotenv -e ../.env.localtest -- vitest src/test/infrastructure/billing/`
- Example billing test: `server/src/test/infrastructure/billing/invoices/invoiceGeneration.test.ts`
## Links / References
- Billing package: `packages/billing/src/`
- Invoice model: `packages/billing/src/models/invoice.ts`
- Contract model: `packages/billing/src/models/contract.ts`
- Service catalog model: `packages/billing/src/models/service.ts`
- Tax service: `packages/billing/src/services/taxService.ts`
- PDF generation: `packages/billing/src/services/pdfGenerationService.ts`
- Template AST schema: `packages/billing/src/lib/invoice-template-ast/schema.ts`
- Template evaluator: `packages/billing/src/lib/invoice-template-ast/evaluator.ts`
- Template renderer: `packages/billing/src/lib/invoice-template-ast/react-renderer.tsx`
- Standard templates: `packages/billing/src/lib/invoice-template-ast/standardTemplates.ts`
- Invoice adapters: `packages/billing/src/lib/adapters/invoiceAdapters.ts`
- Billing tabs config: `packages/billing/src/components/billing-dashboard/billingTabsConfig.ts`
- Invoice interfaces: `packages/types/src/interfaces/invoice.interfaces.ts`
- Client portal billing: `packages/client-portal/src/components/billing/`
- Client portal InvoicesTab: `packages/client-portal/src/components/billing/InvoicesTab.tsx`
- Email actions: `packages/email/src/actions/emailLogActions.ts`
- Contract actions: `packages/billing/src/actions/contractActions.ts`
- Contract line mapping: `packages/billing/src/actions/contractLineMappingActions.ts`
- Auth wrapper: `packages/auth/src/lib/withAuth.ts`
- Numbering service: `shared/services/numberingService.ts` (EntityType: 'TICKET' | 'INVOICE' | 'PROJECT' — add 'QUOTE')
- Invoice service (package): `packages/billing/src/services/invoiceService.ts` (canonical, server copy deleted)
- Billing engine (package): `packages/billing/src/lib/billing/billingEngine.ts` (canonical, server copy deleted)
## Open Questions
- ~~How does invoice numbering work?~~ RESOLVED: Uses `SharedNumberingService.getNextNumber()` with `generate_next_number` DB function. Add 'QUOTE' entity type, seed with prefix='Q-', padding=4.
- ~~Is `contact_id` a single recipient?~~ RESOLVED: Single primary contact. Email can go to any address. Portal access via billing permissions.
- ~~For conversion: template or direct?~~ RESOLVED: Direct draft contract + client assignment.
- Deletion validation config: `packages/core/src/config/deletion/index.ts` — need to add quote entity with dependency checks (activities, emails, converted entities).
- Contract template system: `packages/billing/src/models/contractTemplate.ts` — reference for quote business template implementation.
- ~~Quote business template wizard~~ RESOLVED: Both wizard + quick create (matching contract pattern). "Save as Template" for existing quotes in Phase 6.
- (2026-03-13) Archived quotes: visible via status filter dropdown in quote list. Filter options include All, Drafts, Sent, Accepted, etc., plus Archived. No separate tab.
## Delivery Log
- (2026-03-13) F097 complete — P4: Added `resendQuote` and `sendQuoteReminder` billing actions that reuse the quote PDF/email pipeline for already-sent quotes, log `resent`/`reminder_sent` activities, and exposed both actions in the MSP quote detail view for sent quotes.
- (2026-03-13) F096 complete — P4: `getClientQuoteById` now stamps `viewed_at` only on first portal open and records a dedicated `viewed` quote activity, while leaving subsequent views untouched.
- (2026-03-13) F094 complete — P4: Added `rejectClientQuote` with required rejection comments, persisted `rejected_at`/`rejection_reason`, logged a client rejection activity, and extended the portal quote detail with a reject form and post-rejection summary banner.
- (2026-03-13) F093a complete — P4: Updated `QuoteDetail.tsx` so accepted quotes surface a review banner plus per-item highlighting for optional items the client selected vs. declined, giving the MSP a clear pre-conversion review state.
- (2026-03-13) F093 complete — P4: Added `acceptClientQuote` in the client portal actions to persist optional selections, move quotes from `sent` to `accepted`, stamp `accepted_at`/`accepted_by`, and log the selected vs. deselected optional items for MSP review; `QuotesTab.tsx` now exposes an Accept Quote action for sent quotes.
- (2026-03-13) F092 complete — P4: Added client-portal optional-item toggles in `QuotesTab.tsx` with optimistic client-side total recalculation, persisted `is_selected` updates through `updateClientQuoteSelections`, and quote-list/detail total refresh after each selection change.
- (2026-03-13) F068 complete — P2: Added `calculateDraftQuoteTotals()` to derive subtotal/discount/tax/total from the in-memory line-item draft state, so the quote form totals refresh immediately as rows are added or edited.
- (2026-03-13) F067 complete — P2: Added dedicated totals sections to both `QuoteForm.tsx` and `QuoteDetail.tsx`, surfacing subtotal, discounts, tax, and grand total with consistent currency formatting.
- (2026-03-13) F066 complete — P2: `QuoteDetail.tsx` now loads `listQuoteVersions()` and renders version buttons for the whole revision chain, letting users hop between quote versions directly from the detail screen.
- (2026-03-13) F065 complete — P2: Added `Quote.listVersions()` and `listQuoteVersions()` to resolve every quote revision in a root-based version chain ordered by `version`, ready for upcoming history UI work.
- (2026-03-13) F064 complete — P2: Quote revisions now retain the original `quote_number`, while `listByTenant()` and `QuoteDetail.tsx` format versioned displays as `Q-XXXX vN` when `version > 1`.
- (2026-03-13) F063 complete — P2: Revising a quote now marks the source version as `superseded` and logs the handoff in quote activities, preserving a clear system-generated revision trail.
- (2026-03-13) F062 complete — P2: `Quote.createRevision()` now clones every existing `quote_item` into the new revision before recalculating totals, so revised quotes start from the full prior configuration.
- (2026-03-13) F061 complete — P2: Added `Quote.createRevision()` plus `createQuoteRevision()` action support, creating a new draft quote revision with `version + 1` and a stable root `parent_quote_id` when revising sent or rejected quotes.
- (2026-03-13) F091 complete — P4: Added `getClientQuoteById` and expanded `QuotesTab.tsx` with an inline quote-detail panel that shows quote metadata, full line items, totals, and terms/conditions when a portal user selects a quote row.
- (2026-03-13) F090 complete — P4: Added `getClientQuotes` to the client-portal billing actions and turned `QuotesTab.tsx` into a `DataTable` view showing the authenticated clients non-draft quotes with amount/date/status columns for any portal user holding `billing:read`.
- (2026-03-13) F089 complete — P4: Added a lazily loaded `QuotesTab` to `packages/client-portal/src/components/billing/BillingOverview.tsx`, including URL tab handling and the new client-portal billing tab entry for quote access.
- (2026-03-13) F088 complete — P4: `sendQuote` now passes `entityType='quote'`/`entityId=quoteId` into `TenantEmailService`, which writes the outbound audit record to `email_sending_logs` for quote email tracking.
- (2026-03-13) F087 complete — P4: Added an MSP-facing “Quote Accepted Confirmation” template to `packages/billing/src/lib/quote-email-templates.ts`, covering accepted amount/date plus a deep link back to the quote for conversion review.
- (2026-03-13) F086 complete — P4: Extended `packages/billing/src/lib/quote-email-templates.ts` with a reusable “Quote Reminder” template, including amount, expiration date, and portal-link messaging for the later reminder flow.
- (2026-03-13) F085 complete — P4: Added `packages/billing/src/lib/quote-email-templates.ts` and switched `sendQuote` to use a dedicated “Quote Sent” template with quote summary details, the PDF attachment context, and a client-portal review link.
- (2026-03-13) F084 complete — P4: Added `sendQuote` to `packages/billing/src/actions/quoteActions.ts`; it validates sendable quotes, generates a PDF, emails one or more recipients through `TenantEmailService`, stamps `sent_at`/`status='sent'`, and records an explicit `sent` activity.
- (2026-03-13) F083 complete — P3: Added `QuoteDocumentTemplate` model methods for custom/standard/all-template reads plus upsert saves, and introduced a shared `IQuoteDocumentTemplate` type so quote document template storage mirrors the invoice template API surface.
- (2026-03-13) F082 complete — P3: Added `packages/billing/src/lib/quote-template-ast/templateSelection.ts` and wired it into quote rendering so document templates resolve in the planned order: explicit per-quote override, tenant default assignment, then the standard default fallback.
- (2026-03-13) F081 complete — P3: Extended `QuotePDFGenerationService` with `renderPreview()`, exposing the same AST evaluator + React-renderer output used for PDF generation so the quote document can be previewed in-browser without Puppeteer.
- (2026-03-13) F080 complete — P3: Extended `QuotePDFGenerationService` with `generateAndStore()`, uploading rendered quote PDFs through the shared storage provider/file-store path so quote documents can be persisted like invoices.
- (2026-03-13) F079 complete — P3: Added `packages/billing/src/services/quotePdfGenerationService.ts`, reusing the AST evaluator, React renderer, browser pool, and Puppeteer pipeline to turn a mapped quote into a PDF buffer with the standard quote template fallback.
- (2026-03-13) F078 complete — P3: Added `packages/billing/src/lib/adapters/quoteAdapters.ts` with `mapDbQuoteToViewModel()` and `mapLoadedQuoteToViewModel()`, which hydrate quote items plus client/contact/default-tenant-company context into the shared `QuoteViewModel` shape, including grouped phases.
- (2026-03-13) F077 complete — P3: Extended `packages/billing/src/lib/quote-template-ast/standardTemplates.ts` with `standard-quote-detailed`, adding a branded header plus dedicated phase, optional, and recurring markers in the detailed quote line-item layout.
- (2026-03-13) F076 complete — P3: Added `packages/billing/src/lib/quote-template-ast/standardTemplates.ts` with the first standard quote AST (`standard-quote-default`), covering quote metadata, scope, a line-item table, totals, a validity notice, and terms/conditions via the shared quote bindings.
- (2026-03-13) F075 complete — P3: Extended the shared quote binding catalog with `lineItems` and `phases` collection bindings, pointing at the new `QuoteViewModel` item/phase arrays so optional, recurring, and phase metadata can flow into dynamic quote template sections.
- (2026-03-13) F074 complete — P3: Added `packages/billing/src/lib/quote-template-ast/bindings.ts` with shared quote value bindings for quote number/date/validity, scope, totals, terms, notes, and party labels so quote templates can target stable binding IDs separate from invoice templates.
- (2026-03-13) F073 complete — P3: Added `QuoteViewModel`, `QuoteViewModelLineItem`, `QuoteViewModelPhase`, and supporting party types to `packages/types/src/interfaces/quote.interfaces.ts`, giving the quote PDF/template pipeline a shared, strongly-typed rendering contract.
- (2026-03-13) F072 complete — P3: Added `server/migrations/20260313132000_create_quote_document_template_assignments.cjs`, mirroring the invoice-template assignment pattern so tenants can map quote document defaults to either standard codes or tenant-scoped custom templates.
- (2026-03-13) F071 complete — P3: Added `server/migrations/20260313131000_create_standard_quote_document_templates.cjs`, creating `standard_quote_document_templates` plus seeded `standard-quote-default` and `standard-quote-detailed` rows with future-friendly AST payloads and stable template codes.
- (2026-03-13) F070 complete — P3: Added `server/migrations/20260313130000_create_quote_document_templates.cjs`, creating the tenant-scoped `quote_document_templates` table with composite `(tenant, template_id)` identity, JSONB `templateAst`, default timestamps, and `is_default` parity with invoice templates.
- (2026-03-13) F069 complete — P2: Added a discount-line composer to `QuoteLineItemsEditor` with percentage/fixed inputs plus quote/item/service targeting; `QuoteForm.tsx` now saves discount metadata and resolves local item targets to persisted quote-item IDs before creating discount rows.
- (2026-03-13) F060 complete — P2: Quote recalculation now treats `is_selected=false` optional items as excluded from subtotal/discount/tax/total calculations, giving Phase 4s client selections a backend-ready totals model.
- (2026-03-13) F059 complete — P2: `QuoteItem.create()`, `update()`, `delete()`, `reorder()`, and `Quote.update()` now all invoke `recalculateQuoteFinancials`, so quote totals and tax stay synchronized whenever items or key quote metadata change.
- (2026-03-13) F058 complete — P2: Quote recalculation now writes `subtotal`, `discount_total`, `tax`, and `total_amount` back to the parent quote using the planned formula: subtotal of non-discount lines, minus discount lines, plus accumulated tax.
- (2026-03-13) F057 complete — P2: Unscoped discount lines now fall back to the quote subtotal in recalculation, which makes quote-level fixed and percentage discounts work without any item/service target.
- (2026-03-13) F056 complete — P2: Discount recalculation now aggregates base totals by `service_id`, enabling `applies_to_service_id` discounts to price against every matching service line on the quote.
- (2026-03-13) F055 complete — P2: Discount recalculation now honors `applies_to_item_id`, pricing percentage discounts against the targeted quote line instead of the full quote subtotal.
- (2026-03-13) F054 complete — P2: Quote recalculation now treats `is_discount` rows specially, deriving percentage discounts from their scoped base amount and fixed discounts from the stored quantity/unit price so discount lines carry meaningful totals.
- (2026-03-13) F053 complete — P2: Added quote-level `tax_source` support (`internal` / `external` / `pending_external`) via a new migration, shared quote typing/schema updates, and recalculation logic that skips internal tax computation when the quote delegates tax externally.
- (2026-03-13) F052 complete — P2: The quote recalculation path now persists `tax_region` (item override or client region fallback) and rounded `tax_rate` back onto each quote item, and the quote-item Zod schemas now accept those fields.
- (2026-03-13) F051 complete — P2: Quote tax recalculation now honors `is_taxable` directly and inherits client tax-exempt / reverse-charge behavior from `TaxService`, yielding zero tax when those client conditions apply.
- (2026-03-13) F050 complete — P2: Added `quoteCalculationService.ts`, which runs `TaxService.calculateTax()` for each included, non-discount quote item and is now invoked from quote-item mutations so quote tax fields are recomputed automatically.
- (2026-03-13) F049 complete — P1: Added reusable `QuoteStatusBadge.tsx` backed by `QUOTE_STATUS_METADATA`, and replaced raw status text in both the quote list and quote detail header with consistent colored badges.
- (2026-03-13) F048 complete — P1: `Quote.getById()` now hydrates `quote_activities`, and `QuoteDetail.tsx` renders those entries in a dedicated activity log section for quote-history auditing.
- (2026-03-13) F047 complete — P1: `QuoteDetail.tsx` now renders status-aware action groups: drafts show Edit/Send/Delete, sent quotes show Revise/Cancel, accepted quotes show the three conversion buttons, and Delete/Cancel are backed by the existing `deleteQuote` and `updateQuote` actions while later-phase actions remain visibly disabled.
- (2026-03-13) F046 complete — P1: Added `QuoteDetail.tsx` as the default existing-quote view, showing summary metadata, scope, line items, notes, and terms; `QuotesTab.tsx` now routes existing quote selections into this read-only detail screen while reserving `mode=edit` for `QuoteForm`.
- (2026-03-13) F045 complete — P1: Quote lines can now be removed locally and reordered by drag in `QuoteLineItemsEditor`; `QuoteForm.tsx` tracks persisted item IDs, deletes removed rows through `removeQuoteItem`, and writes display order back via `reorderQuoteItems` after each save.
- (2026-03-13) F044 complete — P1: Added per-row `Optional` and `Recurring` toggles plus billing-frequency selection in `QuoteLineItemsEditor`, with `QuoteForm.tsx` already persisting those flags through the existing quote-item save path.
- (2026-03-13) F043 complete — P1: Quote line rows now edit description, quantity, and unit price inline in `QuoteLineItemsEditor`, and `QuoteForm.tsx` persists edits for existing rows via `updateQuoteItem` during draft saves.
- (2026-03-13) F042 complete — P1: Expanded `QuoteLineItemsEditor` with a small manual-entry composer (description, quantity, unit price) backed by `createCustomDraftQuoteItem`, so sales users can add custom quote lines without relying on the service catalog.
- (2026-03-13) F041 complete — P1: Added a `QuoteLineItemsEditor` to `QuoteForm.tsx` with `ServiceCatalogPicker` search across services/products, local draft line-item state, and save-time persistence through `addQuoteItem`, so quote drafts can now pick catalog items before saving.
- (2026-03-13) T050d complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a `listQuotes` filtering assertion that distinguishes template views (`is_template=true`) from the standard quote list (`is_template=false` by default).
- (2026-03-13) F040 complete — P1: The same `QuoteForm.tsx` now supports edit mode by loading an existing quote via `getQuote`, pre-filling quote metadata, and saving changes back through `updateQuote`.
- (2026-03-13) F039 complete — P1: Added `QuoteForm.tsx` and wired `QuotesTab.tsx` to open it in create mode with client and contact pickers, a template selector backed by `listQuotes({ is_template: true })`, and draft-save flows for both blank and template-based quote creation.
- (2026-03-13) F038a complete — P1: Added in-tab quote list filters in `QuotesTab.tsx`: a status dropdown with the planned lifecycle states plus archived, and a client filter built from the loaded quote list.
- (2026-03-13) F038 complete — P1: Replaced the quotes dashboard placeholder with a `DataTable`-backed `QuotesTab` that loads non-template quotes via `listQuotes`, shows the required core columns, supports built-in table pagination, and routes row clicks toward upcoming quote detail handling.
- (2026-03-13) F037 complete — P1: Added a `quotes` billing tab definition in `billingTabsConfig.ts` and wired a `QuotesTab` content pane into `BillingDashboard.tsx`, making `/msp/billing?tab=quotes` a valid dashboard destination.
- (2026-03-13) F036b complete — P1: The quote list action/model pair already supports separate template-vs-standard views through the `is_template` filter on `listQuotes`, giving the UI a clean query surface for dedicated template listings.
- (2026-03-13) T050e complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a template lifecycle guard that rejects status transitions on template quotes, proving templates stay outside the sent/accepted/etc. state machine.
- (2026-03-13) T050c complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a `createQuoteFromTemplate` assertion that the returned draft has a fresh `quote_number`, proving template instantiation yields a normal numbered quote rather than another template shell.
- (2026-03-13) T050b complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a `createQuoteFromTemplate` case asserting every template line item is recreated on the new draft quote with the expected recurrence and optional-item metadata.
- (2026-03-13) T050a complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a template-creation case proving `createQuote` preserves `is_template=true` and returns a template without a generated quote number.
- (2026-03-13) F036a complete — P1: Added `createQuoteFromTemplate` in `packages/billing/src/actions/quoteActions.ts`; it validates `billing:create`, loads a template quote, creates a new draft quote from template defaults, clones all template items in a transaction, and returns the populated draft quote with a fresh quote number.
- (2026-03-13) F036 complete — P1: Quote business-template backend now rides on the generic quote actions and model behavior: `is_template=true` quotes are created without numbering, excluded from the normal status lifecycle, and can be read/updated/deleted through the same tenant-scoped CRUD surface.
- (2026-03-13) T050 complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a recurring-item case proving `addQuoteItem` preserves `is_recurring` and `billing_frequency` through the action layer.
- (2026-03-13) T049 complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with an optional-item case proving `addQuoteItem` preserves `is_optional=true` through the server-action boundary.
- (2026-03-13) T048 complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a rate-override case proving `addQuoteItem` forwards an explicit `unit_price` that differs from the service catalog default.
- (2026-03-13) T047 complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a billing-method matrix proving `addQuoteItem` accepts `fixed`, `hourly`, `usage`, and `per_unit` without schema rejection.
- (2026-03-13) T046 complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with an `addQuoteItem` service-backed case asserting the action returns service-derived defaults (name, SKU, billing method, unit metadata) from the quote item creation path.
- (2026-03-13) T045 complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a `deleteQuote` case that propagates the model-layer archive-required error for quotes with business history, covering the action boundary for protected quote deletion.
- (2026-03-13) T044 complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with an `updateQuote` case that surfaces the model-layer invalid status transition error, proving the action path preserves quote lifecycle enforcement.
- (2026-03-13) T043 complete — Extended `packages/billing/tests/quote/quoteActions.test.ts` with a `createQuote` success-path assertion that the action returns the persisted quote including its generated `quote_number` and stamps `created_by` from the authenticated user context.
- (2026-03-13) T042 complete — Added `packages/billing/tests/quote/quoteActions.test.ts` coverage proving `createQuote` returns a permission error when `billing:create` is denied; updated `packages/billing/vitest.config.ts` alias resolution so the billing package test runner can load quote server-action dependencies.
- (2026-03-13) F035 complete — P1: Quote item create/update actions preserve `is_recurring` and `billing_frequency`, and schema validation requires a billing frequency for recurring items so conversion-ready recurrence metadata is stored end-to-end.
- (2026-03-13) F034 complete — P1: Quote item create/update actions preserve `is_optional`, and the quote item model persists/returns that flag so optional line items are available to downstream UI and portal flows.
- (2026-03-13) F033 complete — P1: Quote item actions now accept the full billing method enum (`fixed`, `hourly`, `usage`, `per_unit`) through the quote item schema and preserve service-derived billing methods from the catalog.
- (2026-03-13) F032 complete — P1: `addQuoteItem` and `updateQuoteItem` preserve explicit `unit_price` values, so quote items can override service catalog default pricing without losing the service metadata linkage.
- (2026-03-13) F031 complete — P1: The `addQuoteItem` action now exposes service catalog-backed quote item creation end-to-end: callers provide `service_id`, and the quote item model denormalizes service name/SKU/rate/unit defaults before persisting.
- (2026-03-13) F030 complete — P1: Added `updateQuoteItem`, `removeQuoteItem`, and `reorderQuoteItems` server actions in `packages/billing/src/actions/quoteActions.ts`, all wrapped with `withAuth()`, enforcing `billing:update`, validating item updates with `updateQuoteItemSchema`, and delegating mutation/reorder behavior to the tenant-scoped quote item model.
- (2026-03-13) F029 complete — P1: Added `addQuoteItem` server action in `packages/billing/src/actions/quoteActions.ts`, wrapped with `withAuth()`, enforcing `billing:update`, validating with `createQuoteItemSchema`, and delegating service catalog denormalization/default pricing to the quote item model.
- (2026-03-13) F028 complete — P1: Added `deleteQuote` server action in `packages/billing/src/actions/quoteActions.ts`, wrapped with `withAuth()`, enforcing `billing:delete`, and delegating deletion/archive behavior to the existing quote deletion validation in the model layer.
- (2026-03-13) F027 complete — P1: Added `getQuote` and `listQuotes` server actions in `packages/billing/src/actions/quoteActions.ts`, both wrapped with `withAuth()`, enforcing `billing:read`, and delegating to the tenant-scoped quote model for single-record and paginated list retrieval.
- (2026-03-13) F026 complete — P1: Added `updateQuote` server action in `packages/billing/src/actions/quoteActions.ts`, wrapped with `withAuth()`, enforcing `billing:update`, validating input with `updateQuoteSchema`, defaulting `updated_by` from the authenticated user, and relying on the quote model for status-transition enforcement.
- (2026-03-13) F025 complete — P1: Added `createQuote` server action in `packages/billing/src/actions/quoteActions.ts`, wrapped with `withAuth()`, enforcing `billing:create`, validating input with `createQuoteSchema`, defaulting `created_by` from the authenticated user, and returning the created quote with generated `quote_number`.
- (2026-03-13) F001 complete — P1: Database migration — create `quotes` table with all fields including is_template boolean, indexes, and Citus-compatible composite keys. Implemented via the quote foundation migration.
- (2026-03-13) F002 complete — P1: Database migration — create `quote_items` table modeled on invoice_charges with is_optional, is_selected, is_recurring, phase fields. Implemented via the same quote foundation migration for consistent rollout.
- (2026-03-13) F003 complete — P1: Database migration — create `quote_activities` table for audit trail. Implemented via the same quote foundation migration so audit storage ships together.
- (2026-03-13) F004 complete — P1: Add 'QUOTE' entity type to SharedNumberingService and seed next_number table with prefix='Q-', padding_length=4. Added QUOTE numbering support in the migration seed and shared numbering service.
- (2026-03-13) F005 complete — P1: TypeScript interfaces — IQuote, IQuoteItem, IQuoteActivity, QuoteStatus in packages/types/src/interfaces/quote.interfaces.ts. Added shared quote interfaces in packages/types for billing-side reuse.
- (2026-03-13) F006 complete — P1: TypeScript view models — IQuoteWithClient, IQuoteListItem for list/detail views. Added list/detail quote view types alongside the core quote interfaces.
- (2026-03-13) F007 complete — P1: Zod schemas — createQuoteSchema and updateQuoteSchema with field validation. Added quote create/update validation schemas in the billing package.
- (2026-03-13) F008 complete — P1: Zod schemas — createQuoteItemSchema and updateQuoteItemSchema. Added quote item create/update validation schemas in the billing package.
- (2026-03-13) F009 complete — P1: Zod schema — status transition validation (only allow valid next statuses). Added reusable quote status transition validation helpers for model enforcement.
- (2026-03-13) F010 complete — P1: Quote model — getById with tenant isolation and auto-expiration check. Implemented the quote model getById path with tenant isolation and item hydration.
- (2026-03-13) F011 complete — P1: Quote model — getByNumber (for human-readable lookup). Implemented quote lookup by human-readable number in the model layer.
- (2026-03-13) F012 complete — P1: Quote model — listByTenant with pagination, sorting, and status/client filtering. Implemented paginated tenant quote listing with status/client filters and sorting.
- (2026-03-13) F013 complete — P1: Quote model — listByClient for client-specific quote listing. Implemented client-scoped quote listing in the model layer.
- (2026-03-13) F014 complete — P1: Quote model — create (inserts quote row, generates quote_number, logs activity). Implemented quote creation with QUOTE numbering and created-activity logging.
- (2026-03-13) F015 complete — P1: Quote model — update (validates status transition, updates fields, logs activity). Implemented quote updates with status validation and activity logging.
- (2026-03-13) F016 complete — P1: Quote model — delete via deleteEntityWithValidation: hard delete drafts with no business history, archive for others. Implemented quote deletion through deletion validation with draft hard-delete behavior.
- (2026-03-13) F017 complete — P1: Quote model — auto-expiration: if valid_until < today and status is 'sent', set to 'expired' on read. Implemented on-access quote auto-expiration for sent quotes.
- (2026-03-13) F018 complete P1: Quote item model listByQuoteId ordered by display_order. Implemented ordered quote-item listing by quote.
- (2026-03-13) F019 complete P1: Quote item model create with service catalog lookup (denormalize name, SKU, default rate, unit_of_measure). Implemented quote-item creation with service catalog denormalization.
- (2026-03-13) F020 complete P1: Quote item model update (rate override, quantity, description, flags). Implemented quote-item updates including quantity and rate overrides.
- (2026-03-13) F021 complete P1: Quote item model delete item and recalculate display_order. Implemented quote-item deletion with display-order compaction.
- (2026-03-13) F022 complete P1: Quote item model reorder items (update display_order batch). Implemented batch quote-item reorder support.
- (2026-03-13) F023 complete P1: Quote activity model create activity entry with type, description, performed_by, metadata. Implemented quote activity creation with metadata support.
- (2026-03-13) F024 complete P1: Quote activity model listByQuoteId for audit trail display. Implemented chronological quote activity listing.
- (2026-03-13) F049a complete P1: Register quote entity in deleteEntityWithValidation config with supportsArchive: true and dependency checks. Registered quote deletion rules with archive alternatives and business-history checks.
- (2026-03-13) T001 complete Migration: quotes table created with correct columns including is_template boolean, types, and constraints. Added DB-backed quote infrastructure coverage.
- (2026-03-13) T002 complete Migration: quotes table has indexes on (tenant, client_id), (tenant, status), (tenant, quote_number), (tenant, parent_quote_id). Covered quote index creation in the infrastructure suite.
- (2026-03-13) T003 complete Migration: quote_items table created with correct columns including is_selected, matching invoice_charges pattern plus quote-specific fields. Covered quote_items schema shape in the infrastructure suite.
- (2026-03-13) T004 complete Migration: quote_items FK to quotes cascades on delete. Covered quote_items cascade behavior in the infrastructure suite.
- (2026-03-13) T005 complete Migration: quote_activities table created with correct columns and FK to quotes. Covered quote_activities schema and FK wiring in the infrastructure suite.
- (2026-03-13) T006 complete Numbering: 'QUOTE' entity type generates Q-0001 on first call. Covered first QUOTE numbering generation.
- (2026-03-13) T007 complete Numbering: sequential calls generate Q-0001, Q-0002, Q-0003. Covered sequential QUOTE numbering generation.
- (2026-03-13) T008 complete Numbering: different tenants have independent sequences. Covered QUOTE numbering isolation across tenants.
- (2026-03-13) T009 complete Types: IQuote interface includes all required fields with correct types. Added package-level quote type coverage.
- (2026-03-13) T010 complete Types: QuoteStatus includes draft, sent, accepted, rejected, expired, converted, cancelled, superseded. Added package-level QuoteStatus coverage.
- (2026-03-13) T011 complete Types: IQuoteListItem includes joined client name and computed display fields. Added package-level quote list item type coverage.
- (2026-03-13) T012 complete Schema: createQuoteSchema requires client_id, title, quote_date, valid_until. Added createQuoteSchema required-field coverage.
- (2026-03-13) T013 complete Schema: createQuoteSchema rejects invalid dates (valid_until before quote_date). Added createQuoteSchema date ordering coverage.
- (2026-03-13) T014 complete Schema: createQuoteItemSchema requires description and validates quantity > 0. Added createQuoteItemSchema validation coverage.
- (2026-03-13) T015 complete — Schema: status transition validation allows draft→sent but rejects draft→accepted. Added draft transition validation coverage.
- (2026-03-13) T016 complete — Schema: status transition validation allows sent→accepted, sent→rejected, sent→expired, sent→cancelled. Added sent transition validation coverage.
- (2026-03-13) T017 complete — Schema: status transition validation allows accepted→converted but rejects converted→draft. Added accepted/converted transition validation coverage.
- (2026-03-13) T018 complete — Model: getById returns quote with items for correct tenant. Added getById tenant read coverage.
- (2026-03-13) T019 complete — Model: getById returns null for wrong tenant (isolation). Added getById tenant isolation coverage.
- (2026-03-13) T020 complete — Model: getById auto-expires quote if valid_until < today and status is 'sent'. Added sent quote auto-expiration coverage.
- (2026-03-13) T021 complete Model: getById does not auto-expire drafts or accepted quotes. Added non-sent auto-expiration guard coverage.
- (2026-03-13) T022 complete Model: getByNumber returns correct quote by human-readable number within tenant. Added getByNumber coverage.
- (2026-03-13) T023 complete Model: listByTenant returns paginated results with correct total count. Added listByTenant pagination coverage.
- (2026-03-13) T024 complete Model: listByTenant filters by status correctly. Added listByTenant status filter coverage.
- (2026-03-13) T025 complete Model: listByTenant filters by client_id correctly. Added listByTenant client filter coverage.
- (2026-03-13) T026 complete Model: listByTenant sorts by quote_date descending by default. Added listByTenant default sort coverage.
- (2026-03-13) T027 complete Model: listByClient returns only quotes for specified client. Added listByClient coverage.
- (2026-03-13) T028 complete Model: create inserts quote with generated quote_number and logs 'created' activity. Added quote create numbering/activity coverage.
- (2026-03-13) T029 complete Model: create sets default status to 'draft'. Added quote create default status coverage.
- (2026-03-13) T030 complete Model: update changes fields and logs 'updated' activity. Added quote update activity coverage.
- (2026-03-13) T031 complete Model: update rejects invalid status transitions. Added invalid quote transition rejection coverage.
- (2026-03-13) T032 complete Model: delete removes draft quotes with no business history via deleteEntityWithValidation. Added draft quote delete coverage.
- (2026-03-13) T033 complete Model: delete blocks non-draft quotes and offers archive alternative. Added non-draft quote delete blocking coverage.
- (2026-03-13) T033a complete Model: delete blocks drafts that have business history (emails sent, etc.) and offers archive. Added draft-with-history delete blocking coverage.
- (2026-03-13) T034 complete Item model: listByQuoteId returns items ordered by display_order. Added ordered quote-item listing coverage.
- (2026-03-13) T035 complete Item model: create with service_id populates service_name, service_sku, unit_price from catalog. Added service-backed quote-item creation coverage.
- (2026-03-13) T036 complete Item model: create without service_id allows custom item entry. Added manual quote-item creation coverage.
- (2026-03-13) T037 complete Item model: update allows rate override (different unit_price than catalog default). Added quote-item rate override coverage.
- (2026-03-13) T038 complete Item model: delete removes item and adjusts display_order of remaining items. Added quote-item delete reorder coverage.
- (2026-03-13) T039 complete Item model: reorder updates display_order for all items in batch. Added quote-item batch reorder coverage.
- (2026-03-13) T040 complete Activity model: create stores activity with all fields and auto-timestamps. Added quote activity creation coverage.
- (2026-03-13) T041 complete Activity model: listByQuoteId returns activities in chronological order. Added quote activity ordering coverage.
- (2026-03-16) F098 complete P5: Added `convertQuoteToDraftContract()` plus the `convertQuoteToContract` action so accepted quotes with selected recurring items can now create a draft contract shell seeded from the quote title/description/currency and recurring billing cadence.
- (2026-03-16) F098 complete P5: Added `convertQuoteToDraftContract()` plus the `convertQuoteToContract` action so accepted quotes with selected recurring items can now create a draft contract shell seeded from the quote title/description/currency and recurring billing cadence.
- (2026-03-16) F099 complete P5: `convertQuoteToDraftContract()` now creates one custom `contract_lines` row per selected recurring quote item, mapping quote billing methods into contract line types (`Fixed`/`Hourly`/`Usage`) with sensible billing-timing defaults and stable display order.
- (2026-03-16) F100 complete P5: Quotecontract conversion now creates `contract_line_services`, base `contract_line_service_configuration` rows, and type-specific fixed/hourly/usage config records for each recurring service-backed quote item, with `per_unit` items mapped onto fixed-style contract configuration.
- (2026-03-16) F101 complete P5: Contract conversion now creates a draft `client_contracts` assignment for the accepted quote client, using the quote acceptance date (or quote date fallback) as the contract start and leaving the assignment inactive until the draft is finalized.
- (2026-03-16) F102 complete P5: Quotecontract conversion now writes `converted_contract_id` back to the source quote and records a dedicated `converted_to_contract` activity inside the same transaction used by the action wrapper, so the quote and draft contract stay linked atomically.
- (2026-03-16) F103 complete P5: Added `convertQuoteToDraftInvoice()` and the `convertQuoteToInvoice` action so accepted quotes with selected one-time items now generate a draft manual invoice shell with a real invoice number, draft dates, copied currency/PO metadata, and tenant tax-source parity.
- (2026-03-16) F104 complete P5: Quoteinvoice conversion now copies selected one-time quote items into `invoice_charges`, remaps discount targets onto the new invoice item IDs, preserves tax-region/rate metadata, and stores discount lines as negative net amounts so invoice totals reconcile correctly.
- (2026-03-16) F105 complete P5: Invoice conversion now writes `converted_invoice_id` back onto the quote and records a `converted_to_invoice` activity within the same transaction used by the action wrapper, keeping the draft invoice and source quote linked atomically.
- (2026-03-16) F106 complete P5: Added an atomic `convertQuoteToDraftContractAndInvoice()` flow plus `convertQuoteToBoth`, reusing the contract and invoice converters inside one transaction and marking the quote `converted` with a shared activity once both records are created successfully.
- (2026-03-16) F107 complete P5: Added typed quote-conversion preview data plus `getQuoteConversionPreview`, categorizing each quote line as contract-bound, invoice-bound, or excluded so the UI can show exactly what each conversion action will create before confirmation.
- (2026-03-16) F108 complete P5: `QuoteDetail.tsx` now opens a conversion-preview dialog for accepted quotes, loads the backend mapping preview, enables contract/invoice/both conversion buttons only when relevant items exist, and confirms the chosen conversion path inline.
- (2026-03-16) F109 complete P5: Added post-conversion navigation both ways: quotes now link directly to their converted contract/invoice, contract detail can look up and open its source quote via `converted_contract_id`, and invoice preview can do the same through `converted_invoice_id` lookups.
- (2026-03-16) F110 complete P6: Extended `QuoteStatus` and the shared quote status schema/metadata with `pending_approval` and `approved`, and updated the allowed transition graph plus badge variants so approval-stage quotes can exist cleanly across validation and UI surfaces.
- (2026-03-16) F111 complete P6: Added a dedicated `submitQuoteForApproval` billing action and surfaced it in `QuoteDetail.tsx`, letting draft quotes move into `pending_approval` with the existing billing-update permission path and an explicit internal-approval notice in the UI.
- (2026-03-16) F112 complete P6: Added a dedicated `/msp/quote-approvals` route with `QuoteApprovalDashboard.tsx`, giving approvers a focused queue for `pending_approval` and `approved` quotes, row-level drill-in to `QuoteDetail`, and a direct entry point from the main quotes tab.
- (2026-03-16) F113 complete P6: Added tenant-scoped quote-approval workflow settings (`tenant_settings.settings.billing.quotes.approvalRequired`), made `sendQuote` honor that toggle, and added approve/request-changes actions plus review/comment dialogs so pending quotes can move to `approved` or back to `draft` with audit comments.
- (2026-03-16) F114 complete P6: Added `server/migrations/20260316120000_add_quote_approval_permission.cjs` to backfill a dedicated `quotes:approve` permission for MSP admins, and switched quote approval / request-changes actions to require that permission instead of generic `billing:update`.
- (2026-03-16) F115 complete P6: Exposed `opportunity_id` in the quote create/edit schema and form, and surfaced it in the quote detail metadata so MSPs can carry a CRM opportunity reference through the quote lifecycle even before a dedicated CRM module is present.
- (2026-03-16) F116 complete P6: Reworked `QuoteLineItemsEditor.tsx` to group rows by `phase`, render visual section headers, let editors collapse/expand each section, and carry phase membership during drag-and-drop so items can be reorganized between sections directly in the quote builder.
- (2026-03-16) F117 complete P6: Added quote document-template actions plus a dedicated `/msp/quote-document-templates` screen with a code-first editor, standard-template bootstrap, and quote-binding reference data, giving quote PDFs a tenant-editable document-template workflow parallel to the invoice designer stack.
- (2026-03-16) Discovery: the original `quotes_status_check` database constraint still rejected `pending_approval` and `approved`, so I added a follow-up plan item (`F110a` / `T119a`) to keep the approval workflow shippable end-to-end at the DB layer.
- (2026-03-16) F110a complete P6: Added `server/migrations/20260316121500_expand_quote_status_check_for_approval.cjs` so the database status constraint now accepts `pending_approval` and `approved`, matching the Phase 6 approval workflow introduced in the app layer.
- (2026-03-16) Runbook: server typecheck currently needs a larger heap on this branch; use `NODE_OPTIONS=--max-old-space-size=8192 npm --prefix server run typecheck` for scheduler-related validation.
- (2026-03-16) F118 complete P6: Added the scheduled `expire-quotes` background job (handler, registration, scheduler bootstrap) to bulk-expire sent quotes past `valid_until` each day and send a best-effort notification email to each quote creator after the status/activity update commits.
- (2026-03-16) F119 complete P6: Added `duplicateQuote` to clone any non-template quote into a fresh draft with a new quote number and copied line items, and surfaced a Duplicate action in `QuoteDetail.tsx` that opens the cloned draft directly in edit mode.
- (2026-03-16) F120 complete P6: Added `saveQuoteAsTemplate` to strip client-specific fields from an existing quote while cloning its reusable line-item configuration into an `is_template=true` quote, and exposed a Save as Template action from `QuoteDetail.tsx`.
- (2026-03-16) Discovery: `20260313131000_create_standard_quote_document_templates.cjs` used an unquoted `EXCLUDED.templateAst` reference in its upsert, which breaks repeated test migrations because the quoted camel-case column must be referenced as `EXCLUDED."templateAst"`.
- (2026-03-16) F071a complete P3: Fixed the standard quote-document-template seed migration so repeated runs/upserts now reference `EXCLUDED."templateAst"` correctly, unblocking infra tests and tenant bootstrap flows.
- (2026-03-16) T051 complete Added quote infrastructure tax coverage that spies on `TaxService.calculateTax()` during quote-item creation to verify taxable lines call the tax service with the item net amount and resolved region.
- (2026-03-16) T052 complete Added quote-item infrastructure coverage proving `is_taxable=false` short-circuits the tax service result to zero tax on the persisted line item.
- (2026-03-16) T053 complete Added tax-exempt client coverage in the quote infrastructure suite, verifying quote-item tax stays at zero when the client is marked tax exempt.
- (2026-03-16) T054 complete Added reverse-charge quote tax coverage so client tax settings that enable reverse charge persist zero tax on quote items.
- (2026-03-16) T055 complete Added quote-item persistence coverage for tax recomputation, verifying recalculation writes the resolved `tax_region` and rounded `tax_rate` back onto the stored quote line.
- (2026-03-16) T056 complete Added quote infrastructure coverage for percentage discount lines targeted at a single quote item, asserting the recalculated discount total uses that item's net amount.
- (2026-03-16) T057 complete Added fixed-discount infrastructure coverage proving discount rows persist their exact amount into `total_price` during recalculation.
- (2026-03-16) T058 complete Added scoped item-discount coverage verifying `applies_to_item_id` discounts only price against the targeted quote line and not the full quote subtotal.
- (2026-03-16) T059 complete Added service-scoped discount coverage verifying `applies_to_service_id` aggregates all matching service lines and excludes other services from the discount base.
- (2026-03-16) T060 complete Added quote-level discount coverage proving unscoped discount lines fall back to the full non-discount subtotal.
- (2026-03-16) T061 complete Added quote totals coverage verifying `subtotal` is the sum of non-discount line totals even when discount rows are present.
- (2026-03-16) T062 complete Added quote totals coverage verifying `discount_total` is the sum of all recalculated discount-line amounts.
- (2026-03-16) T063 complete Added quote totals coverage with live tax + discount data, asserting `total_amount` persists as subtotal minus discounts plus tax.
- (2026-03-16) T064 complete Added quote-item infrastructure coverage proving a second line-item insert immediately recalculates persisted quote subtotal and total_amount.
- (2026-03-16) T065 complete Added quote-item deletion coverage proving removing a line forces persisted quote totals to recalculate against the remaining items.
- (2026-03-16) T066 complete Added optional-item selection coverage proving `is_selected=false` removes an optional line from persisted quote totals.
- (2026-03-16) T067 complete Added optional-item reselection coverage proving toggling `is_selected` back to true restores the optional line into persisted quote totals.
- (2026-03-16) Discovery quote revisions were still blocked by the original unique `(tenant, quote_number)` index, so Phase 2 versioning needed a follow-up migration to make quote-number uniqueness version-aware while preserving base-number lookups.
- (2026-03-16) F064a complete Added a follow-up migration that replaces the unique base-number index with a version-aware uniqueness constraint and updated `Quote.getByNumber()` to resolve the latest version for a shared quote number.
- (2026-03-16) T068 complete Added infrastructure coverage proving `Quote.createRevision()` creates a new draft row with `version + 1` and a stable `parent_quote_id` root link.
- (2026-03-16) T069 complete Added revision-copy coverage proving every source quote line is cloned onto the new revision with fresh `quote_item_id` values.
- (2026-03-16) T070 complete Added revision lifecycle coverage proving the source quote is marked `superseded` when a new revision is created.
- (2026-03-16) T071 complete Added revision numbering coverage proving new versions retain the original base `quote_number`.
- (2026-03-16) T072 complete Added rejected-quote revision coverage proving `Quote.createRevision()` accepts rejected quotes as valid revision sources.
- (2026-03-16) T073 complete Added version-history coverage proving `Quote.listVersions()` returns a full revision chain ordered by ascending `version`.
- (2026-03-16) T074 complete Added 3-version history coverage proving `Quote.listVersions()` resolves the full chain even when queried from the latest revision.
- (2026-03-16) T075 complete Added infrastructure coverage proving the tenant-scoped quote document template table stores `templateAst` as JSONB.
- (2026-03-16) T076 complete Added seed coverage proving the standard quote document template table contains both the default and detailed seeded template codes.
- (2026-03-16) T076a complete Added repeatability coverage that reruns the standard quote template migration twice against an existing table and confirms the upsert path stays deduplicated.
- (2026-03-16) Discovery `mapDbQuoteToViewModel()` still selected legacy `contacts.phone_number` even though the March 9 contact-phone migration moved phone data into `contact_phone_numbers`, so quote previews/details could not safely hydrate contact phone info.
- (2026-03-16) F078a complete Updated quote contact-party mapping to read phone data from `contact_phone_numbers` with default/display-order fallback, restoring QuoteViewModel compatibility with the current contact schema.
- (2026-03-16) T077 complete Added QuoteViewModel mapping coverage proving quote metadata, optional/recurring line-item flags, and phase grouping are preserved in the mapped rendering contract.
- (2026-03-16) T078 complete Added binding-evaluation coverage proving the shared quote AST bindings resolve quote number and date fields from the mapped view model.
- (2026-03-16) T079 complete Added collection-binding coverage proving `lineItems` exposes optional and recurring flags for template rendering.
- (2026-03-16) T080 complete Added default quote template preview coverage proving the rendered HTML includes the expected core sections and line-item content.
- (2026-03-16) T081 complete Added detailed quote template preview coverage proving the rendered HTML surfaces phase, optional, and recurring markers in the detailed layout.
- (2026-03-16) T082 complete Added adapter integration coverage proving quote view-model hydration joins client, contact, and tenant-company records from the database.
- (2026-03-16) T085 complete Added preview-service coverage proving `renderPreview()` returns HTML/CSS without touching the Puppeteer browser pool.
- (2026-03-16) T086 complete Added template-selection coverage proving a quote-specific `template_id` overrides tenant defaults and standard fallback resolution.
- (2026-03-16) T087 complete Added template-selection coverage proving tenant-level document template assignments are used when a quote has no explicit override.
- (2026-03-16) T088 complete Added template-selection fallback coverage proving quote rendering falls back to `standard-quote-default` when no assignment exists.
- (2026-03-16) T083 complete Added unit coverage for `QuotePDFGenerationService.generatePDF()`, mocking the browser pool and AST pipeline to verify it returns a PDF buffer from quote data.
- (2026-03-16) T084 complete Added unit coverage for `QuotePDFGenerationService.generateAndStore()`, mocking storage/file-store integrations to verify generated quote PDFs are uploaded and persisted with a returned `file_id`.
- (2026-03-16) Discovery billing package tests now exercise `@alga-psa/email` via `sendQuote`, so the package Vitest config needed an `@alga-psa/email` alias alongside the existing auth/core/db aliases.
- (2026-03-16) T089 complete Extended the quote action unit suite to cover `sendQuote()` state validation, with billing-package Vitest aliases updated to resolve the email package during send-email action tests.
- (2026-03-16) T090 complete Added `sendQuote()` success-path coverage proving it generates a PDF, sends email, and persists the quote status as `sent`.
- (2026-03-16) T090a complete Added multi-recipient send coverage proving `sendQuote()` forwards every provided email address to the outbound message.
- (2026-03-16) T091 complete Added activity-log coverage proving `sendQuote()` records a `sent` quote activity with recipients and email message metadata.
- (2026-03-16) T092 complete Added email-payload coverage proving the quote-sent email includes summary details and a PDF attachment named for the quote number.
- (2026-03-16) T093 complete Added email-logging coverage proving quote sends pass `entityType=quote` and `entityId` into the email service for downstream audit logging.
- (2026-03-16) T094 complete Added `packages/client-portal/src/actions/client-portal-actions/client-billing.quote.test.ts`, proving `getClientQuotes()` returns the authenticated client's non-draft quote list for the Quotes tab, and added the missing `@alga-psa/jobs` Vitest alias in `server/vitest.config.ts` so the portal action test imports resolve under the shared server runner.
- (2026-03-16) T095 complete The same `client-billing.quote.test.ts` suite now asserts `getClientQuotes()` calls `Quote.listByClient()` with the portal user's resolved `client_id`, locking the quote list to the authenticated client's scope.
- (2026-03-16) T096 complete Added portal quote-detail coverage showing `getClientQuoteById()` returns full quote items, including `is_optional` metadata needed for the client-side optional item indicators.
- (2026-03-16) T097 complete Added a portal selection-update test that flips an optional quote item off, verifies `quote_items.is_selected` persistence through the transaction layer, and confirms `recalculateQuoteFinancials()` drives the refreshed total back to the client.
- (2026-03-16) T097a complete The portal quote test suite now reloads the same quote after `updateClientQuoteSelections()` and proves the stored optional-item choice survives a follow-up `getClientQuoteById()` fetch.
- (2026-03-16) T098 complete Added portal acceptance coverage proving `acceptClientQuote()` persists optional selections, stamps `accepted_at`/`accepted_by`, moves the quote to `accepted`, and records the MSP-review metadata for selected vs. deselected optional items.
- (2026-03-16) T100 complete Added rejection-flow coverage that rejects blank comments, trims valid comments, persists `rejected_at` and `rejection_reason`, and logs the client rejection activity payload.
- (2026-03-16) T101 complete Added first-view coverage for `getClientQuoteById()`, asserting the portal stamps `viewed_at` once via the quotes table update path and records a dedicated `viewed` activity entry.
- (2026-03-16) T102 complete The portal quote action tests now cover repeat views, proving an existing `viewed_at` timestamp is left unchanged and no duplicate `viewed` activity is emitted.
- (2026-03-16) T103 complete Added an expired-quote guard test showing both portal accept and reject flows fail once a quote leaves the `sent` state, protecting expired proposals from further client action.
- (2026-03-16) T098a complete Added `packages/billing/tests/quote/quoteDetail.test.tsx` to render `QuoteDetail` in `jsdom` and verify accepted quotes show both the review banner and per-item selected/declined optional-item highlights for MSP conversion review; widened `packages/billing/vitest.config.ts` to include `tsx` tests and the `@alga-psa/clients`/`@alga-psa/shared` aliases required by the component graph.
- (2026-03-16) T104 complete Added `server/src/test/infrastructure/billing/quotes/quoteConversion.test.ts` and hardened `packages/billing/src/services/quoteConversionService.ts` to respect real contract/invoice table shapes; the first conversion case now proves an accepted quote becomes a draft contract with the quote title and accepted-date client assignment.
- (2026-03-16) T105 complete Added fixed-price contract conversion coverage that asserts the generated contract line uses `contract_line_type=Fixed` and persists a matching `contract_line_service_fixed_config` base rate.
- (2026-03-16) T106 complete Added hourly conversion coverage proving recurring hourly quote items create a contract line plus the hourly configuration records used for time-based billing.
- (2026-03-16) T107 complete Added usage conversion coverage proving recurring usage quote items create usage contract lines and seed `contract_line_service_usage_config` with the mapped unit metadata.
- (2026-03-16) T108 complete The conversion infra suite now verifies `client_contracts` assignments are created for the accepted quote client with the expected start date and inactive draft state.
- (2026-03-16) T109 complete Added a source-quote persistence assertion showing contract conversion writes `converted_contract_id` back onto the originating quote row.
- (2026-03-16) T110 complete Added draft-invoice conversion coverage proving one-time quote items create a draft invoice flagged `is_manual=true`.
- (2026-03-16) T111 complete Added invoice-charge mapping coverage for one-time quote conversion, including preserved tax fields, negative discount rows, and discount targeting remapped onto the created invoice charge IDs.
- (2026-03-16) T112 complete Added a source-quote persistence assertion showing invoice conversion writes `converted_invoice_id` back onto the originating quote row.
- (2026-03-16) T113 complete The combined conversion path is now covered end-to-end, proving a single accepted quote can create both a draft contract and a draft invoice inside one transaction.
- (2026-03-16) T114 complete Added an injected invoice-numbering failure case around `convertQuoteToDraftContractAndInvoice()`, proving the surrounding transaction rolls back the earlier contract work when the invoice half fails.
- (2026-03-16) T115 complete Added a combined-conversion status assertion that confirms successful dual conversion stamps the quote as `converted` with a `converted_at` timestamp.
- (2026-03-16) T116 complete Added preview coverage for `buildQuoteConversionPreview()`, proving recurring items route to contracts, one-time items route to invoices, and deselected/recurring-discount rows land in the excluded bucket.
- (2026-03-16) T117 complete Added optional-selection conversion coverage showing deselected optional recurring and one-time items are excluded from both the created contract lines and invoice charges.
- (2026-03-16) T118 complete Extended `packages/billing/tests/quote/quoteDetail.test.tsx` so converted quotes render the post-conversion buttons for opening the created contract and invoice directly from quote detail.
- (2026-03-16) T119 complete Added `submitQuoteForApproval` coverage in `packages/billing/tests/quote/quoteActions.test.ts`, asserting draft quotes transition to `pending_approval` through the action layer with the authenticated updater recorded.
- (2026-03-16) T119a complete Added an infrastructure assertion in `server/src/test/infrastructure/billing/quotes/quoteInfrastructure.test.ts` that inspects `quotes_status_check`, proving the DB now accepts `pending_approval` and `approved` statuses from the approval migration.
- (2026-03-16) T120 complete Added `approveQuote` action coverage to confirm `pending_approval` quotes move to `approved` and emit an explicit approval activity/comment for the audit trail.
- (2026-03-16) T121 complete Added `requestQuoteApprovalChanges` coverage in `packages/billing/tests/quote/quoteActions.test.ts`, verifying the approval-review path returns the quote to draft and records the reviewers requested-change comment.
- (2026-03-16) T122 complete Added an approval-permission denial case in `packages/billing/tests/quote/quoteActions.test.ts`, proving `approveQuote` is blocked without `quotes:approve` and does not mutate quote state or activity history.
- (2026-03-16) T123 complete Added `sendQuote` coverage showing that tenants with quote approval disabled can send draft quotes directly, while still running through the normal PDF/email/send-state pipeline.
- (2026-03-16) T124 complete Added `duplicateQuote` action coverage in `packages/billing/tests/quote/quoteActions.test.ts`, confirming a new numbered draft quote is created and repopulated with cloned line items from the source quote.
- (2026-03-16) T125 complete Extended duplication coverage to assert duplicated quote items come back with brand-new `quote_item_id` values instead of reusing the source quotes row identifiers.
- (2026-03-16) T127 complete Added `saveQuoteAsTemplate` action coverage confirming the action creates an `is_template=true` quote shell and clones the source quotes line items into that new business template.
- (2026-03-16) T128 complete Added a `saveQuoteAsTemplate` assertion proving business templates intentionally null out client/contact/date fields so no quote-specific sales context leaks into reusable template records.
- (2026-03-16) T126 complete Added `server/src/test/infrastructure/billing/quotes/expireQuotesHandler.test.ts`, which uses real quote rows plus mocked tenant-wrapper/email edges to verify the scheduled job bulk-expires every overdue sent quote, logs `expired` activities, and leaves non-eligible quotes untouched.