feat(quotes): promote the quote stub to a real entity + quote_lines (PMS-671) #452
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-671-quotes-entity-schema"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
PMS-671, phase 1 of the PMS-670 Quotes epic. Promotes the deliberate
quotesstub into the real sales entity and addsquote_lines.078_change_requests_and_quotes.sql(PMS-484) createdquotespurely so the PMS-470 polymorphic approvals routes had a parent row to existence-check against. Its own header says the richer sales workflow is its own follow-up and that it is "intentionally NOT trying to be the final shape". This is that follow-up.Migrations are immutable, so
078is untouched and everything lands in a new092.Design notes worth reviewing
Client sign-off is not modelled on
ticket_approvals. That table constrains approvers to internal staff (approver_user_idXORapprover_role, both staff concepts), so it cannot express "the customer signed off". Internal approval stays on the existing approvals surface with no changes; client acceptance becomes state on the quote itself (decided_at,decided_by_contact_id,decision_notes). Two different actors, two different events.Money reconciled to
DECIMAL(12,2)/VARCHAR(3). The stub'stotal_cents BIGINT/CHAR(3)was the odd one out;invoices,invoice_lines, andprojects.budget_amountall use DECIMAL. Reconciling means a quote total, an invoice total, and a project budget compare without unit conversion at every call site.total_centsis retained as deprecated-but-readable rather than dropped, the same posture PMS-470 took with the legacyticket_approvals.ticket_id.accepted/declinedare deliberately distinct fromapproved/rejected. The four values078shipped keep their exact meaning as the internal outcomes, so a reader can always tell a client decision from an internal one.company_idis NOT NULL. Safe because no production code path inserts intoquotes(there is no quotes module yet; the only INSERT in the tree is an integration test), so the table is empty in every real database. Written as add-then-enforce rather than a single NOT NULL add so a future backfill has an obvious seam.UNIQUE (converted_project_id)is the real idempotency guard for the phase-4 conversion: an application-level "have we converted yet?" check can be passed by two concurrent requests, a unique index cannot.Verify
Applied against a real Postgres that already had
078through091, then verified directly:sqlx migrate runis a no-op.company_idNOT NULL enforced; a company-less insert is rejected.converted_project_idUNIQUE rejects a second quote linking to the same project.quote_numberis unique per tenant while multiple NULLs coexist.updated_attrigger fires on UPDATE (verified across separate transactions, sinceNOW()is frozen within one).currencyis no longer blank-padded.cargo test --test approvals_polymorphicpasses 5/5 includingquote_approval_round_trip. Becausesqlx::testprovisions a fresh database and runs every migration, that also proves092applies from scratch and the existing/quotes/{id}/approvalsroutes still work against the richer table.Full integration suite passes.
just checkis clean, including the migration prefix and immutability guards.One pre-existing unrelated failure:
tests/readiness.rs::ready_returns_ok_when_db_reachable_and_infisical_unconfiguredfails locally because the dev.envsupplies Infisical vars to the container while the Infisical profile is not running, so/ready503s instead of reporting the probe as skipped. Confirmed identical on a cleanmaintree with these changes stashed, and CI does not set those vars.Follow-ups
Phases 2 to 5 are PMS-672 (module CRUD with server-computed totals), PMS-673 (send + portal accept/decline), PMS-674 (convert to Project), PMS-675 (SPA).