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
8.0 KiB
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
IServicePriceand 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 optionallyservice_category(server/src/lib/services/accountingMappingResolver.ts).
Billing engine gaps
BillingEngine.calculateProductChargesandcalculateLicenseChargesexist but are placeholders returning an emptyplanServiceslist (server/src/lib/billing/billingEngine.ts).- There are TODOs implying
service_catalog.service_typeis missing, but migrations show the schema has evolved; current app code relies oncustom_service_type_id+billing_methodinstead.
Contracts / line types
contract_lines.contract_line_typeis stored as free text in the DB (server/migrations/20251008000001_rename_billing_to_contracts.cjs), but TypeScript types inserver/src/interfaces/billing.interfaces.tsand 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.
Working hypothesis (recommended direction)
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.
Links / references
- 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_catalogsubset/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/srcrg -n "service_catalog" -S server/src ee docs
Implementation gotchas / notes
getServices()now defaults toitem_kind: 'service'to preserve legacy expectations; usegetServices(..., { item_kind: 'product' })or{ item_kind: 'any' }explicitly when you need products included.server/src/components/ui/Buttonrequires anidprop; 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).
- are hidden from product pickers by default (pickers now request
- 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 > 0for non-discount line items.
- Service catalog mutations (create/update/delete) and service price mutations now enforce RBAC via
- 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.tsxto search services + products (filters bybilling_method,item_kind,is_active). - Wired into contract dialogs that previously loaded
getServices(1, 999, ...)and filtered client-side.
- Added
- Product update reliability:
Service.update()now stripsundefinedkeys before calling Knexupdate()to avoid invalid/undefined bindings when optional product fields are omitted. - Product categories (V1): Products use the existing
service_categoriesreference data viaservice_catalog.category_idfor controlled categorization (filtering + accounting mapping); the legacy freeformproduct_categoryis 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/productsand/api/v1/products/{id}provide product catalog CRUD over API keys (RBAC resource:service)./api/v1/serviceslist query now supportsitem_kind(service|product|any) andis_activefilters;billing_methodnow includesper_unit.- Contract Lines API v2 now resolves plan services from
service_catalog(notservices) and acceptsper_unitbilling methods for product services.
- OpenAPI/metadata:
MetadataService.discoverSchemas()originally looked forserver/src/lib/api/schemasrelative toprocess.cwd(), which is wrong in Next (cwd isserver/). Fixed to scansrc/lib/api/schemas, added support forz.object(shapeName)schema patterns, and tagged/api/v1/productsunderConfiguration. - Invoice preview (drafts) bug: Draft invoice preview failed with Postgres error
column reference "tenant" is ambiguousdue to an unqualified.where({ tenant })after joininginvoice_charges as icwithservice_catalog as scinInvoice.getInvoiceCharges. Fixed by qualifying the where clause toic.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.
- Dev stack note: In this worktree the Next.js server runs from a built image (not a bind-mount), so code changes may require
- 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(useContextnull) in this environment; appears unrelated to Products work.
- TypeScript passes under
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.