diff --git a/SECURITY_HARDENING_APPLIED_REPORT.md b/SECURITY_HARDENING_APPLIED_REPORT.md new file mode 100644 index 0000000..89a56e9 --- /dev/null +++ b/SECURITY_HARDENING_APPLIED_REPORT.md @@ -0,0 +1,160 @@ +# Security Hardening Application Report + +Generated: 2026-06-09 +Project: RentalDriveGo / Car Management System +Input archive: `/mnt/data/car_management_system_plan_applied(1).zip` +Plan applied: `/mnt/data/SECURITY_HARDENING_IMPLEMENTATION_PLAN(1).md` + +## Executive result + +The uploaded project was inspected and the security hardening plan was applied as far as safely possible inside the source archive. The archive already contained a broad previous hardening pass. I did not blindly trust that state, because that is how software becomes an expensive apology letter. I re-audited the implementation against the plan and applied additional corrections where the code still contradicted the target security model. + +The final output includes a patched project archive, this report, a changed-file list, and an incremental diff for the additional changes made during this pass. + +## What was already present in the uploaded project + +The project already contained many of the plan-aligned building blocks: + +- Centralized JWT helpers for actor tokens. +- HttpOnly session cookie helpers for admin, employee, and renter sessions. +- Company authorization policy helpers. +- Admin 2FA and fresh-2FA enforcement middleware. +- Public booking access-token helpers. +- Webhook idempotency helpers. +- Hashed `CompanyApiKey` model and migration. +- `ReservationPublicAccess` model and migration. +- `WebhookEvent` model and migration. +- Upload validation helpers and public/private storage separation. +- Request ID and sanitized error response middleware. +- Production Docker/Traefik hardening, including Redis authentication and `x-middleware-subrequest` blocking. +- Static security scan script and CI security gates. + +That base work was useful, but it had a few security and test-consistency gaps. + +## Additional changes applied in this pass + +### 1. Removed legacy plaintext company API key storage + +The plan requires company API keys to be hash-only. The code had introduced `CompanyApiKey`, but the legacy `Company.apiKey` field still existed in the Prisma schema and middleware still allowed a fallback lookup against that plaintext field when `ALLOW_LEGACY_COMPANY_API_KEYS=true`. + +Changes applied: + +- Removed `Company.apiKey` from `packages/database/prisma/schema.prisma`. +- Removed `apiKey` from the local `Company` TypeScript interface in `packages/database/src/index.ts` and `packages/database/src/index.d.ts`. +- Removed the legacy plaintext fallback path from `apps/api/src/middleware/requireApiKey.ts`. +- Added migration `20260609233000_drop_legacy_company_api_key` to drop the old column and unique index. +- Rewrote `requireApiKey` tests around prefix lookup, hash comparison, revocation, and `lastUsedAt` updates. + +Security effect: raw company API keys are no longer accepted through the legacy company column and are no longer represented in the current Prisma schema. + +### 2. Centralized Socket.io token verification + +The main API process still verified Socket.io auth tokens directly with `jwt.verify(token, JWT_SECRET)` and did not enforce issuer, audience, actor type, or allowed algorithm. This contradicted the session-authentication phase of the plan. + +Changes applied: + +- Added `verifyAnyActorToken()` to `apps/api/src/security/tokens.ts`. +- Updated `apps/api/src/index.ts` to use centralized actor-token verification for Socket.io authentication. +- Removed the direct `jsonwebtoken` import from the main API entrypoint. + +Security effect: Socket.io no longer accepts tokens that bypass the centralized actor-token constraints. + +### 3. Hardened employee password-reset JWT verification + +Employee password-reset tokens were signed and verified directly with the JWT secret and no issuer, audience, or algorithm constraints. + +Changes applied: + +- Added explicit `HS256` signing for employee password-reset tokens. +- Added issuer `rentaldrivego-api`. +- Added audience `employee_password_reset`. +- Added matching verification constraints. + +Security effect: reset tokens now reject wrong issuer, wrong audience, or wrong algorithm instead of relying on a bare shared-secret verification. + +### 4. Repaired test fixtures and middleware tests + +Some tests still expected pre-hardening behavior. That is a bad smell: tests defending old weaknesses are basically tiny lobbyists for future incidents. + +Changes applied: + +- Updated API-key middleware tests to validate hashed-key behavior instead of plaintext `Company.apiKey` lookup. +- Updated auth middleware tests to match the centralized token verifier behavior. +- Updated integration test helper token generation to use `signActorToken()` so generated test tokens include issuer and audience. + +Security effect: future test runs are less likely to push developers back toward weaker auth behavior. + +## Verification performed in this sandbox + +| Check | Result | Notes | +|---|---:|---| +| Static security scan | PASS | `npm run security:static` completed successfully. | +| Critical production dependency audit | PASS | `npm audit --package-lock-only --omit=dev --audit-level=critical` exited successfully. | +| JSON syntax check | PASS | Root and app `package.json` files and lockfile parsed successfully. | +| Shell syntax check | PASS | Shell scripts and production entrypoint parsed with `bash -n`. | +| YAML parse check | PASS | `docker-compose.production.yml` and `.gitlab-ci.yml` parsed successfully. | +| Node script syntax | PASS | `scripts/security-static-check.mjs` passed `node --check`. | +| Full dependency install | NOT COMPLETED | `npm ci --ignore-scripts --prefer-offline` could not complete in this sandbox. | +| Full type-check/test/build | NOT RUN | Requires dependencies to be installed. Run in CI or a normal development environment. | +| Docker compose config/render | NOT RUN | Docker is unavailable in this sandbox. | +| Prisma generate/migrate | NOT RUN | Requires dependency installation and a normal Prisma/DB environment. | + +## Dependency audit note + +The critical audit gate passes. The audit still reports moderate findings involving `postcss` through Next.js and `uuid` through Firebase/cron-related dependency chains. The lockfile’s suggested fixes require forced or breaking upgrades, so I did not casually smash the dependency graph with a hammer and call the mess “security.” Those should be handled in a controlled dependency-upgrade ticket with full frontend and notification regression testing. + +## Files changed by this pass + +- `apps/api/src/index.ts` +- `apps/api/src/middleware/requireApiKey.ts` +- `apps/api/src/middleware/requireApiKey.test.ts` +- `apps/api/src/middleware/requireCompanyAuth.test.ts` +- `apps/api/src/middleware/requireRenterAuth.test.ts` +- `apps/api/src/modules/auth/auth.employee.service.ts` +- `apps/api/src/security/tokens.ts` +- `apps/api/src/tests/helpers/fixtures.ts` +- `packages/database/prisma/schema.prisma` +- `packages/database/prisma/migrations/20260609233000_drop_legacy_company_api_key/migration.sql` +- `packages/database/src/index.ts` +- `packages/database/src/index.d.ts` + +See also: + +- `security_hardening_incremental.diff` +- `security_hardening_changed_files.txt` + +## Phase status against the hardening plan + +| Phase | Status | Evidence / caveat | +|---|---:|---| +| Phase 0: Emergency stabilization | Partial / needs operator action | Static scan passes, placeholders are present, but real production secret rotation cannot be performed inside the archive. | +| Phase 1: Sessions and authentication | Substantially applied | HttpOnly session helpers and centralized actor JWT verification exist; Socket.io and reset-token gaps were corrected in this pass. | +| Phase 2: Authorization and tenant isolation | Substantially applied | Company policy middleware exists; tenant-safe patterns are present in many modules. Full proof requires test suite and code review across all repository methods. | +| Phase 3: Public booking privacy | Applied at source level | Public access token model/helper and safe public booking flow are present. Must be verified with integration tests. | +| Phase 4: Admin hardening | Applied at source level | Mandatory 2FA and fresh-2FA middleware are present. Enrollment and recovery-code workflows still need production validation. | +| Phase 5: API key hardening | Strengthened in this pass | Legacy plaintext company API key storage/fallback removed; hash-only `CompanyApiKey` path remains. | +| Phase 6: Payments and webhooks | Applied at source level | Raw-body webhook handling and idempotency helpers are present. Must be verified against provider test events. | +| Phase 7: Upload and storage hardening | Applied at source level | Magic-byte validation and public/private storage split are present. Must be verified with upload abuse tests. | +| Phase 8: Rate limiting, errors, browser security | Applied at source level | Request IDs, sanitized errors, rate limit middleware, and security headers are present. Must be verified in deployed environment. | +| Phase 9: Deployment hardening | Applied at config level | Redis auth, non-public DB tooling, private networks, and Traefik header blocking are present. Must be verified on the real host. | +| Phase 10: Observability, auditability, jobs | Partial | Logging/audit hooks exist, but queue migration and operational observability need dedicated validation. | +| CI/CD security gates | Applied at config level | Security scan and critical audit gates exist; full CI must run outside this sandbox. | + +## Required follow-up before launch + +1. Rotate all real production secrets and invalidate old sessions/API keys where appropriate. +2. Run `npm ci` in CI or a normal development environment. +3. Run `npm run db:generate` and apply the new migration after backup. +4. Run full type-check, unit tests, integration tests, e2e tests, and builds. +5. Run payment-provider webhook test events using real sandbox provider signatures. +6. Verify public/private storage behavior with real uploaded files. +7. Verify Redis and PostgreSQL are not externally reachable from the production host. +8. Run container image build and Trivy scan. +9. Confirm admin 2FA enrollment and fresh-2FA gates before enabling privileged admin actions in production. +10. Document any deferrals with owner, risk acceptance, compensating control, deadline, and ticket number. + +## Launch recommendation + +Do not launch publicly yet based only on the patched archive. The source now better matches the plan, and the critical static/audit checks pass, but the hard launch gate still depends on full CI, Prisma migration validation, deployment verification, and production secret rotation. + +The patched archive is suitable for the next CI/staging pass. Treat it as implementation-ready source, not production clearance. Production clearance comes from reproducible evidence, not vibes in a ZIP file. diff --git a/SECURITY_HARDENING_LEFTOVER_APPLIED_REPORT.md b/SECURITY_HARDENING_LEFTOVER_APPLIED_REPORT.md new file mode 100644 index 0000000..f641a28 --- /dev/null +++ b/SECURITY_HARDENING_LEFTOVER_APPLIED_REPORT.md @@ -0,0 +1,255 @@ +# Security Hardening Leftover Application Report + +Project: RentalDriveGo / Car Management System +Input archive: `car_management_system_hardened_applied.zip` +Output archive: `car_management_system_leftover_applied.zip` +Date: 2026-06-09 + +## Executive Summary + +This pass applied the remaining source-level hardening gaps that were still practical to implement directly in the repository after the first hardening pass. The focus was on eliminating browser-readable authentication assumptions, enforcing app-layer blocking for the `x-middleware-subrequest` bypass class, improving actor-aware rate limiting, adding an admin 2FA recovery-code workflow, and bringing documentation/static checks into line with the hardened authentication model. + +This does not replace production operator work such as real secret rotation, live infrastructure verification, container scanning, applying migrations to a real database, or running the complete CI/test pipeline. Those items remain launch-gate evidence requirements. + +## Applied Changes + +### 1. Removed remaining browser-side employee token assumptions + +Changed files: + +- `apps/dashboard/src/lib/api.ts` +- `apps/dashboard/src/components/layout/TopBar.tsx` +- `apps/dashboard/src/components/layout/Sidebar.tsx` +- `apps/dashboard/src/app/(dashboard)/team/page.tsx` +- `apps/dashboard/src/app/sign-in/[[...sign-in]]/SignInPageClient.tsx` + +What changed: + +- Removed dashboard use of employee auth tokens from `localStorage`. +- Dashboard API calls now use `credentials: 'include'` and depend on HttpOnly cookies. +- Dashboard Socket.io connection now uses cookie credentials instead of script-provided auth tokens. +- Team page no longer decodes employee identity from a localStorage JWT. It resolves the current actor through `/auth/employee/me`. +- Admin 2FA sign-in form now accepts either a six-digit TOTP code or a recovery code value. + +Security effect: + +- Reduces script-readable authentication exposure. +- Aligns the dashboard with the intended HttpOnly session-cookie model. +- Prevents UI code from treating a readable JWT as the authority for employee identity. + +### 2. Added Socket.io HttpOnly-cookie session support + +Changed file: + +- `apps/api/src/index.ts` + +What changed: + +- Added Socket.io session-token extraction from HttpOnly cookies. +- Preserved explicit token verification for trusted non-browser/server contexts, while browser clients can now authenticate through cookies. +- Reused centralized actor-token verification. + +Security effect: + +- Real-time dashboard connections no longer require JavaScript-readable employee tokens. +- Socket authentication now follows the same actor-token validation path used elsewhere. + +### 3. Added app-layer `x-middleware-subrequest` blocking + +Changed files: + +- `apps/api/src/app.ts` +- `apps/dashboard/src/middleware.ts` +- `apps/marketplace/src/middleware.ts` +- `apps/admin/src/middleware.ts` +- `apps/dashboard/src/middleware.test.ts` +- `apps/marketplace/src/middleware.test.ts` + +What changed: + +- API now rejects requests containing `x-middleware-subrequest` before route handling. +- Dashboard, marketplace, and admin Next middleware now reject the same header at the app layer. +- Added/updated middleware tests for the rejection path. + +Security effect: + +- Adds defense in depth beyond reverse-proxy filtering. +- Prevents the project from depending on a single infrastructure control for this bypass class. + +### 4. Hardened admin/browser fetch behavior + +Changed files include: + +- `apps/admin/src/lib/api.ts` +- `apps/admin/src/app/dashboard/admin-users/page.tsx` +- `apps/admin/src/app/dashboard/renters/page.tsx` +- `apps/admin/src/app/dashboard/companies/[id]/page.tsx` +- `apps/admin/src/app/dashboard/containers/page.tsx` +- `apps/admin/src/app/dashboard/pricing/page.tsx` +- `apps/admin/src/app/forgot-password/page.tsx` +- `apps/admin/src/app/reset-password/page.tsx` + +What changed: + +- Admin API wrapper uses `credentials: 'include'`. +- Manual admin fetch calls now include credentials where they directly call the admin API. +- Removed dead placeholder `getToken()` helpers that returned empty strings and created meaningless `Authorization: Bearer ` headers. + +Security effect: + +- Admin browser requests now consistently rely on the HttpOnly admin session cookie. +- Removes misleading bearer-token scaffolding from the admin UI. + +### 5. Improved actor-aware rate limiting + +Changed files: + +- `apps/api/src/middleware/rateLimiter.ts` +- `apps/api/src/app.ts` + +What changed: + +- API rate-limit keys now prefer a verified actor identity from session cookies or valid Bearer tokens. +- Actor-aware rate limiting falls back safely to existing request actor fields or anonymous IP keys. +- Admin authentication routes now receive the stricter authentication limiter before the broader admin limiter. + +Security effect: + +- Authenticated traffic is limited by actor identity instead of only coarse IP data. +- Login and admin-auth abuse get stricter protection. +- Multi-container correctness still depends on Redis availability/configuration in production, which must be verified during deployment. + +### 6. Added admin 2FA recovery-code backend workflow + +Changed files: + +- `packages/database/prisma/schema.prisma` +- `packages/database/prisma/migrations/20260610001500_add_admin_recovery_codes/migration.sql` +- `apps/api/src/modules/admin/admin.repo.ts` +- `apps/api/src/modules/admin/admin.service.ts` +- `apps/api/src/modules/admin/admin.schemas.ts` +- `apps/api/src/modules/admin/admin.routes.ts` + +What changed: + +- Added `AdminRecoveryCode` model. +- Recovery codes are stored as bcrypt hashes, not plaintext. +- TOTP enrollment now issues one-time recovery codes. +- Recovery-code regeneration is protected by authenticated admin access and fresh 2FA. +- Login can consume a valid unused recovery code when TOTP is enabled. +- Recovery-code issuance and use are audited. + +Security effect: + +- Adds a recovery path for mandatory admin 2FA without storing backup codes in plaintext. +- Preserves one-time-use semantics. +- Adds auditability for recovery-code lifecycle events. + +Limitation: + +- The backend returns recovery codes after enrollment/regeneration. A production-ready admin UI still needs to display them once with clear save instructions and must not persist them client-side. + +### 7. Strengthened static scanning for auth-token regressions + +Changed file: + +- `scripts/security-static-check.mjs` + +What changed: + +- Static scan now catches common regressions involving auth/session token names in `localStorage` and `document.cookie`. +- Scan specifically targets auth token/session names such as employee/admin/renter tokens and sessions. + +Security effect: + +- Makes it harder for future code changes to quietly reintroduce script-readable authentication tokens. + +### 8. Updated documentation to match the hardened model + +Changed files: + +- `apps/dashboard/README.md` +- `memory/project_auth_architecture.md` +- `docs/project-design/COOKIE_POLICY.md` +- `apps/api/src/swagger/openapi.ts` + +What changed: + +- Replaced stale localStorage/JWT handoff claims with HttpOnly session-cookie wording. +- Clarified that browser clients use HttpOnly sessions and that Bearer tokens are only for documented trusted server/mobile contexts. +- Updated cookie policy references from legacy `employee_token` wording to `employee_session`. + +Security effect: + +- Reduces the odds that a future developer follows stale documentation and reintroduces the old pattern. + +## Validation Performed + +The following checks were run successfully in the available environment: + +```bash +npm run security:static +node --check scripts/security-static-check.mjs +# JSON parse validation for package.json and package-lock.json files +# YAML parse validation for docker-compose.production.yml and .gitlab-ci.yml +bash -n docker/entrypoint.production.sh scripts/docker-prod-*.sh scripts/docker-registry-local-up.sh scripts/setup-clerk-keys.sh +# TypeScript/TSX syntax transpile check over 507 source files +npm audit --package-lock-only --omit=dev --audit-level=critical +``` + +Results: + +- Security static check: passed. +- Static-check script syntax: passed. +- Package JSON validation: passed. +- YAML validation: passed. +- Shell syntax validation: passed. +- TypeScript/TSX syntax transpile validation: passed. +- Critical production dependency audit: passed. + +Audit note: + +- `npm audit --audit-level=critical` exited successfully. +- Moderate advisories remain for transitive `postcss` and `uuid` paths. The available automatic fixes require breaking/force dependency changes, so they were not applied blindly in this source pass. + +## Not Fully Verified in This Environment + +The following items require a real development/CI/deployment environment: + +- `npm ci` from a clean checkout. +- Full workspace typecheck. +- Full unit, integration, security, and e2e test suites. +- Prisma client generation. +- Applying the new database migration to a real database. +- Database backup/restore verification. +- Docker image build and runtime validation. +- Container scanning with Trivy or equivalent. +- Live reverse-proxy validation for `x-middleware-subrequest` blocking. +- Redis-backed distributed rate-limit validation across multiple API containers. +- Provider webhook sandbox tests. +- Live admin 2FA recovery-code UX verification. + +## Remaining Launch-Gate Work + +These are still not things source edits can prove by themselves: + +1. Rotate all real production secrets. +2. Confirm no real secrets exist in repository history or image layers. +3. Apply and verify the new `admin_recovery_codes` migration. +4. Run the full CI gate: lint, typecheck, unit tests, integration tests, security tests, build, dependency audit, secret scan, and container scan. +5. Confirm Redis and PostgreSQL are private in production. +6. Confirm DB management tools are not publicly reachable. +7. Confirm production containers run non-root with reduced capabilities and resource limits. +8. Confirm webhook signature/idempotency behavior against real provider sandbox payloads. +9. Confirm private files are inaccessible through static routes in the deployed environment. +10. Confirm the admin UI displays recovery codes once and instructs admins to save them securely. + +## Changed Files + +See `security_hardening_leftover_changed_files.txt` for the complete file list and `security_hardening_leftover.diff` for the unified diff. + +## Final Assessment + +This pass closes a meaningful set of leftover source-level gaps from the security-hardening plan. The project is closer to the intended model: API-enforced security, HttpOnly browser sessions, stronger admin 2FA recovery, app-layer bypass blocking, and less stale documentation. + +However, this still should not be treated as production-ready until the remaining launch-gate evidence is collected from CI and the live deployment environment. Security that has not been tested in the actual runtime is mostly optimism with a lanyard. diff --git a/apps/admin/src/components/PublicShell.tsx b/apps/admin/src/components/PublicShell.tsx index 6654abd..cfeb63f 100644 --- a/apps/admin/src/components/PublicShell.tsx +++ b/apps/admin/src/components/PublicShell.tsx @@ -1,6 +1,7 @@ -import React from "react"; 'use client' +import React from "react"; + import PublicFooter from '@/components/PublicFooter' import PublicHeader from '@/components/PublicHeader' diff --git a/docs/B2B_BILLING_AND_INVOICE_EXECUTION_PLAN.md b/docs/B2B_BILLING_AND_INVOICE_EXECUTION_PLAN.md new file mode 100644 index 0000000..be33e78 --- /dev/null +++ b/docs/B2B_BILLING_AND_INVOICE_EXECUTION_PLAN.md @@ -0,0 +1,1670 @@ +# B2B Billing and Invoice Execution Plan + +## 1. Objective + +Build a billing and invoice system that manages the financial lifecycle behind B2B subscriptions. + +The system must support: + +- Subscription invoices +- Trial conversion invoices +- Renewal invoices +- One-time invoices +- Usage-based invoices +- Manual invoices +- Payment collection +- Payment retries +- Credits +- Refunds +- Taxes +- Discounts +- Invoice adjustments +- Failed payments +- Invoice reconciliation +- Revenue auditability +- Admin billing operations + +The billing system must integrate with the subscription policy system but remain logically separate from subscription status. + +--- + +## 2. Core Principle + +Subscription state and invoice state are related, but they are not the same thing. + +Bad design: + +```text +subscription.status = invoice.status +``` + +Better design: + +```text +subscription.status = active +invoice.status = open +payment.status = failed +entitlement.access_level = full +``` + +A customer can have an active subscription and an unpaid invoice during a grace period. + +A customer can have a canceled subscription and still owe an unpaid invoice. + +A customer can have a paid invoice and no active subscription if the invoice was for a one-time service. + +Do not merge these concepts. That is how billing systems become haunted. + +--- + +## 3. Core Billing Entities + +The billing system should use these primary objects: + +```text +billing_account +subscription +invoice +invoice_line_item +payment_intent +payment_attempt +payment_method +credit_balance +credit_note +refund +tax_record +billing_event +``` + +| Entity | Purpose | +|---|---| +| `billing_account` | Represents the paying customer or company | +| `subscription` | Represents recurring service agreement | +| `invoice` | Represents money owed | +| `invoice_line_item` | Represents individual charges or credits | +| `payment_intent` | Represents attempt to collect invoice payment | +| `payment_attempt` | Represents each actual payment try | +| `payment_method` | Represents card, ACH, bank transfer, invoice terms, etc. | +| `credit_balance` | Tracks customer prepaid or credited amount | +| `credit_note` | Reduces or reverses invoice amount | +| `refund` | Returns money after payment | +| `tax_record` | Stores calculated tax details | +| `billing_event` | Immutable audit trail | + +--- + +## 4. Billing Account Model + +A billing account represents the organization or legal entity responsible for payment. + +```sql +CREATE TABLE billing_accounts ( + id UUID PRIMARY KEY, + + workspace_id UUID NOT NULL, + customer_id UUID NOT NULL, + + legal_name TEXT NOT NULL, + billing_email TEXT NOT NULL, + + billing_address_line1 TEXT, + billing_address_line2 TEXT, + billing_city TEXT, + billing_state TEXT, + billing_postal_code TEXT, + billing_country TEXT, + + tax_id TEXT, + tax_exempt BOOLEAN DEFAULT FALSE, + + default_currency TEXT NOT NULL DEFAULT 'USD', + default_payment_method_id UUID, + + invoice_terms TEXT NOT NULL DEFAULT 'due_on_receipt', + net_terms_days INTEGER DEFAULT 0, + + provider_customer_id TEXT, + + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +Billing account rules: + +- A workspace should have one primary billing account. +- Enterprise customers may have multiple billing accounts if departmental billing is supported. +- A billing account owns invoices, payment methods, and credit balance. +- Billing contact information must be snapshotted on finalized invoices. +- Invoices should preserve historical billing details and must not dynamically read current billing address after creation. + +--- + +## 5. Invoice Lifecycle + +Invoices should have their own state machine. + +```text +draft +open +paid +partially_paid +payment_pending +past_due +void +uncollectible +refunded +partially_refunded +``` + +| Status | Meaning | +|---|---| +| `draft` | Invoice is being prepared and is not payable yet | +| `open` | Invoice is finalized and awaiting payment | +| `payment_pending` | Payment has been initiated but not completed | +| `partially_paid` | Some amount has been paid, but balance remains | +| `paid` | Invoice is fully paid | +| `past_due` | Invoice due date has passed | +| `void` | Invoice was canceled before payment | +| `uncollectible` | Invoice is written off as bad debt | +| `refunded` | Fully paid invoice was fully refunded | +| `partially_refunded` | Paid invoice was partially refunded | + +--- + +## 6. Invoice State Transitions + +Happy path: + +```text +draft + ↓ +open + ↓ +payment_pending + ↓ +paid +``` + +Failure flow: + +```text +open + ↓ +payment_pending + ↓ +open + ↓ +past_due + ↓ +uncollectible +``` + +Void flow: + +```text +draft/open + ↓ +void +``` + +Refund flow: + +```text +paid + ↓ +partially_refunded + ↓ +refunded +``` + +| Current Status | Event | New Status | Notes | +|---|---|---|---| +| `draft` | Invoice finalized | `open` | Invoice becomes payable | +| `draft` | Invoice canceled | `void` | No payment expected | +| `open` | Payment started | `payment_pending` | Payment is in progress | +| `open` | Full payment received | `paid` | Invoice closed | +| `open` | Partial payment received | `partially_paid` | Balance remains | +| `open` | Due date passed | `past_due` | Trigger dunning | +| `payment_pending` | Payment succeeded | `paid` | Invoice closed | +| `payment_pending` | Payment failed | `open` | Retry may continue | +| `partially_paid` | Remaining balance paid | `paid` | Invoice closed | +| `partially_paid` | Due date passed | `past_due` | Balance overdue | +| `past_due` | Payment received | `paid` | Restore account if needed | +| `past_due` | Written off | `uncollectible` | Bad debt | +| `paid` | Partial refund issued | `partially_refunded` | Access review may be needed | +| `paid` | Full refund issued | `refunded` | Access review required | +| `open` | Admin voids invoice | `void` | Audit required | + +--- + +## 7. Invoice Types + +```text +subscription_initial +subscription_renewal +trial_conversion +subscription_upgrade +subscription_downgrade_credit +usage_based +one_time +manual +correction +cancellation_fee +``` + +| Invoice Type | Description | +|---|---| +| `subscription_initial` | First paid invoice for a new subscription | +| `subscription_renewal` | Recurring invoice for next billing period | +| `trial_conversion` | Invoice generated when trial converts to paid | +| `subscription_upgrade` | Prorated invoice for plan upgrade | +| `subscription_downgrade_credit` | Credit issued for downgrade | +| `usage_based` | Usage charges for metered products | +| `one_time` | Non-recurring charge | +| `manual` | Admin-created invoice | +| `correction` | Accounting correction | +| `cancellation_fee` | Fee after contract cancellation, if applicable | + +--- + +## 8. Invoice Creation Policy + +### 8.1 Trial Conversion Invoice + +```text +trialing subscription + ↓ +generate trial_conversion invoice + ↓ +attempt payment +``` + +Rules: + +- Generate invoice at trial end. +- Include subscription plan line item. +- Include tax if applicable. +- Apply credits if available. +- Apply discounts if valid. +- Attempt payment immediately if payment method exists. +- If payment succeeds, mark invoice `paid` and subscription `active`. +- If payment fails, mark invoice `open` or `payment_pending` and subscription `payment_pending`. + +### 8.2 Renewal Invoice + +```text +active subscription + ↓ +generate renewal invoice + ↓ +finalize invoice + ↓ +attempt payment +``` + +| Billing Model | Invoice Timing | +|---|---| +| Credit card / auto-pay | Generate and charge at renewal time | +| ACH / bank debit | Generate 3-5 days before due date | +| Net terms | Generate at period start, due later | +| Enterprise contract | Generate according to contract schedule | + +Default SaaS rule: + +```text +Generate renewal invoice at current_period_end. +Charge immediately. +``` + +Default B2B enterprise rule: + +```text +Generate invoice at period start. +Due date = invoice_date + net_terms_days. +``` + +### 8.3 Upgrade Invoice + +```text +active subscription + ↓ +calculate prorated charge + ↓ +generate upgrade invoice + ↓ +attempt payment +``` + +Recommended policy: + +- Upgrade takes effect immediately. +- Charge prorated amount immediately. +- If payment succeeds, apply upgraded entitlements. +- If payment fails, keep existing plan active. +- Do not remove already-paid access because an upgrade payment failed. + +### 8.4 Downgrade Invoice or Credit + +Recommended policy: + +```text +Downgrade takes effect at next billing cycle. +No immediate refund by default. +``` + +Alternative policy: + +```text +Immediate downgrade creates account credit. +``` + +Preferred default: + +| Action | Policy | +|---|---| +| Downgrade | Effective next billing period | +| Refund | No automatic refund | +| Credit | Optional, admin-controlled | +| Entitlement reduction | At period end | + +### 8.5 Manual Invoice + +Admins may create manual invoices for: + +- Custom services +- Enterprise onboarding +- Extra seats +- Support packages +- Contracted fees +- Usage corrections +- Migration adjustments + +Manual invoices require: + +```text +admin_id +reason +line_items +approval if over threshold +audit event +``` + +--- + +## 9. Invoice Line Items + +Every invoice must be composed of line items. + +```text +subscription_fee +seat_fee +usage_fee +setup_fee +discount +tax +credit +proration +refund_adjustment +manual_adjustment +``` + +```sql +CREATE TABLE invoice_line_items ( + id UUID PRIMARY KEY, + + invoice_id UUID NOT NULL, + subscription_id UUID, + plan_id UUID, + + type TEXT NOT NULL, + description TEXT NOT NULL, + + quantity NUMERIC NOT NULL DEFAULT 1, + unit_amount INTEGER NOT NULL, + amount INTEGER NOT NULL, + + currency TEXT NOT NULL, + + period_start TIMESTAMP, + period_end TIMESTAMP, + + metadata JSONB, + + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +Rules: + +- Invoice totals must be derived from line items. +- Never manually mutate invoice total without a corresponding line item. +- Negative line items are allowed for discounts, credits, and adjustments. +- Tax should be represented as a separate line item or tax record. +- Historical line item descriptions should not change after invoice finalization. + +--- + +## 10. Invoice Amount Calculation + +```text +subtotal = sum(charge line items) +discount_total = sum(discount line items) +credit_total = sum(credit line items) +tax_total = calculated tax +total = subtotal - discount_total - credit_total + tax_total +amount_due = total - amount_paid +``` + +Constraints: + +```text +amount_due cannot be less than 0 +paid invoice cannot have amount_due > 0 +void invoice cannot accept payments +draft invoice cannot accept payments +``` + +If credits exceed invoice total: + +```text +invoice amount_due = 0 +remaining credit stays on billing account +invoice may be marked paid +``` + +--- + +## 11. Payment Lifecycle + +Payments should be tracked separately from invoices. + +```text +requires_payment_method +requires_confirmation +requires_action +processing +succeeded +failed +canceled +refunded +partially_refunded +``` + +Payment flow: + +```text +invoice.open + ↓ +payment_intent.created + ↓ +payment.processing + ↓ +payment.succeeded + ↓ +invoice.paid +``` + +Failure flow: + +```text +payment.failed + ↓ +invoice.open + ↓ +retry scheduled + ↓ +subscription payment_pending / past_due +``` + +--- + +## 12. Payment Methods + +Supported payment methods: + +```text +card +ach_debit +bank_transfer +wire_transfer +manual_invoice +purchase_order +``` + +| Customer Type | Payment Method | +|---|---| +| Self-serve SaaS | Card | +| Mid-market | Card or ACH | +| Enterprise | Invoice terms / ACH / wire | +| High-risk customer | Card or upfront payment only | + +Rules: + +- Each billing account may have one default payment method. +- Payment methods must be tokenized through the provider. +- Do not store raw card or bank details. +- Expired cards should trigger notification before renewal. +- Failed payment method should not be retried indefinitely. + +--- + +## 13. Net Terms Policy + +Supported terms: + +```text +due_on_receipt +net_7 +net_15 +net_30 +net_45 +net_60 +``` + +Default: + +```text +Self-serve: due_on_receipt +B2B standard: net_15 or net_30 +Enterprise: contract-specific +``` + +Rules: + +- Net terms require approval above a revenue threshold. +- Net terms customers may remain `active` while invoice is `open`. +- Once due date passes, invoice becomes `past_due`. +- Subscription state should only degrade after the configured grace policy. + +Example: + +```json +{ + "invoice_date": "2026-06-01T00:00:00Z", + "net_terms_days": 30, + "due_at": "2026-07-01T00:00:00Z" +} +``` + +--- + +## 14. Dunning and Payment Recovery + +Dunning is triggered by unpaid invoices. + +| Day | Invoice Status | Subscription Status | Action | +|---:|---|---|---| +| 0 | `open` | `active` or `payment_pending` | Notify billing owner | +| 3 | `open` | `payment_pending` | Retry payment | +| 7 | `past_due` | `past_due` | Limit access | +| 14 | `past_due` | `suspended` | Suspend account | +| 30 | `uncollectible` or `void` | `canceled` | Cancel subscription | + +Rules: + +- Dunning is invoice-driven. +- Subscription status reacts to unpaid invoice state. +- A customer may have multiple unpaid invoices. +- The oldest unpaid invoice should control escalation. +- Payment of all blocking invoices should restore subscription. +- Admins may pause dunning for enterprise customers. + +--- + +## 15. Credits and Credit Balance + +Credits should reduce future invoice amounts. + +Credit sources: + +```text +downgrade credit +service credit +SLA credit +manual goodwill credit +overpayment +refund converted to credit +``` + +```sql +CREATE TABLE credit_balances ( + id UUID PRIMARY KEY, + + billing_account_id UUID NOT NULL, + currency TEXT NOT NULL, + + balance_amount INTEGER NOT NULL DEFAULT 0, + + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +```sql +CREATE TABLE credit_ledger_entries ( + id UUID PRIMARY KEY, + + billing_account_id UUID NOT NULL, + invoice_id UUID, + + type TEXT NOT NULL, + amount INTEGER NOT NULL, + currency TEXT NOT NULL, + + reason TEXT NOT NULL, + created_by TEXT, + + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +Credit rules: + +- Credits must be ledgered. +- Credit balance cannot change without a ledger entry. +- Credits should be currency-specific. +- Credits apply before payment collection. +- Credits should not silently expire unless terms clearly say so. +- Enterprise credits may require approval. + +--- + +## 16. Discounts and Coupons + +Discount types: + +```text +percentage +fixed_amount +trial_extension +contractual +promotional +manual +``` + +Rules: + +- Discounts must have start and end dates. +- Discounts must define scope: invoice, subscription, plan, or account. +- Discounts should not apply to taxes unless legally permitted. +- Expired discounts must not apply to new invoices. +- Manual discounts require admin reason. + +Example: + +```json +{ + "discount_type": "percentage", + "percent_off": 20, + "applies_to": "subscription", + "starts_at": "2026-06-01T00:00:00Z", + "ends_at": "2026-12-01T00:00:00Z" +} +``` + +--- + +## 17. Tax Policy + +Tax calculation should happen at invoice finalization. + +Requirements: + +- Use billing address for tax jurisdiction. +- Use tax exemption status if present. +- Store tax calculation result. +- Store tax rate and jurisdiction. +- Do not recalculate finalized invoice taxes unless issuing correction. +- Support reverse charge or VAT if applicable. + +```sql +CREATE TABLE tax_records ( + id UUID PRIMARY KEY, + + invoice_id UUID NOT NULL, + + jurisdiction TEXT, + tax_rate NUMERIC, + tax_amount INTEGER NOT NULL, + tax_type TEXT, + + tax_exempt BOOLEAN DEFAULT FALSE, + exemption_reason TEXT, + + provider_tax_id TEXT, + metadata JSONB, + + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +Recommended: integrate tax calculation through a provider if selling across jurisdictions. Tax logic is not the place to be brave. + +--- + +## 18. Refund Policy + +Refunds reverse payments, not invoices. + +Refund types: + +```text +full +partial +goodwill +duplicate_payment +service_issue +fraud +chargeback +``` + +Rules: + +- Refunds require a paid invoice. +- Refunds must reference the original payment. +- Refunds must create a refund event. +- Full refund may require subscription cancellation or entitlement review. +- Partial refund may leave subscription active. +- Refund does not delete the invoice. +- Refund does not erase revenue history. + +```sql +CREATE TABLE refunds ( + id UUID PRIMARY KEY, + + invoice_id UUID NOT NULL, + payment_attempt_id UUID NOT NULL, + billing_account_id UUID NOT NULL, + + amount INTEGER NOT NULL, + currency TEXT NOT NULL, + + reason TEXT NOT NULL, + status TEXT NOT NULL, + + provider_refund_id TEXT, + created_by TEXT, + + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +| Refund Event | Invoice Impact | Subscription Impact | +|---|---|---| +| Partial refund | `partially_refunded` | Usually no change | +| Full refund current period | `refunded` | Review access | +| Fraud refund | `refunded` | Suspend or cancel | +| Duplicate payment refund | Keep `paid` | No change | +| SLA credit as refund | `partially_refunded` | No change | + +--- + +## 19. Credit Notes + +Credit notes reduce the amount owed on an invoice. + +Use credit notes when: + +- Invoice was wrong +- Customer received a contractual credit +- Customer downgraded and receives credit +- Tax correction is needed +- Admin adjusts invoice before collection + +Do not use refunds when money was never collected. + +```sql +CREATE TABLE credit_notes ( + id UUID PRIMARY KEY, + + invoice_id UUID NOT NULL, + billing_account_id UUID NOT NULL, + + amount INTEGER NOT NULL, + currency TEXT NOT NULL, + + reason TEXT NOT NULL, + status TEXT NOT NULL, + + created_by TEXT, + + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +--- + +## 20. Chargeback Policy + +Chargebacks are high-risk and should override normal happy-path billing. + +```text +chargeback.created + ↓ +invoice disputed + ↓ +subscription suspended + ↓ +admin/risk review +``` + +Rules: + +- Chargeback should immediately flag billing account. +- Current subscription should become `suspended` unless enterprise exception applies. +- Disable automatic reactivation until resolved. +- Require admin review to restore. +- Track dispute outcome. + +| Outcome | Action | +|---|---| +| Won | Restore invoice/payment state | +| Lost | Keep invoice unpaid or refunded | +| Withdrawn | Restore access after review | +| Under review | Keep account restricted | + +--- + +## 21. Reconciliation Policy + +The billing system must reconcile local records with the payment provider. + +Daily reconciliation should compare: + +- Provider customer records +- Provider subscriptions +- Provider invoices +- Provider payments +- Provider refunds +- Provider disputes + +| Mismatch | Action | +|---|---| +| Provider invoice paid, local invoice open | Mark local invoice paid | +| Provider payment failed, local payment processing | Mark failed | +| Provider subscription canceled, local active | Flag for review | +| Local invoice exists, provider missing | Flag critical | +| Provider refund exists, local missing | Import refund | + +Rules: + +- Do not blindly overwrite local admin overrides. +- Create reconciliation events. +- Alert on high-risk mismatches. +- Store provider snapshot for audit. + +--- + +## 22. Billing Events + +Every financial action must create an immutable event. + +```text +billing_account.created +billing_account.updated + +invoice.created +invoice.finalized +invoice.sent +invoice.payment_started +invoice.payment_failed +invoice.payment_succeeded +invoice.partially_paid +invoice.paid +invoice.past_due +invoice.voided +invoice.marked_uncollectible + +payment_intent.created +payment_attempt.created +payment_attempt.succeeded +payment_attempt.failed +payment_method.updated + +credit.created +credit.applied +credit.expired +credit.reversed + +discount.applied +discount.expired + +tax.calculated +tax.exempt_applied + +refund.created +refund.succeeded +refund.failed + +chargeback.created +chargeback.won +chargeback.lost + +billing.reconciled +billing.reconciliation_failed +admin.billing_override_created +``` + +--- + +## 23. Admin Billing Console + +Admins need operational tools, but every button should leave fingerprints. + +Capabilities: + +- View billing account +- View invoices +- View invoice line items +- View payments +- View payment attempts +- View credit balance +- View credit ledger +- View refunds +- View chargebacks +- Retry payment +- Send invoice +- Void invoice +- Mark invoice uncollectible +- Issue credit note +- Issue refund +- Apply discount +- Update billing email +- Update tax ID +- Update billing address +- Change net terms +- Pause dunning +- Resume dunning + +Audit fields: + +```json +{ + "admin_id": "admin_123", + "action": "void_invoice", + "invoice_id": "inv_123", + "reason": "Duplicate invoice generated during migration", + "previous_status": "open", + "new_status": "void", + "created_at": "2026-06-15T00:00:00Z" +} +``` + +Require approval for: + +- Refunds above threshold +- Manual credits above threshold +- Changing enterprise net terms +- Marking invoice uncollectible +- Voiding paid invoices +- Removing tax +- Applying large discounts + +--- + +## 24. Customer Billing Portal + +Customers should be able to: + +- View current plan +- View billing account details +- View invoices +- Download invoices as PDF +- Update payment method +- Pay open invoices +- View payment history +- View credit balance +- Update billing email +- Update tax ID +- Update billing address +- Cancel subscription if permitted +- Request plan change + +Do not expose internal statuses like: + +```text +uncollectible +dunning_paused +provider_mismatch +``` + +Map them to customer-friendly labels. + +--- + +## 25. Customer-Facing Invoice Labels + +| Internal Status | Customer Label | +|---|---| +| `draft` | Preparing | +| `open` | Due | +| `payment_pending` | Payment processing | +| `paid` | Paid | +| `partially_paid` | Partially paid | +| `past_due` | Past due | +| `void` | Canceled | +| `uncollectible` | Contact support | +| `refunded` | Refunded | +| `partially_refunded` | Partially refunded | + +--- + +## 26. Billing Notifications + +| Event | Recipient | Notification | +|---|---|---| +| Invoice finalized | Billing owner | Invoice available | +| Invoice due soon | Billing owner | Upcoming due date | +| Payment succeeded | Billing owner | Receipt | +| Payment failed | Billing owner + admins | Payment failed | +| Invoice past due | Billing owner + admins | Payment overdue | +| Account suspended | Billing owner + admins | Access restricted | +| Invoice paid after failure | Billing owner + admins | Payment recovered | +| Refund issued | Billing owner | Refund confirmation | +| Credit applied | Billing owner | Credit applied | + +Rules: + +- Every notification must include invoice number. +- Payment failure notifications must include update-payment link. +- Past due notifications must include deadline. +- Enterprise invoices should include PO number if available. +- Receipts should include amount paid and payment method summary. + +--- + +## 27. Invoice Numbering + +Invoice numbers must be sequential and immutable. + +Recommended format: + +```text +INV-2026-000001 +``` + +Rules: + +- Invoice number assigned at finalization. +- Draft invoices may not need invoice numbers. +- Invoice numbers must never be reused. +- Voided invoices keep their invoice numbers. +- Region-specific sequencing may be needed for tax compliance. + +Fields: + +```sql +invoice_number TEXT UNIQUE +invoice_sequence INTEGER UNIQUE +``` + +Do not generate invoice numbers using random UUIDs. Accounting teams will hate you, and this time they will be right. + +--- + +## 28. Data Model Summary + +### Invoices Table + +```sql +CREATE TABLE invoices ( + id UUID PRIMARY KEY, + + billing_account_id UUID NOT NULL, + subscription_id UUID, + workspace_id UUID NOT NULL, + + invoice_number TEXT UNIQUE, + invoice_type TEXT NOT NULL, + status TEXT NOT NULL, + + currency TEXT NOT NULL, + + subtotal_amount INTEGER NOT NULL DEFAULT 0, + discount_amount INTEGER NOT NULL DEFAULT 0, + credit_amount INTEGER NOT NULL DEFAULT 0, + tax_amount INTEGER NOT NULL DEFAULT 0, + total_amount INTEGER NOT NULL DEFAULT 0, + amount_paid INTEGER NOT NULL DEFAULT 0, + amount_due INTEGER NOT NULL DEFAULT 0, + + invoice_date TIMESTAMP, + due_at TIMESTAMP, + finalized_at TIMESTAMP, + paid_at TIMESTAMP, + voided_at TIMESTAMP, + marked_uncollectible_at TIMESTAMP, + + billing_name TEXT, + billing_email TEXT, + billing_address JSONB, + + provider_invoice_id TEXT, + + metadata JSONB, + + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +### Payment Attempts Table + +```sql +CREATE TABLE payment_attempts ( + id UUID PRIMARY KEY, + + invoice_id UUID NOT NULL, + billing_account_id UUID NOT NULL, + + provider_payment_id TEXT, + payment_method_id UUID, + + status TEXT NOT NULL, + amount INTEGER NOT NULL, + currency TEXT NOT NULL, + + failure_code TEXT, + failure_message TEXT, + + attempted_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +### Billing Events Table + +```sql +CREATE TABLE billing_events ( + id UUID PRIMARY KEY, + + billing_account_id UUID, + invoice_id UUID, + subscription_id UUID, + workspace_id UUID, + + event_type TEXT NOT NULL, + source TEXT NOT NULL, + + payload JSONB NOT NULL, + + occurred_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +--- + +## 29. API Requirements + +### Create Invoice + +```http +POST /billing/invoices +``` + +```json +{ + "billing_account_id": "ba_123", + "invoice_type": "manual", + "line_items": [ + { + "type": "setup_fee", + "description": "Enterprise onboarding", + "quantity": 1, + "unit_amount": 500000 + } + ] +} +``` + +### Finalize Invoice + +```http +POST /billing/invoices/{invoice_id}/finalize +``` + +Behavior: + +- Assign invoice number. +- Calculate totals. +- Calculate tax. +- Apply credits. +- Set status to `open`. +- Send invoice if configured. + +### Pay Invoice + +```http +POST /billing/invoices/{invoice_id}/pay +``` + +```json +{ + "payment_method_id": "pm_123" +} +``` + +### Retry Payment + +```http +POST /billing/invoices/{invoice_id}/retry-payment +``` + +### Void Invoice + +```http +POST /billing/invoices/{invoice_id}/void +``` + +```json +{ + "reason": "Duplicate invoice" +} +``` + +### Issue Credit Note + +```http +POST /billing/invoices/{invoice_id}/credit-notes +``` + +```json +{ + "amount": 10000, + "reason": "Service credit" +} +``` + +### Issue Refund + +```http +POST /billing/invoices/{invoice_id}/refunds +``` + +```json +{ + "amount": 10000, + "reason": "Customer requested partial refund" +} +``` + +--- + +## 30. Scheduled Jobs + +| Job | Frequency | Purpose | +|---|---:|---| +| Invoice generation job | Hourly/Daily | Generate renewal invoices | +| Invoice finalization job | Hourly | Finalize pending invoices | +| Payment collection job | Hourly | Charge open invoices | +| Payment retry job | Hourly | Retry failed payments | +| Invoice due reminder job | Daily | Notify customers before due date | +| Past due job | Hourly | Mark overdue invoices as past due | +| Dunning escalation job | Daily | Escalate unpaid invoices | +| Credit application job | On invoice finalization | Apply available credits | +| Reconciliation job | Daily | Match provider and local state | +| Tax sync job | Daily/On demand | Validate tax records | +| Invoice PDF generation job | On finalization | Generate downloadable invoices | + +--- + +## 31. Subscription Integration Rules + +Billing should drive subscription changes only through defined events. + +| Billing Event | Subscription Reaction | +|---|---| +| `invoice.paid` for trial conversion | `trialing → active` | +| `invoice.payment_failed` for renewal | `active → payment_pending` | +| `invoice.past_due` | `payment_pending → past_due` | +| `invoice.marked_uncollectible` | `suspended → canceled` or admin review | +| `refund.succeeded` full current period | Entitlement review | +| `chargeback.created` | `active → suspended` | + +Important: not every invoice should affect subscription status. + +A manual invoice for onboarding should not cancel the subscription if unpaid unless policy says it is blocking. + +Add this field: + +```text +is_subscription_blocking +``` + +Example: + +```json +{ + "invoice_type": "manual", + "is_subscription_blocking": false +} +``` + +--- + +## 32. Blocking vs Non-Blocking Invoices + +Blocking invoices can affect subscription access: + +```text +subscription_initial +subscription_renewal +trial_conversion +subscription_upgrade +``` + +Non-blocking invoices should not automatically affect access: + +```text +manual +setup_fee +professional_services +correction +one_time +``` + +| Invoice Type | Blocks Subscription? | +|---|---| +| `subscription_initial` | Yes | +| `trial_conversion` | Yes | +| `subscription_renewal` | Yes | +| `subscription_upgrade` | Usually no | +| `manual` | Configurable | +| `one_time` | No by default | +| `usage_based` | Configurable | +| `setup_fee` | No by default | +| `cancellation_fee` | No, subscription already ending | + +--- + +## 33. Accounting Rules + +### Immutability + +Once finalized: + +- Invoice number cannot change. +- Invoice date cannot change. +- Line items cannot be edited directly. +- Totals cannot be edited directly. +- Billing address snapshot cannot be edited directly. + +Corrections require: + +```text +credit note +adjustment invoice +refund +void if unpaid +``` + +### Revenue Integrity + +- Paid invoices remain in history. +- Refunded invoices remain in history. +- Voided invoices remain in history. +- Deleted invoices should not exist in production. +- Admin changes must be audited. + +--- + +## 34. Testing Plan + +### Unit Tests + +Test: + +- Invoice total calculation +- Tax calculation hooks +- Credit application +- Discount application +- Invoice state transitions +- Payment state transitions +- Refund rules +- Credit note rules +- Net terms due date logic +- Blocking invoice logic + +### Integration Tests + +Test: + +- Create invoice +- Finalize invoice +- Pay invoice +- Failed payment +- Retry payment +- Apply credit +- Issue refund +- Void invoice +- Reconcile provider payment +- Handle duplicate webhook +- Handle out-of-order webhook + +### End-to-End Tests + +Test: + +```text +Trial ends → invoice created → payment succeeds → subscription active +Trial ends → invoice created → payment fails → subscription payment_pending +Renewal invoice fails → dunning starts → subscription past_due +Past due invoice paid → subscription restored +Enterprise invoice created with net_30 → due date passes → past_due +Upgrade invoice fails → existing subscription remains active +Full refund current period → entitlement review triggered +Chargeback created → subscription suspended +``` + +--- + +## 35. Monitoring and Alerts + +Track: + +```text +invoice_created_count +invoice_finalized_count +invoice_paid_count +invoice_failed_count +invoice_past_due_count +invoice_voided_count +invoice_uncollectible_count +payment_attempt_count +payment_success_rate +payment_failure_rate +refund_count +credit_note_count +reconciliation_mismatch_count +average_days_to_payment +accounts_receivable_total +past_due_amount_total +``` + +Alert on: + +```text +Payment failure spike +Invoices stuck in payment_pending +Invoices overdue beyond policy +Provider/local invoice mismatch +Provider/local payment mismatch +Refund spike +Credit abuse +Chargeback created +Invoice finalization failure +Tax calculation failure +``` + +--- + +## 36. Rollout Plan + +### Phase 1: Invoice Foundation + +Deliver: + +- Billing account model +- Invoice model +- Invoice line items +- Invoice statuses +- Invoice total calculation +- Basic invoice admin view + +Acceptance criteria: + +- Admin can create draft invoice. +- Admin can finalize invoice. +- Finalized invoice gets immutable number. +- Invoice totals are derived from line items. + +### Phase 2: Payment Collection + +Deliver: + +- Payment method support +- Payment intent model +- Payment attempt model +- Payment success handling +- Payment failure handling + +Acceptance criteria: + +- Open invoice can be paid. +- Failed payment creates payment attempt. +- Successful payment marks invoice paid. +- Payment event is auditable. + +### Phase 3: Subscription Billing Integration + +Deliver: + +- Trial conversion invoices +- Renewal invoices +- Initial subscription invoices +- Blocking invoice logic +- Subscription reaction events + +Acceptance criteria: + +- Trial conversion payment activates subscription. +- Renewal failure moves subscription to payment_pending. +- Invoice payment restores subscription when applicable. +- Non-blocking invoices do not affect subscription. + +### Phase 4: Dunning and Net Terms + +Deliver: + +- Due dates +- Net terms +- Past due logic +- Payment retries +- Dunning notifications +- Subscription escalation rules + +Acceptance criteria: + +- Net terms invoices become past due after due date. +- Failed payments retry on schedule. +- Blocking unpaid invoices escalate subscription status. +- Admin can pause dunning. + +### Phase 5: Credits, Refunds, Adjustments + +Deliver: + +- Credit balance ledger +- Credit note support +- Refund support +- Discount support +- Adjustment invoice support + +Acceptance criteria: + +- Credits apply to future invoices. +- Credit balance is ledgered. +- Refunds reference original payments. +- Finalized invoices are corrected through credit notes or adjustments. + +### Phase 6: Tax and Compliance + +Deliver: + +- Tax calculation integration +- Tax records +- Tax exemption handling +- Invoice PDF generation +- Invoice address snapshots + +Acceptance criteria: + +- Tax calculated at finalization. +- Tax result is stored. +- Finalized invoice preserves billing details. +- Customer can download invoice PDF. + +### Phase 7: Reconciliation and Hardening + +Deliver: + +- Provider reconciliation +- Webhook replay +- Dead-letter queue +- Mismatch alerts +- Admin correction workflows + +Acceptance criteria: + +- Provider and local states stay aligned. +- Failed webhooks are replayable. +- Mismatches create alerts. +- Reconciliation does not overwrite valid admin decisions. + +--- + +## 37. Final Default Billing Policy + +Use this as the baseline B2B billing policy: + +```text +Invoices are generated from subscriptions, usage, or admin action. +Invoices start as draft and become payable only after finalization. +Invoice numbers are assigned only at finalization. +Finalized invoices are immutable. +Subscription invoices are blocking by default. +Manual invoices are non-blocking by default. +Credits are applied before payment collection. +Tax is calculated during finalization. +Payment failures create payment attempts and dunning events. +Payment retries follow configured schedule. +Open invoices become past_due after due date. +Blocking past_due invoices affect subscription status. +Refunds reverse payments, not invoices. +Credit notes reduce invoice balances before or after finalization depending on accounting rules. +All billing changes are event-driven and audited. +Daily reconciliation compares local billing state with payment provider state. +``` + +--- + +## 38. Definition of Done + +The billing and invoice system is complete when: + +- Billing accounts exist and own invoices. +- Invoices have a clear lifecycle. +- Invoice totals are line-item driven. +- Finalized invoices are immutable. +- Invoice numbers are sequential and permanent. +- Payment attempts are tracked. +- Failed payments trigger dunning. +- Paid invoices update subscription status when appropriate. +- Non-blocking invoices do not affect access. +- Credits and refunds are ledgered. +- Admin billing actions are audited. +- Customers can view and pay invoices. +- Provider reconciliation is operational. +- Tests cover invoice, payment, refund, and subscription integration flows. diff --git a/docs/B2B_SUBSCRIPTION_EXECUTION_PLAN.md b/docs/B2B_SUBSCRIPTION_EXECUTION_PLAN.md new file mode 100644 index 0000000..0b7f673 --- /dev/null +++ b/docs/B2B_SUBSCRIPTION_EXECUTION_PLAN.md @@ -0,0 +1,1340 @@ +# B2B Subscription Execution Plan + +## 1. Objective + +Build a reliable B2B subscription management system that supports: + +- Free trials +- Paid subscriptions +- Payment pending states +- Grace periods +- Payment retry logic +- Suspension +- Cancellation +- Reactivation +- Admin overrides +- Auditable subscription lifecycle events +- Entitlement-based access control + +The system must avoid ambiguous subscription states and must separate billing status from product access. + +--- + +## 2. Guiding Principles + +### 2.1 Subscription Status Is Not Access + +A subscription status describes the billing lifecycle. + +Access should be determined by an entitlement layer. + +Example: + +```text +subscription.status = "past_due" +entitlement.access_level = "limited" +``` + +Do not scatter billing checks throughout the product. + +--- + +### 2.2 Events Are the Source of Truth + +Every important lifecycle action must create an immutable event. + +Examples: + +```text +subscription.created +trial.started +trial.ended +payment.succeeded +payment.failed +subscription.suspended +subscription.canceled +subscription.reactivated +``` + +The current subscription state may be stored directly for performance, but every change must be explainable through historical events. + +--- + +### 2.3 Webhooks Are Unreliable Until Reconciled + +Billing provider webhooks may be delayed, duplicated, or arrive out of order. + +The system must support: + +- Idempotent webhook processing +- Event deduplication +- Provider reconciliation jobs +- Manual admin review + +--- + +## 3. Core Status Model + +Use the following subscription statuses: + +```text +trialing +active +payment_pending +past_due +suspended +canceled +expired +paused +``` + +Optional advanced statuses: + +```text +refunded +chargeback +``` + +Avoid adding more statuses unless there is a real operational need. Status bloat is how billing systems become folklore. + +--- + +## 4. Status Definitions + +| Status | Meaning | +|---|---| +| `trialing` | Customer is in a free or paid trial period. | +| `active` | Customer has a valid paid subscription. | +| `payment_pending` | Payment is expected but has not completed. | +| `past_due` | Payment is overdue after the pending window. | +| `suspended` | Access is blocked due to unresolved billing issue. | +| `canceled` | Subscription has been canceled and will not renew. | +| `expired` | Trial or subscription ended without renewal. | +| `paused` | Subscription is intentionally paused. | + +--- + +## 5. Recommended B2B Policy + +### 5.1 Trial Policy + +Default policy: + +```text +Trial length: 14 days +Payment method required: Yes +Trial eligibility: One trial per company or billing account +Trial conversion: Automatic +``` + +Trial rules: + +- A customer may start one trial per company workspace. +- A valid payment method should be required before trial activation. +- Trial start and end dates must be stored. +- Trial users should receive notifications before conversion. +- Trial abuse checks should be applied at account and company level. + +Recommended fields: + +```json +{ + "trial_start_at": "2026-05-25T00:00:00Z", + "trial_end_at": "2026-06-08T00:00:00Z", + "trial_used": true, + "payment_method_required": true +} +``` + +--- + +### 5.2 Trial Ending Policy + +At trial end: + +| Condition | Result | +|---|---| +| Payment succeeds | Move to `active` | +| Payment fails | Move to `payment_pending` | +| No payment method exists | Move to `expired` or require payment method before trial starts | + +Recommended rule: + +```text +If payment method is required, no payment method means trial cannot start. +``` + +This is stricter, but cleaner. + +--- + +### 5.3 Active Subscription Policy + +An active subscription means: + +- Payment is valid +- Current billing period is open +- Customer has access based on purchased plan +- Renewal should occur automatically + +Fields: + +```json +{ + "status": "active", + "current_period_start": "2026-06-08T00:00:00Z", + "current_period_end": "2026-07-08T00:00:00Z", + "cancel_at_period_end": false +} +``` + +--- + +### 5.4 Payment Pending Policy + +Payment pending begins when payment is required but incomplete. + +Triggers: + +- Trial conversion payment failed +- Renewal payment failed +- Upgrade payment requires confirmation +- Payment provider marks payment as incomplete +- Bank transfer or invoice payment is awaiting settlement + +Default B2B policy: + +```text +Payment pending timeout: 7 days +Access during payment pending: Full access +Notifications: Day 0, Day 3, Day 6 +``` + +B2B payments often involve finance teams, approvals, cards expiring, invoice routing, and other thrilling artifacts of corporate civilization. + +--- + +### 5.5 Past Due Policy + +After the payment pending window expires: + +```text +payment_pending → past_due +``` + +Default policy: + +```text +Past due duration: 7 days +Access: Limited access +Admin visibility: Required +Customer notification: Required +``` + +Limited access may include: + +- Read-only access +- No new projects +- No exports +- No premium actions +- Existing data remains accessible + +--- + +### 5.6 Suspension Policy + +After the past due window expires: + +```text +past_due → suspended +``` + +Default policy: + +```text +Suspension starts: 14 days after first failed payment +Access: Blocked or read-only +Data retention: Continue retaining account data +Admin notification: Required +``` + +Suspended accounts should not be deleted. + +Suspension means: + +- Billing issue is unresolved +- Customer can still update payment method +- Customer can still contact support +- Admins can manually extend access if needed + +--- + +### 5.7 Cancellation Policy + +After the suspension window expires: + +```text +suspended → canceled +``` + +Default B2B cancellation policy: + +```text +Cancel after: 30 days unpaid +Access: Removed +Renewal: Disabled +Data: Retained according to data retention policy +``` + +Cancellation should be final for that subscription instance, but the customer may create a new subscription or reactivate if allowed. + +--- + +## 6. Full Lifecycle Flow + +```text +none + ↓ +trialing + ↓ +active + ↓ +payment_pending + ↓ +past_due + ↓ +suspended + ↓ +canceled +``` + +Alternative trial expiration flow: + +```text +none + ↓ +trialing + ↓ +expired +``` + +Reactivation flow: + +```text +payment_pending / past_due / suspended / canceled + ↓ +payment succeeds + ↓ +active +``` + +--- + +## 7. Transition Table + +| Current Status | Event | New Status | Notes | +|---|---|---|---| +| `none` | Trial started | `trialing` | Customer begins trial | +| `none` | Paid subscription started | `payment_pending` | First payment required | +| `payment_pending` | Payment succeeded | `active` | Grant paid access | +| `trialing` | Trial ended and payment succeeded | `active` | Convert trial | +| `trialing` | Trial ended and payment failed | `payment_pending` | Start recovery flow | +| `trialing` | Trial ended without payment method | `expired` | No access | +| `active` | Renewal payment succeeded | `active` | Extend period | +| `active` | Renewal payment failed | `payment_pending` | Start dunning | +| `payment_pending` | Payment succeeded | `active` | Restore normal billing | +| `payment_pending` | Timeout reached | `past_due` | Payment unresolved | +| `past_due` | Payment succeeded | `active` | Restore access | +| `past_due` | Timeout reached | `suspended` | Restrict access | +| `suspended` | Payment succeeded | `active` | Restore access | +| `suspended` | Timeout reached | `canceled` | End subscription | +| `active` | User cancels | `active` with `cancel_at_period_end = true` | Access until period end | +| `active` | Admin cancels immediately | `canceled` | Used for fraud, abuse, or special cases | +| `canceled` | Customer reactivates | `payment_pending` | Payment required | +| `expired` | Customer subscribes | `payment_pending` | Payment required | +| `paused` | Resume date reached | `active` | Resume subscription | + +--- + +## 8. Timeout Policy + +Recommended B2B timeline: + +| Day | Status | Access | Action | +|---:|---|---|---| +| 0 | `payment_pending` | Full | Payment fails, notify billing owner | +| 3 | `payment_pending` | Full | Retry payment, send reminder | +| 6 | `payment_pending` | Full | Final warning before past due | +| 7 | `past_due` | Limited | Restrict risky actions | +| 14 | `suspended` | Read-only or blocked | Notify admins and billing owner | +| 30 | `canceled` | No access | Cancel subscription | + +Recommended constants: + +```json +{ + "payment_pending_timeout_days": 7, + "past_due_timeout_days": 7, + "suspension_timeout_days": 16, + "cancel_after_days_unpaid": 30 +} +``` + +--- + +## 9. Payment Retry Policy + +Use a configurable retry schedule. + +Recommended retry schedule: + +```json +{ + "retry_schedule_days": [0, 3, 6, 10, 14], + "max_retry_attempts": 5 +} +``` + +Retry rules: + +- Retry immediately after first failure. +- Retry again after 3 days. +- Retry again before moving to past due. +- Retry again before suspension. +- Retry one final time before cancellation. +- Stop retries if the customer cancels. +- Stop retries if invoice is manually voided. +- Stop retries if subscription is canceled. + +--- + +## 10. Access Control Policy + +Access must be derived through entitlements. + +### Access Levels + +```text +full +limited +read_only +none +``` + +### Mapping + +| Subscription Status | Access Level | +|---|---| +| `trialing` | `full` | +| `active` | `full` | +| `payment_pending` | `full` | +| `past_due` | `limited` | +| `suspended` | `read_only` or `none` | +| `canceled` | `none` | +| `expired` | `none` | +| `paused` | `read_only` | + +### Entitlement Decision Example + +```json +{ + "workspace_id": "workspace_123", + "subscription_id": "sub_123", + "subscription_status": "past_due", + "access_level": "limited", + "reason": "payment_overdue", + "valid_until": "2026-06-22T00:00:00Z" +} +``` + +--- + +## 11. Data Model + +### 11.1 Subscriptions Table + +```sql +CREATE TABLE subscriptions ( + id UUID PRIMARY KEY, + workspace_id UUID NOT NULL, + customer_id UUID NOT NULL, + plan_id UUID NOT NULL, + + status TEXT NOT NULL, + + billing_interval TEXT NOT NULL, + current_period_start TIMESTAMP, + current_period_end TIMESTAMP, + + trial_start_at TIMESTAMP, + trial_end_at TIMESTAMP, + + payment_pending_since TIMESTAMP, + payment_due_at TIMESTAMP, + + past_due_since TIMESTAMP, + suspended_at TIMESTAMP, + + cancel_at_period_end BOOLEAN DEFAULT FALSE, + canceled_at TIMESTAMP, + ended_at TIMESTAMP, + + retry_count INTEGER DEFAULT 0, + max_retry_count INTEGER DEFAULT 5, + + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +--- + +### 11.2 Subscription Events Table + +```sql +CREATE TABLE subscription_events ( + id UUID PRIMARY KEY, + subscription_id UUID NOT NULL, + workspace_id UUID NOT NULL, + + event_type TEXT NOT NULL, + source TEXT NOT NULL, + payload JSONB NOT NULL, + + occurred_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +--- + +### 11.3 Invoices Table + +```sql +CREATE TABLE invoices ( + id UUID PRIMARY KEY, + subscription_id UUID NOT NULL, + workspace_id UUID NOT NULL, + + provider_invoice_id TEXT, + status TEXT NOT NULL, + + amount_due INTEGER NOT NULL, + amount_paid INTEGER DEFAULT 0, + currency TEXT NOT NULL, + + due_at TIMESTAMP, + paid_at TIMESTAMP, + failed_at TIMESTAMP, + voided_at TIMESTAMP, + + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +--- + +### 11.4 Payment Attempts Table + +```sql +CREATE TABLE payment_attempts ( + id UUID PRIMARY KEY, + invoice_id UUID NOT NULL, + subscription_id UUID NOT NULL, + + provider_payment_id TEXT, + status TEXT NOT NULL, + + failure_code TEXT, + failure_message TEXT, + + attempted_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +--- + +### 11.5 Entitlements Table + +```sql +CREATE TABLE entitlements ( + id UUID PRIMARY KEY, + workspace_id UUID NOT NULL, + subscription_id UUID NOT NULL, + + feature_key TEXT NOT NULL, + access_level TEXT NOT NULL, + + starts_at TIMESTAMP NOT NULL, + ends_at TIMESTAMP, + + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +--- + +## 12. Event Catalog + +### Subscription Events + +```text +subscription.created +subscription.updated +subscription.activated +subscription.payment_pending +subscription.past_due +subscription.suspended +subscription.canceled +subscription.expired +subscription.reactivated +subscription.paused +subscription.resumed +``` + +### Trial Events + +```text +trial.started +trial.ending_soon +trial.ended +trial.converted +trial.expired +``` + +### Payment Events + +```text +payment.started +payment.succeeded +payment.failed +payment.requires_action +payment.retry_scheduled +payment.retry_failed +payment.retry_exhausted +``` + +### Invoice Events + +```text +invoice.created +invoice.finalized +invoice.paid +invoice.payment_failed +invoice.voided +invoice.refunded +``` + +### Risk Events + +```text +chargeback.created +chargeback.resolved +fraud.review_required +admin.override_created +``` + +--- + +## 13. Notification Plan + +### 13.1 Trial Notifications + +| Timing | Message | +|---|---| +| Trial start | Welcome and trial end date | +| 7 days before trial end | Reminder | +| 3 days before trial end | Conversion reminder | +| 1 day before trial end | Final reminder | +| Trial converted | Payment receipt and activation | +| Trial expired | Upgrade prompt | + +--- + +### 13.2 Payment Failure Notifications + +| Timing | Recipient | Message | +|---|---|---| +| Day 0 | Billing owner | Payment failed | +| Day 3 | Billing owner + admins | Payment retry failed | +| Day 6 | Billing owner + admins | Final warning before past due | +| Day 7 | Billing owner + admins | Account is past due | +| Day 14 | Billing owner + admins | Account suspended | +| Day 30 | Billing owner + admins | Subscription canceled | + +Every notification should include: + +- Company name +- Plan name +- Amount due +- Due date +- Payment update link +- Consequence of non-payment +- Support contact + +--- + +## 14. Admin Console Requirements + +Admins must be able to: + +- View subscription status +- View payment history +- View invoice history +- View retry attempts +- View lifecycle events +- Extend trial +- Extend grace period +- Retry payment +- Cancel immediately +- Cancel at period end +- Suspend account +- Reactivate account +- Grant temporary access +- Change plan +- Add internal notes + +All admin actions must create audit events. + +Audit record: + +```json +{ + "admin_id": "admin_123", + "action": "extend_grace_period", + "reason": "Enterprise customer requested invoice processing time", + "previous_state": "past_due", + "new_state": "payment_pending", + "created_at": "2026-06-15T00:00:00Z" +} +``` + +--- + +## 15. Webhook Processing Requirements + +Webhook processor must support: + +- Signature verification +- Idempotency keys +- Event deduplication +- Out-of-order event handling +- Retry-safe processing +- Dead-letter queue +- Manual replay + +Webhook events to handle: + +```text +customer.subscription.created +customer.subscription.updated +customer.subscription.deleted +invoice.created +invoice.finalized +invoice.paid +invoice.payment_failed +payment_intent.succeeded +payment_intent.payment_failed +charge.dispute.created +charge.refunded +``` + +Processing rule: + +```text +Never trust webhook order. +Always compare provider timestamp, local state, and event history. +``` + +--- + +## 16. Scheduled Jobs + +Create scheduled jobs for: + +| Job | Frequency | Purpose | +|---|---:|---| +| Trial ending reminder job | Daily | Notify customers before trial ends | +| Trial expiration job | Hourly | Expire or convert ended trials | +| Payment retry job | Hourly | Retry failed invoices | +| Payment pending timeout job | Hourly | Move stale pending payments to past due | +| Past due timeout job | Hourly | Move past due subscriptions to suspended | +| Suspension timeout job | Daily | Cancel long-unpaid subscriptions | +| Provider reconciliation job | Daily | Compare local state with billing provider | +| Entitlement sync job | Hourly | Ensure access matches subscription state | + +--- + +## 17. API Requirements + +### Create Trial + +```http +POST /subscriptions/trials +``` + +Request: + +```json +{ + "workspace_id": "workspace_123", + "plan_id": "plan_pro", + "payment_method_id": "pm_123" +} +``` + +Response: + +```json +{ + "subscription_id": "sub_123", + "status": "trialing", + "trial_end_at": "2026-06-08T00:00:00Z" +} +``` + +--- + +### Start Paid Subscription + +```http +POST /subscriptions +``` + +Request: + +```json +{ + "workspace_id": "workspace_123", + "plan_id": "plan_pro", + "payment_method_id": "pm_123" +} +``` + +--- + +### Cancel Subscription + +```http +POST /subscriptions/{subscription_id}/cancel +``` + +Request: + +```json +{ + "mode": "period_end", + "reason": "customer_requested" +} +``` + +--- + +### Reactivate Subscription + +```http +POST /subscriptions/{subscription_id}/reactivate +``` + +Request: + +```json +{ + "payment_method_id": "pm_123" +} +``` + +--- + +### Get Entitlements + +```http +GET /workspaces/{workspace_id}/entitlements +``` + +Response: + +```json +{ + "workspace_id": "workspace_123", + "access_level": "full", + "features": [ + { + "feature_key": "advanced_reports", + "enabled": true + } + ] +} +``` + +--- + +## 18. Policy Configuration + +Create a centralized subscription policy config. + +```json +{ + "trial": { + "enabled": true, + "duration_days": 14, + "requires_payment_method": true, + "one_trial_per_workspace": true, + "one_trial_per_company": true + }, + "payment": { + "payment_pending_timeout_days": 7, + "retry_schedule_days": [0, 3, 6, 10, 14], + "max_retry_attempts": 5 + }, + "past_due": { + "timeout_days": 7, + "access_level": "limited" + }, + "suspension": { + "cancel_after_days": 16, + "access_level": "read_only" + }, + "cancellation": { + "default_customer_cancel_mode": "period_end", + "admin_cancel_mode": "immediate" + }, + "access": { + "trialing": "full", + "active": "full", + "payment_pending": "full", + "past_due": "limited", + "suspended": "read_only", + "canceled": "none", + "expired": "none", + "paused": "read_only" + }, + "notifications": { + "trial_ending_days_before": [7, 3, 1], + "payment_failure_days_after": [0, 3, 6], + "past_due_days_after": [7], + "suspension_days_after": [14], + "cancellation_days_after": [30] + } +} +``` + +--- + +## 19. Implementation Phases + +## Phase 1: Foundation + +### Goals + +- Define status model +- Create subscription tables +- Create event table +- Create basic policy config +- Implement basic subscription service + +### Deliverables + +- Subscription schema +- Event schema +- Subscription status enum +- Policy config +- Subscription creation endpoint +- Trial creation endpoint + +### Acceptance Criteria + +- A workspace can start a trial. +- A workspace can start a paid subscription. +- Subscription status is persisted. +- Every status change creates an event. + +--- + +## Phase 2: Billing Provider Integration + +### Goals + +- Integrate billing provider +- Create customer records +- Create subscriptions +- Track invoices +- Track payment attempts + +### Deliverables + +- Billing provider adapter +- Webhook processor +- Invoice model +- Payment attempt model +- Payment success handling +- Payment failure handling + +### Acceptance Criteria + +- Successful payment activates subscription. +- Failed payment moves subscription to `payment_pending`. +- Webhooks are idempotent. +- Duplicate webhooks do not duplicate events. + +--- + +## Phase 3: Trial Conversion + +### Goals + +- Convert trials automatically +- Handle trial expiration +- Notify users before trial end + +### Deliverables + +- Trial ending reminder job +- Trial conversion job +- Trial expiration handling +- Trial notification templates + +### Acceptance Criteria + +- Trial converts to `active` after successful payment. +- Trial moves to `payment_pending` after failed payment. +- Trial moves to `expired` if payment is not available. +- Trial ending reminders are sent on schedule. + +--- + +## Phase 4: Payment Recovery + +### Goals + +- Implement dunning flow +- Retry failed payments +- Enforce payment pending timeout +- Enforce past due timeout + +### Deliverables + +- Payment retry job +- Payment pending timeout job +- Past due timeout job +- Notification schedule +- Admin visibility for failed payments + +### Acceptance Criteria + +- Failed payments are retried on schedule. +- `payment_pending` becomes `past_due` after timeout. +- `past_due` becomes `suspended` after timeout. +- Customers and admins receive notifications. + +--- + +## Phase 5: Entitlements + +### Goals + +- Separate subscription status from access +- Create entitlement service +- Enforce access levels in product + +### Deliverables + +- Entitlement model +- Entitlement service +- Access decision API +- Feature flag integration +- Middleware or guard layer + +### Acceptance Criteria + +- `active` and `trialing` users receive full access. +- `past_due` users receive limited access. +- `suspended` users receive read-only or no access. +- Product code does not directly hardcode billing statuses. + +--- + +## Phase 6: Cancellation and Reactivation + +### Goals + +- Support customer cancellation +- Support admin cancellation +- Support reactivation +- Support cancel-at-period-end + +### Deliverables + +- Cancel subscription endpoint +- Reactivate subscription endpoint +- Period-end cancellation job +- Admin cancellation flow + +### Acceptance Criteria + +- Customer cancellation defaults to period end. +- Admin cancellation can happen immediately. +- Reactivation requires successful payment. +- Canceled subscriptions do not renew. + +--- + +## Phase 7: Admin Console + +### Goals + +- Give support and finance teams operational control +- Add audit logging for all manual actions + +### Deliverables + +- Subscription detail page +- Invoice history +- Payment attempt history +- Event timeline +- Manual retry action +- Extend trial action +- Extend grace period action +- Suspend action +- Reactivate action +- Cancel action + +### Acceptance Criteria + +- Admins can inspect complete subscription history. +- Admin actions are audited. +- High-risk actions require a reason. +- Manual overrides do not silently mutate state. + +--- + +## Phase 8: Reconciliation and Hardening + +### Goals + +- Prevent billing drift +- Detect inconsistent states +- Make webhook processing resilient + +### Deliverables + +- Provider reconciliation job +- Dead-letter queue +- Webhook replay tool +- State consistency checks +- Alerting + +### Acceptance Criteria + +- Local subscription state matches billing provider state. +- Failed webhooks are visible and replayable. +- Inconsistent states generate alerts. +- Reconciliation does not break valid admin overrides. + +--- + +## 20. State Invariants + +The system must enforce these rules: + +```text +A canceled subscription cannot renew. +A canceled subscription cannot become active without reactivation. +A subscription cannot be both active and canceled. +A trial cannot be active after trial_end_at. +A payment success must be idempotent. +A failed payment must not duplicate payment attempts. +A workspace cannot have multiple active subscriptions for the same product unless explicitly allowed. +Entitlements must be recalculated after every subscription status change. +Every manual override must create an audit event. +``` + +--- + +## 21. Monitoring and Alerts + +Track these metrics: + +```text +trial_started_count +trial_conversion_rate +payment_failed_count +payment_recovered_count +payment_pending_count +past_due_count +suspended_count +canceled_count +reactivated_count +webhook_failure_count +webhook_duplicate_count +reconciliation_mismatch_count +``` + +Alert on: + +```text +Webhook processing failures +Spike in payment failures +Subscriptions stuck in payment_pending +Subscriptions stuck in past_due +Provider/local state mismatch +Entitlement sync failures +Unexpected cancellation spike +``` + +--- + +## 22. Testing Plan + +### Unit Tests + +Test: + +- Status transitions +- Policy config +- Retry schedule +- Entitlement mapping +- Cancellation rules +- Reactivation rules + +### Integration Tests + +Test: + +- Payment success webhook +- Payment failure webhook +- Duplicate webhook +- Out-of-order webhook +- Trial conversion +- Payment retry +- Suspension +- Cancellation + +### End-to-End Tests + +Test: + +```text +Start trial → trial ends → payment succeeds → active +Start trial → trial ends → payment fails → payment_pending → past_due → suspended → canceled +Active subscription → renewal fails → payment_pending → payment succeeds → active +Active subscription → user cancels → active until period end → canceled +Suspended subscription → payment succeeds → active +``` + +--- + +## 23. Rollout Plan + +### Step 1: Internal Testing + +- Enable for internal workspaces only. +- Use test billing provider mode. +- Simulate webhooks. +- Validate event history. + +### Step 2: Limited Beta + +- Enable for selected B2B customers. +- Monitor payment failures. +- Monitor state transitions. +- Review support tickets. + +### Step 3: Full Launch + +- Enable for all new B2B customers. +- Keep legacy subscriptions on old system temporarily. +- Run reconciliation daily. + +### Step 4: Migration + +- Migrate old subscriptions into new status model. +- Generate synthetic migration events. +- Validate entitlements. +- Confirm invoice state. +- Freeze legacy billing writes. + +--- + +## 24. Migration Plan + +For each existing subscription: + +1. Identify current provider status. +2. Map provider status to internal status. +3. Create internal subscription record. +4. Create current entitlement record. +5. Import current invoice state. +6. Create `subscription.migrated` event. +7. Run reconciliation check. +8. Validate workspace access. + +Example mapping: + +| Provider State | Internal Status | +|---|---| +| `trialing` | `trialing` | +| `active` | `active` | +| `incomplete` | `payment_pending` | +| `past_due` | `past_due` | +| `unpaid` | `suspended` | +| `canceled` | `canceled` | + +--- + +## 25. Risks + +| Risk | Mitigation | +|---|---| +| Duplicate webhooks | Idempotency keys and event deduplication | +| Out-of-order webhooks | Compare timestamps and current state | +| Incorrect access removal | Entitlement layer and audit logs | +| Customers canceled too early | Grace period and admin override | +| Trial abuse | Company-level trial eligibility checks | +| Billing provider mismatch | Daily reconciliation | +| Admin misuse | Audit logs and approval rules | +| Status sprawl | Strict status model | + +--- + +## 26. Final Default Policy + +Use this as the baseline B2B policy: + +```text +Trial lasts 14 days. +Payment method is required before trial starts. +Trial converts automatically if payment succeeds. +If payment fails, subscription becomes payment_pending. +Payment pending lasts 7 days with full access. +After 7 unpaid days, subscription becomes past_due with limited access. +After 14 unpaid days, subscription becomes suspended. +After 30 unpaid days, subscription is canceled. +Customer cancellation takes effect at period end. +Admin cancellation can be immediate. +Reactivation requires successful payment. +Chargebacks suspend access immediately. +Access is controlled through entitlements. +All transitions are event-driven and audited. +``` + +--- + +## 27. Definition of Done + +The B2B subscription system is complete when: + +- All subscription statuses are implemented. +- All transition rules are enforced. +- Billing provider webhooks are idempotent. +- Failed payments enter recovery flow. +- Trial conversion works automatically. +- Entitlements control product access. +- Admins can override safely. +- Every lifecycle change has an audit event. +- Reconciliation detects billing drift. +- Monitoring and alerts are live. +- End-to-end tests cover the full lifecycle.