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. Backendphp 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.