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

8.0 KiB

Products — Scratchpad

Plan date: 2026-01-01

Why this plan exists

Add a first-class “Products” capability (MSP offer catalog) integrated with contracts, invoices, taxes, and accounting exports.

Discovery notes (repo)

Existing primitives that overlap with “Products”

  • Service Catalog already exists (service_catalog) and is used as the authoritative sellable item source in billing, tax hooks, and accounting mapping.
  • Multi-currency pricing exists for catalog items via IServicePrice and service pricing UI (server/src/components/settings/billing/ServiceCatalogManager.tsx).
  • Tax selection for catalog items already uses tax_rate_id (nullable = non-taxable).
  • Accounting mapping resolution currently resolves mappings for service (service_catalog row) and optionally service_category (server/src/lib/services/accountingMappingResolver.ts).

Billing engine gaps

  • BillingEngine.calculateProductCharges and calculateLicenseCharges exist but are placeholders returning an empty planServices list (server/src/lib/billing/billingEngine.ts).
  • There are TODOs implying service_catalog.service_type is missing, but migrations show the schema has evolved; current app code relies on custom_service_type_id + billing_method instead.

Contracts / line types

  • contract_lines.contract_line_type is stored as free text in the DB (server/migrations/20251008000001_rename_billing_to_contracts.cjs), but TypeScript types in server/src/interfaces/billing.interfaces.ts and the API service layer are narrower (often 'Fixed' | 'Hourly' | 'Usage').
  • Tests/reference code mention additional types like 'Bucket', and billing engine supports bucket charges; product/license line types likely need formalization.

Implement Products as a “kind” of catalog item and keep a single sellable-item id flowing through:

  • contract services (client_contract_services)
  • invoice items (invoice_items)
  • mapping resolution (accounting export)
  • tax assignment (tax_rate_id)

This avoids duplicating tax/mapping/pricing logic and keeps a clean path for future inventory augmentation.

  • Billing domain overview: docs/billing/billing.md
  • Multi-currency billing plan: ee/docs/plans/2025-11-17-multi-currency-billing-plan.md
  • Tax system completion plan: ee/docs/plans/2025-11-24-tax-system-completion-and-external-passthrough-plan.md

Open decisions to confirm

  • Products as service_catalog subset/view (single catalog id).
  • Contracts: start with recurring only (no bill-once in V1).
  • Price overrides allowed (no audit trail in V1).
  • No line-level discounts in V1.
  • Products must be usable on: contracts + invoices + tickets + projects + time entries.
  • License handling: needs term/period semantics; likely implement as properties on the product (may still emit type: 'license' charges).
  • Materials model: separate “materials” records; V1 starts with materials on tickets/projects only (defer time-entry linkage).
  • Ticket/project materials: auto-bill in V1.
  • Billing path: ticket/project materials roll into billing engine like usage/time (not direct invoice items at entry-time).
  • License semantics: term metadata only in V1 (no start/end, no proration).

Commands used during discovery

  • rg -n "calculateProductCharges|calculateLicenseCharges" -S server/src
  • rg -n "service_catalog" -S server/src ee docs

Implementation gotchas / notes

  • getServices() now defaults to item_kind: 'service' to preserve legacy expectations; use getServices(..., { item_kind: 'product' }) or { item_kind: 'any' } explicitly when you need products included.
  • server/src/components/ui/Button requires an id prop; new buttons added for Products/Materials include explicit IDs.
  • Archive semantics (Products): “Delete product” is implemented as archive (service_catalog.is_active=false). Archived products:
    • are hidden from product pickers by default (pickers now request is_active: true)
    • cannot be attached to new contract lines (server-side enforcement in addServiceToContractLine)
    • can be restored via Products UI (sets is_active=true).
  • Contracts/templates: Client Contract Wizard + Template Wizard now have an explicit Products step; products are created on their own fixed contract line (“… - Products”) to avoid mixing with fixed-fee base rates.
  • Billing safety: Billing engine now throws a clear error if a product has no catalog price in the contract currency and no custom rate override (prevents accidental $0 product charges).
  • Manual invoices: Manual invoice service picker now uses multi-currency catalog prices (and includes products) instead of default_rate (which is often 0 for products).
  • Guardrails added:
    • Service catalog mutations (create/update/delete) and service price mutations now enforce RBAC via hasPermission(user, 'service', ...).
    • Catalog rates (default_rate, cost, service_prices.rate) are normalized to integer cents and rejected if negative.
    • Manual invoice API now requires quantity > 0 for non-discount line items.
  • Scalable catalog pickers (contracts):
    • Added server/src/components/ui/AsyncSearchableSelect.tsx (debounced, server-side search, 10-item limit + “more results” indicator).
    • Added server/src/components/billing-dashboard/contracts/ServiceCatalogPicker.tsx to search services + products (filters by billing_method, item_kind, is_active).
    • Wired into contract dialogs that previously loaded getServices(1, 999, ...) and filtered client-side.
  • Product update reliability: Service.update() now strips undefined keys before calling Knex update() to avoid invalid/undefined bindings when optional product fields are omitted.
  • Product categories (V1): Products use the existing service_categories reference data via service_catalog.category_id for controlled categorization (filtering + accounting mapping); the legacy freeform product_category is treated as an optional “label” field in the UI.
  • Product money formatting: Product list/pricing surfaces avoid hard-coded $ and always display currency context (symbol + (CODE)), consistent with multi-currency catalog pricing.
  • API surface added/updated:
    • /api/v1/products and /api/v1/products/{id} provide product catalog CRUD over API keys (RBAC resource: service).
    • /api/v1/services list query now supports item_kind (service|product|any) and is_active filters; billing_method now includes per_unit.
    • Contract Lines API v2 now resolves plan services from service_catalog (not services) and accepts per_unit billing methods for product services.
  • OpenAPI/metadata: MetadataService.discoverSchemas() originally looked for server/src/lib/api/schemas relative to process.cwd(), which is wrong in Next (cwd is server/). Fixed to scan src/lib/api/schemas, added support for z.object(shapeName) schema patterns, and tagged /api/v1/products under Configuration.
  • Invoice preview (drafts) bug: Draft invoice preview failed with Postgres error column reference "tenant" is ambiguous due to an unqualified .where({ tenant }) after joining invoice_charges as ic with service_catalog as sc in Invoice.getInvoiceCharges. Fixed by qualifying the where clause to ic.tenant/ic.invoice_id (server/src/lib/models/invoice.ts).
    • Dev stack note: In this worktree the Next.js server runs from a built image (not a bind-mount), so code changes may require docker compose ... build server + recreate to take effect.
  • Local build sanity checks:
    • TypeScript passes under NEXTAUTH_SECRET='local-build-secret' NODE_OPTIONS='--max-old-space-size=8192' npm -w server run build
    • Next.js prerender currently fails on /_global-error (useContext null) in this environment; appears unrelated to Products work.

Scope trims (confirmed)

  • Removed “Convert Service Catalog Item to Product” UI: users can create products directly; no explicit in-app “convert service → product” workflow is required for V1.