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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
1116 lines
25 KiB
JSON
1116 lines
25 KiB
JSON
[
|
|
{
|
|
"id": "T001",
|
|
"description": "Migration: quotes table created with correct columns including is_template boolean, types, and constraints",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F001"
|
|
]
|
|
},
|
|
{
|
|
"id": "T002",
|
|
"description": "Migration: quotes table has indexes on (tenant, client_id), (tenant, status), (tenant, quote_number), (tenant, parent_quote_id)",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F001"
|
|
]
|
|
},
|
|
{
|
|
"id": "T003",
|
|
"description": "Migration: quote_items table created with correct columns including is_selected, matching invoice_charges pattern plus quote-specific fields",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F002"
|
|
]
|
|
},
|
|
{
|
|
"id": "T004",
|
|
"description": "Migration: quote_items FK to quotes cascades on delete",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F002"
|
|
]
|
|
},
|
|
{
|
|
"id": "T005",
|
|
"description": "Migration: quote_activities table created with correct columns and FK to quotes",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F003"
|
|
]
|
|
},
|
|
{
|
|
"id": "T006",
|
|
"description": "Numbering: 'QUOTE' entity type generates Q-0001 on first call",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F004"
|
|
]
|
|
},
|
|
{
|
|
"id": "T007",
|
|
"description": "Numbering: sequential calls generate Q-0001, Q-0002, Q-0003",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F004"
|
|
]
|
|
},
|
|
{
|
|
"id": "T008",
|
|
"description": "Numbering: different tenants have independent sequences",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F004"
|
|
]
|
|
},
|
|
{
|
|
"id": "T009",
|
|
"description": "Types: IQuote interface includes all required fields with correct types",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F005"
|
|
]
|
|
},
|
|
{
|
|
"id": "T010",
|
|
"description": "Types: QuoteStatus includes draft, sent, accepted, rejected, expired, converted, cancelled, superseded",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F005"
|
|
]
|
|
},
|
|
{
|
|
"id": "T011",
|
|
"description": "Types: IQuoteListItem includes joined client name and computed display fields",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F006"
|
|
]
|
|
},
|
|
{
|
|
"id": "T012",
|
|
"description": "Schema: createQuoteSchema requires client_id, title, quote_date, valid_until",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F007"
|
|
]
|
|
},
|
|
{
|
|
"id": "T013",
|
|
"description": "Schema: createQuoteSchema rejects invalid dates (valid_until before quote_date)",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F007"
|
|
]
|
|
},
|
|
{
|
|
"id": "T014",
|
|
"description": "Schema: createQuoteItemSchema requires description and validates quantity > 0",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F008"
|
|
]
|
|
},
|
|
{
|
|
"id": "T015",
|
|
"description": "Schema: status transition validation allows draft\u2192sent but rejects draft\u2192accepted",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F009"
|
|
]
|
|
},
|
|
{
|
|
"id": "T016",
|
|
"description": "Schema: status transition validation allows sent\u2192accepted, sent\u2192rejected, sent\u2192expired, sent\u2192cancelled",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F009"
|
|
]
|
|
},
|
|
{
|
|
"id": "T017",
|
|
"description": "Schema: status transition validation allows accepted\u2192converted but rejects converted\u2192draft",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F009"
|
|
]
|
|
},
|
|
{
|
|
"id": "T018",
|
|
"description": "Model: getById returns quote with items for correct tenant",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F010"
|
|
]
|
|
},
|
|
{
|
|
"id": "T019",
|
|
"description": "Model: getById returns null for wrong tenant (isolation)",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F010"
|
|
]
|
|
},
|
|
{
|
|
"id": "T020",
|
|
"description": "Model: getById auto-expires quote if valid_until < today and status is 'sent'",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F017"
|
|
]
|
|
},
|
|
{
|
|
"id": "T021",
|
|
"description": "Model: getById does not auto-expire drafts or accepted quotes",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F017"
|
|
]
|
|
},
|
|
{
|
|
"id": "T022",
|
|
"description": "Model: getByNumber returns correct quote by human-readable number within tenant",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F011"
|
|
]
|
|
},
|
|
{
|
|
"id": "T023",
|
|
"description": "Model: listByTenant returns paginated results with correct total count",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F012"
|
|
]
|
|
},
|
|
{
|
|
"id": "T024",
|
|
"description": "Model: listByTenant filters by status correctly",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F012"
|
|
]
|
|
},
|
|
{
|
|
"id": "T025",
|
|
"description": "Model: listByTenant filters by client_id correctly",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F012"
|
|
]
|
|
},
|
|
{
|
|
"id": "T026",
|
|
"description": "Model: listByTenant sorts by quote_date descending by default",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F012"
|
|
]
|
|
},
|
|
{
|
|
"id": "T027",
|
|
"description": "Model: listByClient returns only quotes for specified client",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F013"
|
|
]
|
|
},
|
|
{
|
|
"id": "T028",
|
|
"description": "Model: create inserts quote with generated quote_number and logs 'created' activity",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F014"
|
|
]
|
|
},
|
|
{
|
|
"id": "T029",
|
|
"description": "Model: create sets default status to 'draft'",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F014"
|
|
]
|
|
},
|
|
{
|
|
"id": "T030",
|
|
"description": "Model: update changes fields and logs 'updated' activity",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F015"
|
|
]
|
|
},
|
|
{
|
|
"id": "T031",
|
|
"description": "Model: update rejects invalid status transitions",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F015"
|
|
]
|
|
},
|
|
{
|
|
"id": "T032",
|
|
"description": "Model: delete removes draft quotes with no business history via deleteEntityWithValidation",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F016",
|
|
"F049a"
|
|
]
|
|
},
|
|
{
|
|
"id": "T033",
|
|
"description": "Model: delete blocks non-draft quotes and offers archive alternative",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F016",
|
|
"F049a"
|
|
]
|
|
},
|
|
{
|
|
"id": "T033a",
|
|
"description": "Model: delete blocks drafts that have business history (emails sent, etc.) and offers archive",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F016",
|
|
"F049a"
|
|
]
|
|
},
|
|
{
|
|
"id": "T034",
|
|
"description": "Item model: listByQuoteId returns items ordered by display_order",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F018"
|
|
]
|
|
},
|
|
{
|
|
"id": "T035",
|
|
"description": "Item model: create with service_id populates service_name, service_sku, unit_price from catalog",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F019"
|
|
]
|
|
},
|
|
{
|
|
"id": "T036",
|
|
"description": "Item model: create without service_id allows custom item entry",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F019"
|
|
]
|
|
},
|
|
{
|
|
"id": "T037",
|
|
"description": "Item model: update allows rate override (different unit_price than catalog default)",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F020"
|
|
]
|
|
},
|
|
{
|
|
"id": "T038",
|
|
"description": "Item model: delete removes item and adjusts display_order of remaining items",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F021"
|
|
]
|
|
},
|
|
{
|
|
"id": "T039",
|
|
"description": "Item model: reorder updates display_order for all items in batch",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F022"
|
|
]
|
|
},
|
|
{
|
|
"id": "T040",
|
|
"description": "Activity model: create stores activity with all fields and auto-timestamps",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F023"
|
|
]
|
|
},
|
|
{
|
|
"id": "T041",
|
|
"description": "Activity model: listByQuoteId returns activities in chronological order",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F024"
|
|
]
|
|
},
|
|
{
|
|
"id": "T042",
|
|
"description": "Action: createQuote requires billing:create permission",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F025"
|
|
]
|
|
},
|
|
{
|
|
"id": "T043",
|
|
"description": "Action: createQuote returns quote with generated number",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F025"
|
|
]
|
|
},
|
|
{
|
|
"id": "T044",
|
|
"description": "Action: updateQuote enforces status transition rules",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F026"
|
|
]
|
|
},
|
|
{
|
|
"id": "T045",
|
|
"description": "Action: deleteQuote rejects deletion of sent/accepted quotes",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F028"
|
|
]
|
|
},
|
|
{
|
|
"id": "T046",
|
|
"description": "Action: addQuoteItem with service_id populates defaults from catalog",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F029"
|
|
]
|
|
},
|
|
{
|
|
"id": "T047",
|
|
"description": "Action: addQuoteItem with all four billing methods (fixed, hourly, usage, per_unit)",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F033"
|
|
]
|
|
},
|
|
{
|
|
"id": "T048",
|
|
"description": "Action: addQuoteItem allows rate override different from catalog default",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F032"
|
|
]
|
|
},
|
|
{
|
|
"id": "T049",
|
|
"description": "Action: addQuoteItem stores is_optional=true when flagged",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F034"
|
|
]
|
|
},
|
|
{
|
|
"id": "T050",
|
|
"description": "Action: addQuoteItem stores is_recurring=true with billing_frequency",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F035"
|
|
]
|
|
},
|
|
{
|
|
"id": "T050a",
|
|
"description": "Business template: creating a quote template sets is_template=true and does not generate a quote_number",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F036"
|
|
]
|
|
},
|
|
{
|
|
"id": "T050b",
|
|
"description": "Business template: createQuoteFromTemplate copies all items from template to new draft quote",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F036a"
|
|
]
|
|
},
|
|
{
|
|
"id": "T050c",
|
|
"description": "Business template: createQuoteFromTemplate generates a new quote_number for the draft",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F036a"
|
|
]
|
|
},
|
|
{
|
|
"id": "T050d",
|
|
"description": "Business template: template list filters by is_template=true, quote list filters by is_template=false/null",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F036b"
|
|
]
|
|
},
|
|
{
|
|
"id": "T050e",
|
|
"description": "Business template: templates are not included in status lifecycle (no sent/accepted/etc.)",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F036"
|
|
]
|
|
},
|
|
{
|
|
"id": "T051",
|
|
"description": "Tax: calculateTax called per taxable item with correct net_amount and region",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F050"
|
|
]
|
|
},
|
|
{
|
|
"id": "T052",
|
|
"description": "Tax: is_taxable=false items get zero tax_amount",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F051"
|
|
]
|
|
},
|
|
{
|
|
"id": "T053",
|
|
"description": "Tax: tax-exempt client gets zero tax on all items",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F051"
|
|
]
|
|
},
|
|
{
|
|
"id": "T054",
|
|
"description": "Tax: reverse charge applicable client gets zero tax",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F051"
|
|
]
|
|
},
|
|
{
|
|
"id": "T055",
|
|
"description": "Tax: per-item tax_region and tax_rate stored correctly after calculation",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F052"
|
|
]
|
|
},
|
|
{
|
|
"id": "T056",
|
|
"description": "Discount: percentage discount calculates correct amount from target item total",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F054"
|
|
]
|
|
},
|
|
{
|
|
"id": "T057",
|
|
"description": "Discount: fixed discount stores exact amount as total_price",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F054"
|
|
]
|
|
},
|
|
{
|
|
"id": "T058",
|
|
"description": "Discount: applies_to_item_id scopes discount to specific item's net_amount",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F055"
|
|
]
|
|
},
|
|
{
|
|
"id": "T059",
|
|
"description": "Discount: applies_to_service_id scopes discount to all items of that service",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F056"
|
|
]
|
|
},
|
|
{
|
|
"id": "T060",
|
|
"description": "Discount: quote-level discount (no target) applies to full subtotal",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F057"
|
|
]
|
|
},
|
|
{
|
|
"id": "T061",
|
|
"description": "Totals: subtotal equals sum of non-discount item total_prices",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F058"
|
|
]
|
|
},
|
|
{
|
|
"id": "T062",
|
|
"description": "Totals: discount_total equals sum of discount line amounts",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F058"
|
|
]
|
|
},
|
|
{
|
|
"id": "T063",
|
|
"description": "Totals: total_amount = subtotal - discount_total + tax",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F058"
|
|
]
|
|
},
|
|
{
|
|
"id": "T064",
|
|
"description": "Totals: adding an item triggers recalculation of all totals",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F059"
|
|
]
|
|
},
|
|
{
|
|
"id": "T065",
|
|
"description": "Totals: removing an item triggers recalculation",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F059"
|
|
]
|
|
},
|
|
{
|
|
"id": "T066",
|
|
"description": "Totals: toggling optional item off excludes it from totals",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F060"
|
|
]
|
|
},
|
|
{
|
|
"id": "T067",
|
|
"description": "Totals: toggling optional item back on includes it in totals",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F060"
|
|
]
|
|
},
|
|
{
|
|
"id": "T068",
|
|
"description": "Versioning: revise creates new quote row with version+1 and parent_quote_id set",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F061"
|
|
]
|
|
},
|
|
{
|
|
"id": "T069",
|
|
"description": "Versioning: revise copies all quote_items to new version with new item_ids",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F062"
|
|
]
|
|
},
|
|
{
|
|
"id": "T070",
|
|
"description": "Versioning: old version status set to 'superseded' after revision",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F063"
|
|
]
|
|
},
|
|
{
|
|
"id": "T071",
|
|
"description": "Versioning: new version has same quote_number as original",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F064"
|
|
]
|
|
},
|
|
{
|
|
"id": "T072",
|
|
"description": "Versioning: can revise a rejected quote (creates new version from rejected)",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F061"
|
|
]
|
|
},
|
|
{
|
|
"id": "T073",
|
|
"description": "Version history: query returns all versions ordered by version number",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F065"
|
|
]
|
|
},
|
|
{
|
|
"id": "T074",
|
|
"description": "Version history: works for quotes with 3+ versions",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F065"
|
|
]
|
|
},
|
|
{
|
|
"id": "T075",
|
|
"description": "Template migration: quote_templates table has templateAst JSONB column",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F070"
|
|
]
|
|
},
|
|
{
|
|
"id": "T076",
|
|
"description": "Template migration: standard_quote_templates seeded with default and detailed templates",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F071"
|
|
]
|
|
},
|
|
{
|
|
"id": "T076a",
|
|
"description": "Template migration: standard quote template seed upsert succeeds on repeated runs",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F071a"
|
|
]
|
|
},
|
|
{
|
|
"id": "T077",
|
|
"description": "QuoteViewModel: correctly maps all quote fields including items with optional/recurring metadata",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F073"
|
|
]
|
|
},
|
|
{
|
|
"id": "T078",
|
|
"description": "AST bindings: quoteNumber, quoteDate, validUntil resolve to correct quote values",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F074"
|
|
]
|
|
},
|
|
{
|
|
"id": "T079",
|
|
"description": "AST bindings: lineItems collection includes is_optional and is_recurring flags per item",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F075"
|
|
]
|
|
},
|
|
{
|
|
"id": "T080",
|
|
"description": "Standard template: 'standard-quote-default' renders valid HTML with all sections",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F076"
|
|
]
|
|
},
|
|
{
|
|
"id": "T081",
|
|
"description": "Standard template: 'standard-quote-detailed' renders phase grouping and optional item markers",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F077"
|
|
]
|
|
},
|
|
{
|
|
"id": "T082",
|
|
"description": "Adapter: mapDbQuoteToViewModel fetches and joins client, contact, tenant data",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F078"
|
|
]
|
|
},
|
|
{
|
|
"id": "T083",
|
|
"description": "PDF: generates valid PDF buffer from quote data",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F079"
|
|
]
|
|
},
|
|
{
|
|
"id": "T084",
|
|
"description": "PDF: stores generated file in file storage and returns file_id",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F080"
|
|
]
|
|
},
|
|
{
|
|
"id": "T085",
|
|
"description": "Preview: renders quote template in-browser without Puppeteer",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F081"
|
|
]
|
|
},
|
|
{
|
|
"id": "T086",
|
|
"description": "Template selection: uses quote-specific template_id if set",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F082"
|
|
]
|
|
},
|
|
{
|
|
"id": "T087",
|
|
"description": "Template selection: falls back to tenant default when no per-quote template",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F082"
|
|
]
|
|
},
|
|
{
|
|
"id": "T088",
|
|
"description": "Template selection: falls back to standard-quote-default when no tenant default",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F082"
|
|
]
|
|
},
|
|
{
|
|
"id": "T089",
|
|
"description": "Send: sendQuote rejects quotes not in draft/approved status",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F084"
|
|
]
|
|
},
|
|
{
|
|
"id": "T090",
|
|
"description": "Send: sendQuote generates PDF, sends email to specified address(es), updates sent_at and status to 'sent'",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F084"
|
|
]
|
|
},
|
|
{
|
|
"id": "T090a",
|
|
"description": "Send: sendQuote accepts array of email addresses and sends to all of them",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F084"
|
|
]
|
|
},
|
|
{
|
|
"id": "T091",
|
|
"description": "Send: sendQuote logs 'sent' activity",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F084"
|
|
]
|
|
},
|
|
{
|
|
"id": "T092",
|
|
"description": "Email: 'Quote Sent' email includes quote summary and PDF attachment",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F085"
|
|
]
|
|
},
|
|
{
|
|
"id": "T093",
|
|
"description": "Email: sent email logged in email_sending_logs with entity_type='quote'",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F088"
|
|
]
|
|
},
|
|
{
|
|
"id": "T094",
|
|
"description": "Client portal: QuotesTab loads and displays client's quotes",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F089",
|
|
"F090"
|
|
]
|
|
},
|
|
{
|
|
"id": "T095",
|
|
"description": "Client portal: QuotesTab only shows quotes for authenticated client",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F090"
|
|
]
|
|
},
|
|
{
|
|
"id": "T096",
|
|
"description": "Client portal: QuoteDetail shows full line items with optional item indicators",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F091"
|
|
]
|
|
},
|
|
{
|
|
"id": "T097",
|
|
"description": "Client portal: toggling optional item on/off recalculates displayed total and persists is_selected server-side",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F092"
|
|
]
|
|
},
|
|
{
|
|
"id": "T097a",
|
|
"description": "Client portal: optional item selections persist across page reloads",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F092"
|
|
]
|
|
},
|
|
{
|
|
"id": "T098",
|
|
"description": "Client portal: Accept persists optional item selections and sets accepted_at, accepted_by, status to 'accepted'",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F093"
|
|
]
|
|
},
|
|
{
|
|
"id": "T098a",
|
|
"description": "MSP detail view: accepted quote with optional items shows client's selections highlighted for review",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F093a"
|
|
]
|
|
},
|
|
{
|
|
"id": "T100",
|
|
"description": "Client portal: Reject requires comment, sets rejected_at, rejection_reason, status to 'rejected'",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F094"
|
|
]
|
|
},
|
|
{
|
|
"id": "T101",
|
|
"description": "Client portal: first view sets viewed_at timestamp and logs activity",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F096"
|
|
]
|
|
},
|
|
{
|
|
"id": "T102",
|
|
"description": "Client portal: second view does not overwrite viewed_at",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F096"
|
|
]
|
|
},
|
|
{
|
|
"id": "T103",
|
|
"description": "Client portal: cannot accept/reject expired quote",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F093",
|
|
"F094"
|
|
]
|
|
},
|
|
{
|
|
"id": "T104",
|
|
"description": "Conversion: Quote\u2192Contract creates draft contract with correct name and dates",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F098"
|
|
]
|
|
},
|
|
{
|
|
"id": "T105",
|
|
"description": "Conversion: recurring fixed-price item creates contract_line + _fixed_config",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F099",
|
|
"F100"
|
|
]
|
|
},
|
|
{
|
|
"id": "T106",
|
|
"description": "Conversion: recurring hourly item creates contract_line + _hourly_config",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F099",
|
|
"F100"
|
|
]
|
|
},
|
|
{
|
|
"id": "T107",
|
|
"description": "Conversion: recurring usage item creates contract_line + _usage_config",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F099",
|
|
"F100"
|
|
]
|
|
},
|
|
{
|
|
"id": "T108",
|
|
"description": "Conversion: client_contracts assignment created with correct client and dates",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F101"
|
|
]
|
|
},
|
|
{
|
|
"id": "T109",
|
|
"description": "Conversion: converted_contract_id set on quote after contract creation",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F102"
|
|
]
|
|
},
|
|
{
|
|
"id": "T110",
|
|
"description": "Conversion: Quote\u2192Invoice creates draft invoice with is_manual=true",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F103"
|
|
]
|
|
},
|
|
{
|
|
"id": "T111",
|
|
"description": "Conversion: one-time items mapped to invoice_charges with correct tax/discount data",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F104"
|
|
]
|
|
},
|
|
{
|
|
"id": "T112",
|
|
"description": "Conversion: converted_invoice_id set on quote after invoice creation",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F105"
|
|
]
|
|
},
|
|
{
|
|
"id": "T113",
|
|
"description": "Conversion: combined conversion creates both contract and invoice in single transaction",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F106"
|
|
]
|
|
},
|
|
{
|
|
"id": "T114",
|
|
"description": "Conversion: combined conversion rolls back both if invoice creation fails after contract creation",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F106"
|
|
]
|
|
},
|
|
{
|
|
"id": "T115",
|
|
"description": "Conversion: status set to 'converted' after successful conversion",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F106"
|
|
]
|
|
},
|
|
{
|
|
"id": "T116",
|
|
"description": "Conversion: preview correctly categorizes items as contract-bound vs invoice-bound",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F107"
|
|
]
|
|
},
|
|
{
|
|
"id": "T117",
|
|
"description": "Conversion: optional items with is_selected=false (excluded by client) are not converted",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F098",
|
|
"F103"
|
|
]
|
|
},
|
|
{
|
|
"id": "T118",
|
|
"description": "Conversion: post-conversion quote detail shows links to created contract and invoice",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F109"
|
|
]
|
|
},
|
|
{
|
|
"id": "T119",
|
|
"description": "Approval: 'Submit for Approval' changes status from draft to pending_approval",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F111"
|
|
]
|
|
},
|
|
{
|
|
"id": "T119a",
|
|
"description": "Approval migration: quotes status constraint allows pending_approval and approved",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F110a"
|
|
]
|
|
},
|
|
{
|
|
"id": "T120",
|
|
"description": "Approval: approve changes status from pending_approval to approved",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F113"
|
|
]
|
|
},
|
|
{
|
|
"id": "T121",
|
|
"description": "Approval: reject returns quote to draft with comment",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F113"
|
|
]
|
|
},
|
|
{
|
|
"id": "T122",
|
|
"description": "Approval: quotes:approve permission required \u2014 user without it gets denied",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F114"
|
|
]
|
|
},
|
|
{
|
|
"id": "T123",
|
|
"description": "Approval: when disabled per tenant, draft\u2192sent transition allowed directly",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F113"
|
|
]
|
|
},
|
|
{
|
|
"id": "T124",
|
|
"description": "Duplication: creates new quote with new number, draft status, copied items",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F119"
|
|
]
|
|
},
|
|
{
|
|
"id": "T125",
|
|
"description": "Duplication: duplicated items have new item_ids (not referencing original)",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F119"
|
|
]
|
|
},
|
|
{
|
|
"id": "T126",
|
|
"description": "Auto-expiration job: bulk-expires all sent quotes past valid_until",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F118"
|
|
]
|
|
},
|
|
{
|
|
"id": "T127",
|
|
"description": "Save as Template: creates business template from existing quote with is_template=true, copies items",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F120"
|
|
]
|
|
},
|
|
{
|
|
"id": "T128",
|
|
"description": "Save as Template: template does not copy quote-specific fields (client, contact, dates, status)",
|
|
"implemented": true,
|
|
"featureIds": [
|
|
"F120"
|
|
]
|
|
}
|
|
]
|