Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
7.0 KiB
PRD — Extension Invoicing (Manual Invoice MVP)
- Slug:
extension-invoicing-manual-invoices - Date:
2026-01-14 - Status: Draft
Summary
Expose a minimal invoicing host API to extensions so an extension can create a draft manual invoice for a client inside the tenant where it is installed. Access is controlled via the existing extension capabilities model (granted at install time) and surfaced to extension code via a new host.invoicing interface.
Problem
Extensions currently cannot create invoices using first-class platform primitives. Extension authors who need to bill for work performed, usage synced from third parties, or one-off charges must either:
- ask admins to create invoices manually in the UI, or
- attempt to call internal/private APIs via proxy patterns not designed for secure platform integration.
We want a least-privilege, capability-gated way for extensions to create invoices.
Goals
- Add a new capability to grant invoice creation access:
cap:invoice.manual.create. - Provide a host API (
host.invoicing.createManualInvoice) usable from extension WASM handlers. - Reuse existing manual invoice creation logic (tax distribution, totals, numbering) where possible.
- Ensure invoices are created in the correct tenant and are attributable (user-driven where possible).
Non-goals (MVP)
- Finalizing/sending invoices, taking payments, refunds, credit application.
- Updating or deleting invoices.
- Adding/editing invoice templates or PDFs.
- Creating service catalog items / services on the fly.
- A new UI flow in core Alga PSA (extensions can build their own UI and call their handler).
Users and Primary Flows
Primary persona: Extension developer building billing automation for a tenant.
Flow: Create a manual invoice from an extension UI
- Tenant admin installs extension and grants
cap:invoice.manual.create. - Extension UI collects
clientIdand line items (mapped toserviceIds known to the tenant). - UI calls the extension handler (e.g.,
POST /create-invoicevia the usual proxy pattern). - Handler calls
host.invoicing.createManualInvoice(...). - Handler returns invoice identifiers/totals so the UI can show confirmation and deep-link to the invoice.
Flow: Create a manual invoice from a non-UI integration
- An external system triggers the extension handler (webhook, scheduled task, etc.).
- Handler calls
host.invoicing.createManualInvoice(...). - Handler returns created invoice details for downstream sync/notification.
Capability + Host API Design
Capability
- New capability string:
cap:invoice.manual.create - Not granted by default; must be explicitly granted per install.
Host bindings (TypeScript SDK)
Add a new invoicing namespace to HostBindings:
export interface ManualInvoiceItemInput {
serviceId: string; // UUID (required)
quantity: number; // > 0
description: string;
rate: number; // >= 0 (minor units; align with existing manual invoice API)
isDiscount?: boolean;
discountType?: 'percentage' | 'fixed';
appliesToItemId?: string;
appliesToServiceId?: string;
}
export interface CreateManualInvoiceInput {
clientId: string; // UUID
items: ManualInvoiceItemInput[]; // min 1
// Header fields (MVP)
invoiceDate?: string; // YYYY-MM-DD (defaults to "today" in tenant timezone)
dueDate?: string; // YYYY-MM-DD (defaults to invoiceDate for MVP)
poNumber?: string | null; // optional client PO number snapshot
}
export type CreateManualInvoiceResult =
| { success: true; invoice: { invoiceId: string; invoiceNumber: string; status: string; subtotal: number; tax: number; total: number } }
| { success: false; error: string; fieldErrors?: Record<string, string> };
export interface InvoicingHost {
createManualInvoice(input: CreateManualInvoiceInput): Promise<CreateManualInvoiceResult>;
}
Notes:
- Keep the returned invoice payload intentionally small for the WASM boundary (IDs + totals). We can add richer “get invoice” capabilities later.
- Prefer camelCase in the JS SDK, and map to snake_case as needed on the wire.
Host/Runner Wiring (How it’s exposed)
Runner ↔ Server internal API
Mirror the scheduler host API pattern:
- Runner calls an internal host route on the Alga server:
POST /api/internal/ext-invoicing/install/{installId}- Auth via
x-runner-authtoken (RUNNER_STORAGE_API_TOKENorRUNNER_SERVICE_TOKEN). - Server resolves
installId → tenantIdvia@ee/lib/extensions/installConfig.
- Request includes an
operationdiscriminator (createManualInvoice) and payload.
User attribution + permissions (decision needed)
MVP decision: capability-only authorization.
- If
cap:invoice.manual.createis granted, invoices can be created even when no end-user is present (e.g., scheduled tasks). - Attribution uses a stopgap “system principal” strategy: invoice charge rows are created with a tenant user id fallback (first
users.user_idfor the tenant). This preserves tenant scoping but is not a final audit model; follow-up work should introduce a dedicated extension/system principal representation.
Data / Integration Notes
- Reuse existing manual invoice logic where possible:
- Prefer using lower-level helpers used by manual invoice creation (
persistManualInvoiceCharges, tax distribution, totals update), since MVP does not require a user context.
- Prefer using lower-level helpers used by manual invoice creation (
- Required tenant data:
clientIdmust exist in tenant.serviceIdmust exist in tenant (extensions must be configured with service IDs, or use existing tenant services).- Client billing email / tax settings requirements should match the core manual invoice behavior.
Risks / Constraints
- Capability naming and future expansion: ensure the capability name leaves room for
cap:invoice.read,cap:invoice.finalize, etc. - Attribution and audit: invoice creation is sensitive; capability-only authorization may be surprising if not clearly surfaced in the install UI.
- Tax requirements: manual invoice creation may fail if client tax region/settings are incomplete; the host API must return clear errors.
Open Questions
- Attribution: what is the best representation for “created by extension” in invoice/audit records (installId marker vs dedicated service user)? (MVP uses tenant user id fallback; replace with a dedicated model.)
- Should we expose currency selection in MVP (header-level
currencyCode) or rely on tenant/client defaults only?
Acceptance Criteria / Definition of Done
- A tenant can grant
cap:invoice.manual.createto an extension install. - An extension handler can call
host.invoicing.createManualInvoice(...)and receive a success result with invoice identifiers. - Created invoices are tenant-scoped and appear as manual draft invoices in existing invoice views.
- Unauthorized calls (missing capability, missing/invalid attribution inputs) fail with structured, debuggable errors.
- A sample extension exists in
sdk/samples/component/invoicing-demo/demonstrating an extension UI creating a draft manual invoice.