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

144 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
1. Tenant admin installs extension and grants `cap:invoice.manual.create`.
2. Extension UI collects `clientId` and line items (mapped to `serviceId`s known to the tenant).
3. UI calls the extension handler (e.g., `POST /create-invoice` via the usual proxy pattern).
4. Handler calls `host.invoicing.createManualInvoice(...)`.
5. 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
1. An external system triggers the extension handler (webhook, scheduled task, etc.).
2. Handler calls `host.invoicing.createManualInvoice(...)`.
3. 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`:
```ts
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 its 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-auth` token (`RUNNER_STORAGE_API_TOKEN` or `RUNNER_SERVICE_TOKEN`).
- Server resolves `installId → tenantId` via `@ee/lib/extensions/installConfig`.
- Request includes an `operation` discriminator (`createManualInvoice`) and payload.
### User attribution + permissions (decision needed)
MVP decision: **capability-only authorization**.
- If `cap:invoice.manual.create` is 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_id` for 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.
- Required tenant data:
- `clientId` must exist in tenant.
- `serviceId` must 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
1. 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.)
2. 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.create` to 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.