Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
24 KiB
MSP Billing System Design
System Purpose
The billing platform supports the full contract-centric workflow used by managed service providers. It replaces the older "billing plan" and "bundle" language with contract lines, client contracts, and contract templates. Clients can combine fixed recurring fees, hourly work, consumption-based services, license passthrough, and prepayment credits under a single contract umbrella. The billing engine produces detailed charge breakdowns, applies discounts, reconciles credits, and feeds those results into the invoicing subsystem.
Key goals:
- Represent reusable offer structures only through templates while keeping instantiated contract headers client-owned.
- Allow multiple simultaneous contract lines per client, each with its own pricing configuration.
- Capture time, usage, and product charges without losing the context required for auditing or taxation.
- Harmonize manual invoices and automated contract billing through the same taxation and transaction pipelines.
Updated Domain Vocabulary
| Term | Description | Primary Tables |
|---|---|---|
| Contract template | A reusable blueprint for a contract, including recommended lines, default billing frequencies, and metadata. | contract_templates, contract_template_lines, contract_template_line_services, contract_template_line_service_configuration, contract_template_line_service_* |
| Contract | A client-owned instantiated contract header. Non-template contracts belong to exactly one owning client and hold the line/configuration data used for billing. | contracts, contract_lines, contract_pricing_schedules |
| Client contract | The assignment/lifecycle record for a client-owned contract header, including start/end dates, PO requirements, renewal workflow state, and live status. | client_contracts |
| Contract line | A billable line definition (fixed, hourly, usage, bucket, product, license). | contract_lines, contract_line_fixed_config, contract_line_service_configuration, contract_line_service_* |
| Client contract line | A client-scoped instance of a contract line. Stores cloned template data, pricing overrides, and service configuration snapshots. | client_contract_lines, client_contract_line_pricing, client_contract_line_terms, client_contract_services, client_contract_service_configuration, client_contract_service_* |
| Billing cycle | Defines the cadence for invoicing a client. | client_billing_cycles, retrieved through BillingEngine.getBillingCycle |
Supporting entities still in use:
time_entries,usage_tracking,bucket_usage,contract_line_discounts,discountshold the underlying activity feeding the engine.invoices,invoice_items,invoice_item_details,invoice_item_fixed_detailsstore rendered billing output.transactions,credit_tracking,credit_reconciliation_reportsmaintain the financial ledger and credit balances.
Products (Catalog → Contracts → Invoices)
Products are implemented as a subset of the existing service catalog:
- Catalog rows live in
service_catalogwithitem_kind = 'product'. - Products are quantity-based sellable items (
billing_method = 'per_unit'in V1). - Multi-currency pricing is stored in
service_prices(one row per currency).service_catalog.default_ratemirrors the “primary” price for convenience. - Tax behavior is driven by
service_catalog.tax_rate_id(nullable = non-taxable). - Optional license metadata (term/cadence) is stored on the catalog row (
is_license,license_term,license_billing_cadence) without start/end/proration in V1.
Attaching products to contracts
- Contract templates and contract wizards allow attaching products as recommended/default items (with quantity) and cloning them into client snapshots.
- When a contract is assigned to a client, the attached products are stored as
client_contract_servicesfor the relevantclient_contract_line_id. - Rate selection follows the normal hierarchy (catalog price for the contract currency unless an override is explicitly entered at the client/contract level).
Billing and invoice output
- During invoice generation, products are translated into
type = 'product'charges in the billing engine and persisted asinvoice_items+ details rows. - Products appear on invoices as distinct line items (and should be visually distinguished from time/usage/services in templates and client portal views).
- Currency correctness rules:
- The contract currency controls which
service_prices.currency_codeis used. - If a product lacks a price in the contract currency, billing requires an explicit override (to avoid accidental $0 lines).
- The contract currency controls which
Taxes and accounting mapping
- Tax calculation uses the same invoice persistence pipeline (
invoiceService.calculateAndDistributeTax) and applies the product’stax_rate_id(or falls back to client defaults depending on tenant tax configuration). - Accounting exports and mappings treat products as catalog items; they reuse the existing mapping resolution paths for
service/service_categorywhere applicable.
Data Model Layers
Template Layer (authoring reusable offers)
Templates give sales and operations teams a curated starting point.
contract_templates– high level metadata (name, default frequency, status, optional JSON metadata).contract_template_lines– line-level defaults (type, descriptions, frequency, overtime rules) along with display order, billing timing, and optional template-level custom rates.contract_template_line_terms– stores timing/terms metadata for template lines, including the newbilling_timingflag (arrearsoradvance) that seeds client assignments.contract_template_line_services– recommended catalog services and default quantities for a template line.contract_template_line_service_configurationand child tables (_fixed,_hourly,_usage,_bucket) – configuration defaults for each service type.
Publishing or cloning a template never mutates the template tables; instead, the structure is copied into the client-specific tables via cloneTemplateContractLine (server/src/lib/billing/utils/templateClone.ts).
Client-Owned Contract Header Layer
Contracts live in tenant scope and are managed through server/src/lib/actions/contractActions.ts, but non-template rows are no longer a reusable shared library concept.
contracts– stores instantiated contract headers. Non-template rows must carryowner_client_id. Headerstatusremains useful for draft/header workflow, but live client lifecycle now comes fromclient_contracts.contract_lines– stores contract-specific lines (including display order, billing timing, and optional custom rates) alongside shared metadata (contract_line_name, type, frequency, etc.).contract_pricing_schedules– time-bound overrides that swap in a custom rate when a schedule is effective during billing.
Contracts can be created manually for a client or cloned from templates into a client-owned draft. When cloning, template IDs are preserved in contracts.template_metadata for traceability, but templates remain the only reusable definition layer.
Client Instance Layer
When a client-owned contract header is assigned or activated, the system snapshots all relevant data:
client_contracts– assignment/lifecycle record with start/end dates, PO requirements, renewal workflow state, and live status semantics. Managed byclientContractActions(server/src/lib/actions/client-actions/clientContractActions.ts).- Purchase order fields:
po_required(boolean) – blocks invoice generation when enabled andpo_numberis missing.po_number(text) – the customer’s PO reference used on invoices and accounting exports.po_amount(bigint cents, nullable) – advisory “authorized spend” amount for warning on overages.
- Purchase order fields:
client_contract_lines– individual lines the client receives. Each record may reference both the base contract line and the originating template line (for audits).client_contract_line_terms– per-client billing rules (frequency, overtime, rounding) plus thebilling_timingsetting that determines whether a line bills in advance or arrears.client_contract_line_pricing– the rate strategy used for the client instance. Stores template references and overrides applied either by templates, contracts, or pricing schedules.client_contract_services– concrete service list attached to a client line with tenant-specific quantity and rate overrides.client_contract_service_configurationand_fixed,_hourly,_usage,_bucket,_rate_tiers– cloned configuration records so the billing engine never reads template tables during invoice generation.client_contract_line_discounts– optional mapping of discounts to client contract lines.
The cloning helper ensures that future template edits do not retroactively change existing client contracts while still allowing the UI to surface “template vs client” differences.
Purchase Orders (PO)
Alga stores PO context on the client contract assignment (client_contracts) and snapshots the PO number onto invoices generated from contract billing:
invoices.po_number– stored as a snapshot at invoice creation time.invoices.client_contract_id– links an invoice back to the generating contract assignment for PO consumption tracking.
PO limits (advisory)
If client_contracts.po_amount is set, billing computes warnings when a newly generated invoice would exceed the remaining authorized spend:
- Remaining is calculated from finalized invoices for the same
client_contract_id. - Overages are warnings (invoice generation is not blocked), but UI flows prompt users to allow or skip overages during batch invoicing.
Activity & Reference Data
- Time –
time_entries,user_type_rates, approval workflow tables. - Usage –
usage_tracking,usage_summary, relevant service configuration. - Buckets –
bucket_usagetracks consumption for retainer-style offerings. - Catalog –
service_catalog,service_categoriesfeed descriptions, default rates, and tax attributes. - Tax –
tax_rates,client_tax_settings,client_tax_rates(default region lookup), with helpers inclientTaxRateActions. - Discounts & Adjustments –
discounts,contract_line_discounts, plannedadjustmentstable for ad-hoc corrections.
Contract Lifecycle
- Author or import template – users manage templates through
ContractTemplateModelactions (server/src/lib/models/contractTemplate.ts) and UI inserver/src/components/billing-dashboard/contracts/templates/*. - Create contract –
createContractincontractActions.tscreates a client-owned contract header for one owning client. Templates can be cloned using wizard actions (contractWizardActions.ts) to seed contract lines and metadata. - Attach contract lines – server actions in
server/src/lib/actions/contractActions.tscall the shared repository to clone template lines intocontract_lines(or update template snapshots) while exposingaddContractLine(contractId, contractLineId, customRate?)to callers. - Assign to client –
assignContractToClient(clientId, contractId, startDate, endDate?)fromclientContractActions.tscreatesclient_contractsrows. For non-template contracts, the assignment client must matchcontracts.owner_client_id. Concurrent active assignments (including overlapping dates) are allowed; mixed-currency active assignments for the same client remain blocked as a separate policy. - Clone template data – if the assignment originated from a template,
cloneTemplateContractLinecopies default terms, services, and configuration into the client tables. Additional overrides can be applied throughclientContractLineActionsandclientContractServiceActions. - Maintain lifecycle –
updateContract,updateClientContract, and the pricing schedule actions keep data in sync as contracts renew, expire, or are repriced. Live status shown in UI/reporting derives fromclient_contracts, notcontracts.status.
Contracts API Semantics
/api/v1/contractsand/api/v2/contractsrepresent client-owned instantiated contract headers.- These endpoints exclude reusable templates; template authoring and listing lives under the dedicated template APIs/UI.
- Contract responses include
owner_client_id, and list responses also expose the owner client name so consumers do not have to infer ownership fromclient_contracts. - Live client lifecycle still belongs to
client_contracts;/contractsshould be treated as the header/line owner resource, not the live status fact table.
Example (simplified):
import { createContract } from 'server/src/lib/actions/contractActions';
import { addContractLine } from 'server/src/lib/actions/contractActions';
import { assignContractToClient } from 'server/src/lib/actions/client-actions/clientContractActions';
const contract = await createContract({
contract_name: 'Standard MSP Package',
contract_description: 'Baseline services for managed clients',
owner_client_id: 'client-id',
billing_frequency: 'monthly',
status: 'draft',
is_active: false
});
await addContractLine(contract.contract_id, 'support-contract-line-id');
await addContractLine(contract.contract_id, 'security-contract-line-id', 12999); // cents override
await assignContractToClient('client-id', contract.contract_id, '2025-01-01', null);
Billing Engine Flow
The billing engine lives in server/src/lib/billing/billingEngine.ts. It operates per client and billing cycle and returns an IBillingResult consumed by invoice generation.
- Initialize tenant context –
createTenantKnex()establishes the multi-tenant connection. - Load billing cycle –
client_billing_cyclesprovides effective date ranges using[start, end)semantics (end is exclusive). If no explicit period exists, the engine derives it from the client’s frequency. - Guard rails –
validateBillingPeriodensures the requested range does not span cycle changes. Existing invoices are detected viahasExistingInvoiceForCycle. - Collect client contract lines –
getClientContractLinesAndCyclejoinsclient_contract_lines,contract_lines,client_contract_line_pricing,client_contract_line_terms, and the parent contract to build a normalized in-memory model. Template references are resolved so both template-sourced and bespoke lines participate. - Charge calculation – for each client contract line the engine executes:
calculateFixedPriceCharges– handles fixed-fee lines. Custom rates fromclient_contract_line_pricingor activecontract_pricing_schedulesshort-circuit to a single consolidated charge. Otherwise the function gathers services fromclient_contract_services+ configuration tables, derives FMV allocations, prorates when required, and calculates per-service tax usingTaxService.calculateTimeBasedCharges– pulls approvedtime_entriestied to the line, respecting overtime rules, user type overrides, and rounding settings fromclient_contract_line_termsandclient_contract_service_hourly_config.calculateUsageBasedCharges– consumesusage_tracking, applies tiered pricing viaclient_contract_service_rate_tiers, and producesIUsageBasedChargeentries.calculateBucketPlanCharges– reconcilesbucket_usagerollovers and overages for retainer-style offerings.calculateProductChargesandcalculateLicenseCharges– forward-fill catalog-driven passthrough items such as licenses. These functions rely onclient_contract_servicesto know which catalog items to include for the period.
- Proration –
applyProrationToPlanprorates fixed charges when the contract line starts or ends mid-cycle based on settings captured in the client term snapshot. - Discounts & adjustments –
applyDiscountsAndAdjustmentslooks for active discounts incontract_line_discountsfor the client. Adjustment support is scaffolded (fetchAdjustments) and is the next planned enhancement. - Tax normalization – the billing engine gathers preliminary tax data, but final calculation happens during invoice persistence via
calculateAndDistributeTaxinserver/src/lib/services/invoiceService.ts. That service reconciles rounding and ensures totals align with jurisdiction rules. - Result delivery – the engine returns
{ charges, totalAmount, discounts, adjustments, finalAmount }. Upstream actions persist the charges to invoices and generate transactions.
Discount & Pricing Inputs
discountsandcontract_line_discountsdefine percentage or fixed discounts with effective windows.- Pricing hierarchy: template defaults → contract-level overrides (
contract_lines.custom_rate) → active pricing schedule (contract_pricing_schedules) → client-specific overrides (client_contract_line_pricing.custom_rate). The first non-null value in that chain wins.
Data Quality & Validation
- Client overlap checks exist in both contract assignment (
ClientContract.assignContractToClient) and contract updates (updateClientContract). clientContractLineActionsenforce that contract lines referenced by existing invoices cannot be removed without safe handling.- Recurring invoice generation blocks matching unapproved billable time instead of moving time entries into a later billing period.
Manual Invoicing
Manual invoices share the same persistence and taxation pipeline as automated billing.
import { generateManualInvoice, updateManualInvoice } from 'server/src/lib/actions/manualInvoiceActions';
await generateManualInvoice({
clientId: 'client-id',
items: [
{
service_id: 'service-id',
quantity: 2,
description: 'Ad hoc consulting',
rate: 150 // dollars; the action converts to cents
}
]
});
await updateManualInvoice(existingInvoiceId, {
clientId: 'client-id',
items: [...]
});
- Items are stored in
invoice_items; detailed breakdown stays consistent with automated invoices. - Tax uses the same
TaxService+invoiceService.calculateAndDistributeTaxflow. - Ledger entries are recorded in
transactions(invoice_generated,invoice_adjustment). - UI entry points:
server/src/components/billing-dashboard/ManualInvoices.tsxandInvoices.tsx.
Credits & Transactions
Credits are issued for prepayments, negative invoices, and manual adjustments.
transactionscontains authoritative ledger events (credit_issuance,credit_application,credit_expiration, etc.).credit_trackingmirrors each credit’s remaining balance and expiration.credit_reconciliation_reportscaptures discrepancies detected by scheduled validation jobs (creditReconciliationValidationand friends).- Application and expiration logic is implemented in
server/src/lib/actions/creditActions.tsand background handlers inserver/src/lib/jobs/handlers/*Credits*.ts.
Invoice Template Selection
Invoice rendering uses WebAssembly-based templates (see Invoice Template System). Default selection data lives in invoice_template_assignments:
scope_typeis currently either'tenant'or'company'(the legacy label for client-specific defaults). The schema still uses'company'to avoid breaking existing data, even though the UI now surfaces the entity as “client”.- Each record is exclusive: either
standard_invoice_template_code(for standard templates) orinvoice_template_id(for custom templates) is populated. - The tenant-level default is discovered by querying scope
(tenant, 'tenant', NULL); client overrides use(tenant, 'company', client_id). - Legacy columns
invoice_templates.is_defaultandclients.invoice_template_idare kept in sync for backward compatibility but should be treated as derived data.
Key Interfaces
The TypeScript interfaces in server/src/interfaces/billing.interfaces.ts and contract.interfaces.ts describe the shapes returned by the engine:
// server/src/interfaces/billing.interfaces.ts:18
export interface IBillingResult extends TenantEntity {
charges: IBillingCharge[];
totalAmount: number;
discounts: IDiscount[];
adjustments: IAdjustment[];
finalAmount: number;
}
export interface IBillingCharge extends TenantEntity {
type: 'fixed' | 'time' | 'usage' | 'bucket' | 'product' | 'license';
serviceId?: string;
serviceName: string;
quantity?: number;
rate: number; // cents
total: number; // cents
tax_amount: number;
tax_rate: number;
tax_region?: string;
is_taxable?: boolean;
client_contract_line_id?: string;
client_contract_id?: string;
contract_name?: string;
}
export interface IFixedPriceCharge extends IBillingCharge {
type: 'fixed';
quantity: number;
config_id?: string;
base_rate?: number;
fmv?: number;
proportion?: number;
allocated_amount?: number;
enable_proration?: boolean;
billing_cycle_alignment?: string;
}
export interface IClientContractLine extends TenantEntity {
client_contract_line_id: string;
client_id: string;
contract_line_id: string;
template_contract_line_id?: string;
service_category?: string;
start_date: ISO8601String;
end_date: ISO8601String | null;
is_active: boolean;
custom_rate?: number;
client_contract_id?: string;
template_contract_id?: string | null;
contract_id?: string;
contract_line_name?: string;
billing_frequency?: string;
contract_name?: string;
}
These interfaces are consumed throughout the billing dashboard (ClientContractLineDashboard, BillingOverview) and the billing engine.
Database Quick Reference
| Area | Tables |
|---|---|
| Templates | contract_templates, contract_template_lines, contract_template_line_services, contract_template_line_service_configuration, contract_template_line_service_fixed_config, contract_template_line_service_hourly_config, contract_template_line_service_usage_config, contract_template_line_service_bucket_config |
| Contract library | contracts, contract_lines, contract_pricing_schedules, contract_line_fixed_config, contract_line_service_configuration, contract_line_service_fixed_config, contract_line_service_hourly_config, contract_line_service_usage_config, contract_line_service_bucket_config, contract_line_service_rate_tiers, contract_line_discounts |
| Client instances | client_contracts, client_contract_lines, client_contract_line_pricing, client_contract_line_terms, client_contract_services, client_contract_service_configuration, client_contract_service_fixed_config, client_contract_service_hourly_config, client_contract_service_usage_config, client_contract_service_bucket_config, client_contract_service_rate_tiers, client_contract_line_discounts |
| Activity | time_entries, usage_tracking, bucket_usage, license_assignments, product_usage |
| Invoicing | client_billing_cycles, invoices, invoice_items, invoice_item_details, invoice_item_fixed_details, invoice_template_assignments |
| Finance | transactions, credit_tracking, credit_reconciliation_reports, client_credits |
| Tax | tax_rates, client_tax_settings, client_tax_rates |
Key column highlights:
contract_template_line_terms/client_contract_line_termsnow include abilling_timingflag (arrearsoradvance) that drives line-level billing behaviour.invoice_item_detailsincludesservice_period_start,service_period_end, andbilling_timingso invoices can represent advance and arrears charges on a single document.
References
server/src/lib/billing/billingEngine.tsserver/src/lib/actions/contractActions.tsserver/src/lib/repositories/contractLineRepository.tsserver/src/lib/actions/client-actions/clientContractActions.tsserver/src/lib/actions/client-actions/clientContractLineActions.tsserver/src/lib/billing/utils/templateClone.tsserver/src/lib/services/invoiceService.tsserver/src/lib/actions/manualInvoiceActions.tsdocs/invoice_templates.md
This document reflects the current contract-line architecture and should be updated alongside schema migrations affecting the billing domain.