P9 A Invoice Activation Runbook
Source: docs/operations/p9-a-invoice-activation-runbook.md
# P9-A Invoice Activation Runbook
Operational runbook for first-customer gain-share invoice activation in staging.
## Scope
- Validate billing readiness for one tenant.
- Generate/cancel draft invoices safely for validation.
- Optionally seed temporary `RECOVERED` value events when staging has no billable data.
Core script:
- `apps/api/scripts/invoice-dry-run.ts`
- wrapper: `scripts/run-invoice-dry-run.sh`
## Prerequisites
- `DATABASE_URL` for staging DB.
- `pnpm` installed locally.
- Tenant UUID for the target customer.
Find tenant UUID:
```bash
DATABASE_URL='<staging-db-url>' \
psql "$DATABASE_URL" -c "SELECT id, name, slug, status FROM app.tenants ORDER BY name;"
```
## Step 1: Read-only Dry Run
Run this first. It does not create invoices.
```bash
DATABASE_URL='<staging-db-url>' \
scripts/run-invoice-dry-run.sh \
--tenant-id='<tenant-uuid>' \
--period-start=2026-02 \
--period-end=2026-03 \
--verbose
```
Expected pass criteria:
- tenant exists and is active
- billable `RECOVERED` events exist in period
- no overlapping draft/issued invoices
- gain-share total is computed
## Step 2: Execute + Cleanup Validation
This validates draft generation and amount reconciliation, then removes the draft.
```bash
DATABASE_URL='<staging-db-url>' \
scripts/run-invoice-dry-run.sh \
--tenant-id='<tenant-uuid>' \
--period-start=2026-02 \
--period-end=2026-03 \
--execute \
--cleanup \
--verbose
```
Expected pass criteria:
- draft invoice generated
- draft amount matches expected gain-share
- draft cleanup succeeds
## Step 3: If No Billable Events Exist
If dry-run reports no unbilled billable `RECOVERED` events, seed temporary rows.
> **Note:** Validate the UUID format before substitution — the wrapper script
> (`scripts/run-invoice-dry-run.sh`) does this automatically, but the raw `psql`
> path below relies on the `::uuid` cast as a server-side guard.
```bash
DATABASE_URL='<staging-db-url>'
TENANT_ID='<tenant-uuid>'
psql "$DATABASE_URL" <<SQL
INSERT INTO app.value_events (
id,
tenant_id,
category,
amount,
currency,
source_entity_type,
source_entity_id,
finding_type,
reason_code,
idempotency_key,
billable,
billed_at,
occurred_at,
created_at
)
VALUES
(gen_random_uuid(), '$TENANT_ID'::uuid, 'RECOVERED', 1200.50, 'USD', 'SHIP_FINDING', gen_random_uuid(), 'DUPLICATE_CHARGE', 'DRY_RUN', 'dryrun-2026-02-001', true, NULL, '2026-02-10T12:00:00Z'::timestamptz, now()),
(gen_random_uuid(), '$TENANT_ID'::uuid, 'RECOVERED', 875.25, 'USD', 'SHIP_FINDING', gen_random_uuid(), 'ADDRESS_CORRECTION', 'DRY_RUN', 'dryrun-2026-02-002', true, NULL, '2026-02-14T12:00:00Z'::timestamptz, now()),
(gen_random_uuid(), '$TENANT_ID'::uuid, 'RECOVERED', 390.00, 'USD', 'SIMA_RESULT', gen_random_uuid(), 'CLASSIFICATION_ERROR', 'DRY_RUN', 'dryrun-2026-02-003', true, NULL, '2026-02-18T12:00:00Z'::timestamptz, now());
SQL
```
Re-run Steps 1 and 2.
Then clean up seed rows:
```bash
DATABASE_URL='<staging-db-url>' \
psql "$DATABASE_URL" -c "DELETE FROM app.value_events WHERE idempotency_key LIKE 'dryrun-2026-02-%';"
```
## Step 4: Manual Invoice Lifecycle (optional)
Use this when validating issue/paid transitions beyond draft checks.
1. Generate draft through admin billing endpoint.
2. Issue invoice (`POST /api/admin/billing/invoices/{id}/issue`) with body `{ "tenantId": "..." }`.
3. Mark paid (`POST /api/admin/billing/invoices/{id}/mark-paid`) with body `{ "tenantId": "..." }`.
4. Verify status transitions in `app.billing_invoices`.
## Evidence to Capture
Store these in session notes or PR comments:
- tenant ID + period used
- dry-run output summary (checks passed/failed)
- generated invoice number (if execute mode used)
- any seeded value-event keys and cleanup confirmation
## Exit Criteria Mapping (P9-A)
P9-A is operationally ready when all are true:
- read-only dry-run passes on staging for target tenant
- execute+cleanup dry-run passes
- gain-share math is validated against expected totals
- no residual dry-run seed data remains