Skip to content

Billing product line picker — Production Readiness Audit ​

FieldValue
Date2026-08-18
StatusGo — migrate-first rollout (Backend before SPA)
ScopeOptional product_id on quotation / estimate / invoice lines + Products HTML description
Branchfeature/billing-documents-discounts-and-terms
Catalogquotations 1.3.1, estimates 1.3.1, invoices 1.5.1, products 1.1.1
CompanionsQuotations · Estimates · Invoices · Products · API quotes · Changelog

Additive MINOR/PATCH on existing billing document modules and Products. No new permissions, queues, or foundation changes (platform freeze).


Executive summary ​

Operators with Products installed and products.view can optionally pick an active catalog product on quotation, estimate, and invoice lines. The SPA uses server-side search (status=active) to fill name, rich details, and unit price; fields remain editable. Clearing the product link does not wipe edited text. The API stores client-sent values plus optional product_id (no catalog re-copy on save). LinkableProduct requires Products entitlement, products.view (or superadmin), an active non-trashed product, and tenant scope. Estimate → invoice convert and recurring invoice clone preserve product_id. Product / document HTML strips style and neutralizes javascript: / data: hrefs.

GateResult
Nullable product_id FK + nullOnDelete (idempotent migrate)Pass
LinkableProduct (entitlement + products.view + active + SoftDeletes)Pass
syncLines persists product_id; convert + recurring copyPass
Line resources expose product_id + optional product {id,sku,name}Pass
Eager load lines.product on show/mutationsPass
SPA picker gated products module + products.view; server search + status=activePass
Clear product link does not wipe edited line textPass
DocumentHtmlSanitizer strips style / data: / javascript:Pass
SPA display DOMPurify (sanitizeDocumentHtml)Pass
Catalog bumps migrate-only (no auto-entitle)Pass
Pest: entitlement, inactive/trashed/permission, convert + recurring product_idPass
Playwright products + quotations (+ clear/hide) + estimates + invoicesPass (see Test evidence)
database.md line schemasPass

Go / No-Go: Go — ship after Backend migrate on all tenant DBs before SPA that posts product_id.


Findings (remediated) ​

IDSeverityStatusFindingRemediation
H1HighOps gateSPA before migrate → SQL missing product_idDeploy Backend migrate first; smoke POST with product_id
H2HighFixedPicker per_page: 100, client filter onlyServer search + status=active via products list API
H3HighFixeddatabase.md still showed description on quote linesUpdated quotation/estimate/invoice line schemas
H4HighFixedPest gaps: entitlement / convert / recurringEstimate + invoice entitlement; convert + recurring assert product_id
M1MediumFixedSanitizer kept style= / data: hrefsStrip style; neutralize data: + unit tests
M2MediumFixedSoft-deleted products passed LinkableProductSoftDeletes-aware Product::query()
M3MediumFixedAPI ignored products.viewRequire products.view or superadmin (SPA + PO picker aligned)
M4MediumFixedConvert / products overview incompleteDeveloper convert list + products guides
M5MediumFixede2e missing clear-without-wipe + picker hiddenQuotations Playwright extended
L1LowFixedInactive products linkableReject unless ProductStatusEnum::Active; picker filters active
L2LowFixedUpgrade runbook omitted migratesNamed under upgrade + this page

Accepted / intentional ​

ItemNotes
No new permissionsReuse document create/update + Products entitlement / products.view
Client-owned line textSelecting a product overwrites on select; save does not re-pull catalog
Soft delete ≠ nullOnDeleteForce-delete nulls FK; soft delete leaves product_id (embed may be null)
Catalog bump ≠ auto-installWorkspaces without Quotations/Estimates/Invoices/Products unchanged

Test evidence ​

SuiteResultNotes
Pest unit DocumentHtmlSanitizerRequired greenstyle / data / javascript
Pest Quotation product link + entitlement + inactive/trashed/permissionRequired greenQuotationTest
Pest Estimate product link + entitlement + convert product_idRequired greenEstimateTest
Pest Invoice product link + entitlement + recurring product_idRequired greenCustomerInvoiceTest / RecurrenceTest
npm run test:e2e:productsRequired greenValidation + CRUD
npm run test:e2e:quotationsRequired greenClear-without-wipe + picker hidden without Products
npm run test:e2e:estimatesRequired greenConvert asserts invoice product_id
npm run test:e2e:invoicesRequired greenProduct draft + recurring

Deploy order ​

  1. Backend — php artisan migrate --force
    • 2026_08_18_000658_bump_products_module_version_to_1_1_0
    • 2026_08_18_001042_add_product_id_to_billing_document_lines_tables
    • 2026_08_18_001043_bump_billing_document_modules_for_product_line_picker
    • 2026_08_18_063000_bump_modules_for_product_line_picker_hardening → catalog 1.1.1 / 1.3.1 / 1.3.1 / 1.5.1
  2. Confirm catalog versions match the table above
  3. SPA — server-search product picker + TipTap product description
  4. Docs
  5. Staging smoke below

Suggested merge: Backend → Frontend → Docs. Do not db:seed.


Pre-flight checklist ​

#CheckOwnerPass?
1Migrations 000658 / 001042 / 001043 / 063000 on all tenant DBsOps☐
2Catalog versions match table aboveOps☐
3Products entitled workspace: pick product on quote/estimate/invoiceEng☐
4Edit line text after pick; save; show still shows edited text + product_idEng☐
5Clear product link → edited text remainsEng☐
6Estimate convert → invoice line retains product_idEng☐
7Workspace without Products: no picker; API reject product_idEng☐
8Pest billing/product filters green in CIEng☐
9Playwright products + quotations + estimates + invoices greenEng☐
10Branded PDF still renders line body HTML safelyEng☐

Staging smoke ​

  1. Entitle Products + Quotations (or Estimates / Invoices).
  2. Create a product with rich HTML description and a price.
  3. New quotation/estimate/invoice → search/select product → confirm name/details/price fill.
  4. Edit details → save → reopen → text unchanged; product_id still set.
  5. Clear product link → edited text remains.
  6. Accept estimate → Convert to invoice → API show line has same product_id.
  7. Download PDF → line details visible, no script / style injection.

Rollback ​

  • SPA only: hide picker / stop posting product_id (column remains nullable).
  • Schema: do not drop product_id in production without a dedicated reverse migration; additive columns are safe to leave.
  • Catalog versions: down migrations restore prior versions only if explicitly run (usually leave bumps).

Sign-off ​

RoleNameDateDecision
Eng2026-08-18Go
OpsMigrate complete ☐
Product

Official documentation for the EloSync SaaS Platform.