feat(billing): billing-service + @fuzefront/billing-ui (re-land of orphaned #81) - #88
Conversation
Add four new TOPICS constants (BILLING_USAGE_RECORDED, BILLING_SUBSCRIPTION_CHANGED, BILLING_TRIAL_ENDING, BILLING_PAYMENT_FAILED) to shared/src/kafka/types.ts. Add two new Zod payload schemas: - billingUsageRecordedSchemaV1 (entityId uuid, entityType enum, meterEventName, quantity int positive, occurredAt datetime) — no correlationId, it lives on the FuzeEvent envelope. - billingSubscriptionChangedSchemaV1 (entityId uuid, entityType enum, planTier, status, optional seatQuantity int, stripeSubscriptionId) — mirrors Stripe subscription status. Wire both schemas into shared/src/kafka/schemas/index.ts barrel. Add minimal Jest test runner to shared (jest + ts-jest mirroring email-service conventions). 18 new schema-parse tests cover valid parses and invalid rejections (bad uuid, negative quantity, wrong enum, bad datetime, missing required fields). TopicName type widens automatically from the updated TOPICS const — no manual changes needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the billing-service microservice skeleton under services/billing-service/. Includes TDD health + config tests (5/5 passing), multi-stage Dockerfile (Docker build verified), and lerna.json registration. Deviations from email-service pattern (documented): - @fuzefront/shared uses file:../../shared (not registry version) for local dev without GitHub token — same as provisioning-service - Dockerfile uses `cd X && npm install` instead of `npm ci --workspace=X` because billing-service is not in root workspaces; `npm ci --workspace` fails for services not declared in root package.json workspaces - Shared built with `npm run build:kafka` (not `npm run build`) to avoid socket.io-client / React DOM dep that blocks full-barrel tsc in Docker Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the callback-based server.close in a Promise so later tasks (Kafka/DB teardown) can sequence async cleanup before process.exit. Addresses T2 review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/migrations/001_billing_schema.sql: idempotent DDL for all 5 billing.* tables (customers, subscriptions, stripe_events, usage_events, plans) with FK, UNIQUE constraints, and pgcrypto extension guard - src/db.ts: createPool (pg.Pool factory) + runMigrations (reads + executes SQL) - src/index.ts: call runMigrations at boot, guarded by config.databaseUrl - tests/db.test.ts: SQL-shape assertions (28 passing, no live DB needed) + skipped integration suite for when DATABASE_URL is present Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds four billing hot-path cache columns to both the `users` and `organizations` tables (public schema): - stripe_customer_id TEXT, nullable - billing_plan_tier TEXT NOT NULL DEFAULT 'free' - billing_plan_status TEXT NOT NULL DEFAULT 'active' - trial_ends_at TIMESTAMPTZ, nullable Each column addition is guarded with hasColumn so the migration is idempotent. down() drops all eight columns with matching guards. Added to both backend/src/migrations/ and backend/security/src/migrations/ (byte-identical) per the dual-location lock-step policy (001–010 identical across both dirs). Extended the security migration integration test to assert all 8 billing columns exist with correct nullability and defaults after the chain runs. The test skips cleanly when Postgres is unreachable (CI-safe). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es, kafka, helm/values, release wiring (incomplete; types pending) [skip ci]
… validation, dockerignore Resolves the salvage checkpoint's "types pending" item. The billing-service TypeScript production build now passes (verified via docker build of services/billing-service/Dockerfile, EXIT=0). - Route input validation: add validateBody() helper returning a non-union ValidationResult so routes read .data/.details without discriminated-union narrowing, which does not fire under the service's strict:false tsconfig (mirrors the cast workaround in sms-service). Refactor setup-intent, subscriptions, credits routes onto it. - @fuzefront/shared kafka imports already resolve via /dist/kafka subpath (classic node resolution can't read the package "exports" map). - Add root .dockerignore excluding **/node_modules + **/.git so a host (Windows) node_modules cannot shadow the in-image install and corrupt the build (was producing a truncated typescript binary). - Add run-tests-in-docker.sh: runs jest in a clean node:18 Linux container (local Windows npm install is unreliable here; CI runs jest natively). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CustomerRepository interface gained findByStripeCustomerId (used by the webhook handlers' reverse lookup). The customer.service unit test's in-memory fake must implement it to satisfy the interface. Fixes the one failing suite (9/10 → 10/10; other 9 suites and 51 tests already passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The identity track also adds a 010_* migration (010_api_tokens). Add an in-file note that final integration must renumber one of the two 010 migrations. PR body carries the same warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The committed lock referenced @types/jest in the manifest mirror but lacked the node_modules/@types/jest install entry, so `npm ci` failed with 'Missing: @types/jest from lock file' in CI and Docker builds. Regenerate the lock so ci/build install deterministically. Verified in node:18-alpine (mirrors CI): - billing-service: npm ci OK, tsc --noEmit (src+tests) OK, jest 54 passed/3 skipped - billing-client: npm ci OK, tsc --noEmit (src+tests) OK, jest 6 passed - stripe 17.7.0 (bundled types), no live Stripe calls (mocked) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ generated client types [skip ci]
…ild, vitest, private publishConfig) [skip ci]
…dd --scrim DS token [skip ci]
…Manager, UsagePanel, PaymentMethodPanel, barrel [skip ci]
…ocked Stripe, subscription, panels, modal, status) [skip ci]
…nder jsx-runtime); ignore .npm-cache [skip ci]
…9 green) [skip ci]
# Conflicts: # deploy/helm/fuzefront/templates/secret.yaml # deploy/helm/fuzefront/values.yaml # lerna.json # package-lock.json # shared/dist/index.d.ts # shared/dist/index.js # shared/dist/kafka/schemas/index.d.ts # shared/src/kafka/schemas/index.ts
…ORM on CI) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t workspaces The agent's workspace registration was left unstaged; the merge commit omitted it, so CI's workspace-deps gate saw @fuzefront/billing-client (a peerDep ^1.0.0) as an unregistered in-repo package. Register both so it resolves from source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… with identity 010_api_tokens) #65 (on master) added backend/security migration 010_create_api_tokens_table; billing's 010_add_billing_to_entities collided. Renumbered to 011 to restore a unique ordering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # package-lock.json
CI autofix: ERR_MODULE_NOT_FOUND crash in shared/dist/kafkaRoot cause: Fix: Changed Branch: |
CI failure root cause + fix (automated)Failing job: Playwright sign-in flow → Error: Root cause: The committed Fix: The Branch: The branch is pushed and ready to be merged into |
Why a new PR
#81 reported MERGED but its squash commit (
a3084f3) was orphaned — it was admin-merged while the branch was behind#87(which auto-merged moments earlier), so master's ref never advanced to it and billing never landed. This re-lands the same work, now up-to-date with master.Contents
services/billing-service(Stripe client, subscription/metering/plan/customer/permit services + tests)@fuzefront/billing-ui(design-system-first UI package) +@fuzefront/billing-clientbilling-client+packages/billing-uiin root workspaces010 → 011(collision with identity's010_create_api_tokens_tablealready on master)Verification
In-repo packages resolve from source(workspace-deps gate) greenbackend-testsis pre-existing-red on master itself (not a billing regression)🤖 Generated with Claude Code