create-ailk 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/component-catalog.md +210 -52
- package/dist/cli.js +0 -0
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +14 -15
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +35 -4
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +471 -133
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +602 -29
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/layout.tsx +13 -1
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +20 -13
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -1
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +1 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/content/_site.mdx +12 -0
- package/templates/database/CHANGELOG.md +94 -0
- package/templates/database/inbox/schema.prisma +165 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/db-generate-locked.sh +0 -0
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1278
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* Org-billing route group (#3701 — the apps/api predecessor to the
|
|
4
|
-
* org-billing UI, #3697).
|
|
5
|
-
*
|
|
6
|
-
* Routes:
|
|
7
|
-
* GET /v1/billing — org-scoped billing read (tier, seat
|
|
8
|
-
* count, billing-customer presence,
|
|
9
|
-
* entitlements)
|
|
10
|
-
* GET /v1/billing/usage — org-scoped usage read (#3717): the
|
|
11
|
-
* current-period report summary + credit
|
|
12
|
-
* status, client-safe (no Stripe id)
|
|
13
|
-
* POST /v1/billing/portal-session — create a Stripe customer-portal
|
|
14
|
-
* session for the caller's own org
|
|
15
|
-
*
|
|
16
|
-
* Registered INSIDE server.ts's `tenantScope` (spec D4 / AC-5) so
|
|
17
|
-
* `request.tenant` is populated by `tenantPreHandlerPlugin` before either
|
|
18
|
-
* handler runs — NOT at the public/checkout registration site, and NOT
|
|
19
|
-
* behind the `X-Internal-Auth` gate (spec 2.1.5).
|
|
20
|
-
*
|
|
21
|
-
* API↔MCP parity: the read is admin/settings-shaped (a human managing their
|
|
22
|
-
* own org's billing), not an agent tool — no MCP twin is warranted (spec §5,
|
|
23
|
-
* AC-6). The tenant-scoped authed MCP surface is tracked separately (#3591),
|
|
24
|
-
* mirroring the sibling /v1/workspaces/* routes' parity note.
|
|
25
|
-
*/
|
|
26
|
-
import type { FastifyPluginAsync } from "fastify";
|
|
27
|
-
|
|
28
|
-
import { billingPortalSessionHandler } from "./portal.js";
|
|
29
|
-
import { billingReadHandler } from "./read.js";
|
|
30
|
-
import { billingUsageHandler } from "./usage.js";
|
|
31
|
-
|
|
32
|
-
export const billingRoutes: FastifyPluginAsync = async (fastify) => {
|
|
33
|
-
fastify.get("/v1/billing", billingReadHandler);
|
|
34
|
-
fastify.get("/v1/billing/usage", billingUsageHandler);
|
|
35
|
-
fastify.post("/v1/billing/portal-session", billingPortalSessionHandler);
|
|
36
|
-
};
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* POST /v1/billing/portal-session
|
|
4
|
-
*
|
|
5
|
-
* Creates a Stripe billing-portal session for the caller's OWN org (#3701 —
|
|
6
|
-
* the apps/api predecessor to the org-billing UI, #3697). Org scope is
|
|
7
|
-
* ALWAYS `request.tenant.organizationId` (spec D1) — never a client-supplied
|
|
8
|
-
* id, so this route can never act on a foreign org. Reuses the existing
|
|
9
|
-
* `createBillingPortalSession` wrapper (spec D3) and mirrors
|
|
10
|
-
* `checkout/sessions.ts`'s 404/500/502 error mapping exactly:
|
|
11
|
-
*
|
|
12
|
-
* 404 no-billing-customer — the org has no Stripe customer yet (no Stripe call)
|
|
13
|
-
* 500 stripe-not-configured — STRIPE_SECRET_KEY is missing/empty (no Stripe call)
|
|
14
|
-
* 500 live-mode-forbidden — STRIPE_SECRET_KEY is a live key (assertSandboxKey guard)
|
|
15
|
-
* 500 config-error — STRIPE_SECRET_KEY invalid configuration (from the SDK wrapper)
|
|
16
|
-
* 502 portal-unavailable — Stripe SDK threw an unexpected error
|
|
17
|
-
*
|
|
18
|
-
* Registered inside `server.ts`'s `tenantScope` (spec D4) behind
|
|
19
|
-
* `tenantPreHandlerPlugin` — cookie/session auth via the tenant scope, NOT
|
|
20
|
-
* the `X-Internal-Auth` internal gate `checkout/sessions.ts` uses (spec
|
|
21
|
-
* 2.1.5): the caller here is apps/workspace forwarding the browser cookie,
|
|
22
|
-
* not a server-to-server internal call.
|
|
23
|
-
*
|
|
24
|
-
* AUTHORIZATION (#3766): the tenant preHandler establishes session + org
|
|
25
|
-
* MEMBERSHIP only — it does not discriminate role. The billing portal is a
|
|
26
|
-
* privileged, MUTATING capability (Stripe's portal lets the holder change the
|
|
27
|
-
* payment method, download invoices, and cancel the subscription), so
|
|
28
|
-
* membership alone is not sufficient: this handler additionally requires an
|
|
29
|
-
* `owner` or `admin` role and answers 403 for a `member`. The check runs
|
|
30
|
-
* BEFORE the `getOrgBillingIdentity` lookup and before any Stripe call,
|
|
31
|
-
* mirroring `middleware/tenant.ts`'s default-deny ordering (deny before any
|
|
32
|
-
* lookup of the target, so an unauthorized caller learns nothing — not even
|
|
33
|
-
* whether the org has a billing customer).
|
|
34
|
-
*
|
|
35
|
-
* PII/secret-min: the raw `billingCustomerId` is used only as an input to
|
|
36
|
-
* `createBillingPortalSession` — it never appears in a response body or log
|
|
37
|
-
* line (spec 5.2).
|
|
38
|
-
*/
|
|
39
|
-
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
40
|
-
|
|
41
|
-
import { config } from "../../config/index.js";
|
|
42
|
-
import {
|
|
43
|
-
assertSandboxKey,
|
|
44
|
-
createBillingPortalSession,
|
|
45
|
-
StripeConfigError,
|
|
46
|
-
} from "../../lib/stripe.js";
|
|
47
|
-
import { getOrgBillingIdentity } from "../../services/entitlements.js";
|
|
48
|
-
import type { MembershipRole } from "../../services/tenant-context.js";
|
|
49
|
-
|
|
50
|
-
function unauthorized(reply: FastifyReply) {
|
|
51
|
-
return reply.status(401).send({
|
|
52
|
-
ok: false,
|
|
53
|
-
error: { code: "unauthorized", message: "Authentication required" },
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function forbidden(reply: FastifyReply) {
|
|
58
|
-
return reply.status(403).send({
|
|
59
|
-
ok: false,
|
|
60
|
-
error: { code: "forbidden", message: "Forbidden" },
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Roles permitted to open the org's Stripe billing portal (#3766).
|
|
66
|
-
*
|
|
67
|
-
* Mirrors the owner/admin floor already applied to workspace creation in
|
|
68
|
-
* `services/tenant-context.ts` (`createWorkspaceForOrg` — "Creating a
|
|
69
|
-
* workspace requires the admin role"): same policy, same two roles. Expressed
|
|
70
|
-
* here as an allow-list rather than a `!==` chain so it is default-deny — a
|
|
71
|
-
* role later added to `MembershipRole` is refused until it is named here,
|
|
72
|
-
* instead of silently falling through into a privileged path.
|
|
73
|
-
*/
|
|
74
|
-
const BILLING_PORTAL_ROLES: ReadonlySet<MembershipRole> = new Set<MembershipRole>([
|
|
75
|
-
"owner",
|
|
76
|
-
"admin",
|
|
77
|
-
]);
|
|
78
|
-
|
|
79
|
-
// Provisional return path (spec OQ-2 — resolved at implementation): the
|
|
80
|
-
// org-billing UI (#3697) has not landed a page path yet. Points at the
|
|
81
|
-
// workspace app's future billing-settings page under the configured frontend
|
|
82
|
-
// origin; finalize this constant when #3697 sets the real path.
|
|
83
|
-
const PORTAL_RETURN_PATH = "/settings/billing";
|
|
84
|
-
|
|
85
|
-
export async function billingPortalSessionHandler(
|
|
86
|
-
request: FastifyRequest,
|
|
87
|
-
reply: FastifyReply,
|
|
88
|
-
): Promise<void> {
|
|
89
|
-
const tenant = request.tenant;
|
|
90
|
-
if (!tenant) return unauthorized(reply);
|
|
91
|
-
|
|
92
|
-
// Role gate (#3766) — BEFORE any lookup or Stripe call. The tenant
|
|
93
|
-
// preHandler proved membership; opening the billing portal additionally
|
|
94
|
-
// requires owner/admin.
|
|
95
|
-
if (!BILLING_PORTAL_ROLES.has(tenant.role)) {
|
|
96
|
-
request.log.warn(
|
|
97
|
-
{ method: request.method, path: request.url, status: 403 },
|
|
98
|
-
"[billing] portal session denied; insufficient role",
|
|
99
|
-
);
|
|
100
|
-
return forbidden(reply);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const identity = await getOrgBillingIdentity(tenant.organizationId);
|
|
104
|
-
if (!identity.billingCustomerId) {
|
|
105
|
-
// No Stripe call — the org has no billing customer to open a portal for.
|
|
106
|
-
return reply.status(404).send({
|
|
107
|
-
ok: false,
|
|
108
|
-
error: {
|
|
109
|
-
code: "no-billing-customer",
|
|
110
|
-
message: "No billing customer for this organization",
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const stripeSecretKey = process.env["STRIPE_SECRET_KEY"];
|
|
116
|
-
if (!stripeSecretKey) {
|
|
117
|
-
return reply.status(500).send({
|
|
118
|
-
ok: false,
|
|
119
|
-
error: {
|
|
120
|
-
code: "stripe-not-configured",
|
|
121
|
-
message: "Stripe is not configured",
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
assertSandboxKey(stripeSecretKey);
|
|
127
|
-
} catch {
|
|
128
|
-
return reply.status(500).send({
|
|
129
|
-
ok: false,
|
|
130
|
-
error: {
|
|
131
|
-
code: "live-mode-forbidden",
|
|
132
|
-
message: "Live Stripe keys are not permitted",
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const { url } = await createBillingPortalSession({
|
|
139
|
-
stripeCustomerId: identity.billingCustomerId,
|
|
140
|
-
returnUrl: `${config.FRONTEND_URL}${PORTAL_RETURN_PATH}`,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
request.log.info(
|
|
144
|
-
{ method: request.method, path: request.url, status: 200 },
|
|
145
|
-
"[billing] portal session created",
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
return reply.status(200).send({ ok: true, data: { url } });
|
|
149
|
-
} catch (err) {
|
|
150
|
-
if (err instanceof StripeConfigError) {
|
|
151
|
-
// 500 — misconfiguration; log message (no PII) but never echo in response.
|
|
152
|
-
request.log.error(
|
|
153
|
-
{
|
|
154
|
-
method: request.method,
|
|
155
|
-
path: request.url,
|
|
156
|
-
errName: err.name,
|
|
157
|
-
message: err.message,
|
|
158
|
-
},
|
|
159
|
-
"[billing] config error",
|
|
160
|
-
);
|
|
161
|
-
return reply.status(500).send({
|
|
162
|
-
ok: false,
|
|
163
|
-
error: { code: "config-error", message: "Billing portal is misconfigured" },
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
request.log.error(
|
|
167
|
-
{
|
|
168
|
-
method: request.method,
|
|
169
|
-
path: request.url,
|
|
170
|
-
errName: err instanceof Error ? err.name : "unknown",
|
|
171
|
-
},
|
|
172
|
-
"[billing] stripe error; portal-unavailable",
|
|
173
|
-
);
|
|
174
|
-
return reply.status(502).send({
|
|
175
|
-
ok: false,
|
|
176
|
-
error: {
|
|
177
|
-
code: "portal-unavailable",
|
|
178
|
-
message: "Billing portal is temporarily unavailable",
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* GET /v1/billing
|
|
4
|
-
*
|
|
5
|
-
* Org-scoped billing read (#3701 — the apps/api predecessor to the
|
|
6
|
-
* org-billing UI, #3697). The org is ALWAYS `request.tenant.organizationId`
|
|
7
|
-
* — there is no client-supplied org id, so cross-tenant enumeration is
|
|
8
|
-
* structurally impossible (spec D1). Returns a client-safe projection:
|
|
9
|
-
* `hasBillingCustomer` is a presence flag only — the raw Stripe customer id
|
|
10
|
-
* never crosses the API boundary (spec D2).
|
|
11
|
-
*
|
|
12
|
-
* Registered inside `server.ts`'s `tenantScope` (spec D4) — `request.tenant`
|
|
13
|
-
* is populated by `tenantPreHandlerPlugin` before this handler ever runs; a
|
|
14
|
-
* caller with no resolvable org membership is denied by the preHandler
|
|
15
|
-
* itself (403), never reaching this code.
|
|
16
|
-
*
|
|
17
|
-
* AUTHORIZATION (#3766) — membership is deliberately sufficient here; this
|
|
18
|
-
* route intentionally carries NO owner/admin role gate, unlike its sibling
|
|
19
|
-
* `portal.ts`. The distinction is capability, not merely read-vs-write:
|
|
20
|
-
*
|
|
21
|
-
* - It exposes no financial detail. The projection is plan metadata only —
|
|
22
|
-
* `tier`, `seatCount`, and the `hasBillingCustomer` PRESENCE FLAG. No
|
|
23
|
-
* invoices, no amounts, no payment methods, no subscription dates, and
|
|
24
|
-
* (per spec D2) never the raw Stripe customer id.
|
|
25
|
-
* - It confers no capability. There is nothing here to mutate; contrast the
|
|
26
|
-
* portal, whose session lets its holder change the payment method and
|
|
27
|
-
* cancel the subscription — which is why that route gates on role.
|
|
28
|
-
* - Its data is operationally load-bearing for ordinary members. `tier` and
|
|
29
|
-
* `entitlements` are what the app reads to decide which features to
|
|
30
|
-
* render; gating them on owner/admin would break the product for members
|
|
31
|
-
* without protecting anything sensitive.
|
|
32
|
-
*
|
|
33
|
-
* If this projection is ever widened to carry invoices, amounts, payment
|
|
34
|
-
* methods, or subscription lifecycle state, that change MUST also add the
|
|
35
|
-
* `portal.ts` role gate — the reason for its absence would no longer hold.
|
|
36
|
-
*/
|
|
37
|
-
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
38
|
-
|
|
39
|
-
import {
|
|
40
|
-
getOrgBillingIdentity,
|
|
41
|
-
resolveEntitlements,
|
|
42
|
-
} from "../../services/entitlements.js";
|
|
43
|
-
|
|
44
|
-
function unauthorized(reply: FastifyReply) {
|
|
45
|
-
return reply.status(401).send({
|
|
46
|
-
ok: false,
|
|
47
|
-
error: { code: "unauthorized", message: "Authentication required" },
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function billingReadHandler(
|
|
52
|
-
request: FastifyRequest,
|
|
53
|
-
reply: FastifyReply,
|
|
54
|
-
): Promise<void> {
|
|
55
|
-
const tenant = request.tenant;
|
|
56
|
-
// Defensive fallback — the tenant preHandler denies an unresolvable tenant
|
|
57
|
-
// before this handler runs; `request.tenant` is `TenantContext | undefined`
|
|
58
|
-
// at the type level, so this mirrors the sibling workspace routes.
|
|
59
|
-
if (!tenant) return unauthorized(reply);
|
|
60
|
-
|
|
61
|
-
const [identity, entitlements] = await Promise.all([
|
|
62
|
-
getOrgBillingIdentity(tenant.organizationId),
|
|
63
|
-
resolveEntitlements(tenant.organizationId),
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
return reply.status(200).send({
|
|
67
|
-
ok: true,
|
|
68
|
-
data: {
|
|
69
|
-
tier: entitlements.limits.static.tier,
|
|
70
|
-
seatCount: entitlements.limits.static.seats,
|
|
71
|
-
hasBillingCustomer: identity.billingCustomerId != null,
|
|
72
|
-
entitlements,
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// oss: false
|
|
2
|
-
/**
|
|
3
|
-
* GET /v1/billing/usage
|
|
4
|
-
*
|
|
5
|
-
* Org-scoped usage read (#3717 — the read surface the P3 usage UI, #3706 P3.S3,
|
|
6
|
-
* consumes). Extends the `billing` route family established by #3701 — same
|
|
7
|
-
* tenant-guard + `/v1` pattern, NOT a forked family.
|
|
8
|
-
*
|
|
9
|
-
* The org is ALWAYS `request.tenant.organizationId` — there is no
|
|
10
|
-
* client-supplied org id, so cross-tenant enumeration is structurally
|
|
11
|
-
* impossible (spec D1 / AC-UB-10). Returns a client-safe projection: P2's
|
|
12
|
-
* aggregation summary (totals + eventCount + the attribution split) plus P3.S1's
|
|
13
|
-
* credit status (`includedRemaining`, `creditBalance`, `effectiveRemaining`).
|
|
14
|
-
* NEVER a Stripe id / `billingCustomerId` (spec D3) — the response is an
|
|
15
|
-
* explicit projection, not a spread of a service result.
|
|
16
|
-
*
|
|
17
|
-
* Registered inside `server.ts`'s `tenantScope` (spec D4) behind
|
|
18
|
-
* `tenantPreHandlerPlugin` — a caller with no resolvable org membership is
|
|
19
|
-
* denied by the preHandler (403), never reaching this handler.
|
|
20
|
-
*
|
|
21
|
-
* Read-only: the credit *consumption* waterfall (P3.S1 `consumeCredits`) is a
|
|
22
|
-
* server-internal service, not this route (spec 2.1.5). `aggregateUsagePeriod`
|
|
23
|
-
* is idempotent (it re-reads events and upserts the same report), so a GET that
|
|
24
|
-
* calls it recomputes rather than mutates.
|
|
25
|
-
*/
|
|
26
|
-
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
27
|
-
|
|
28
|
-
import type { MembershipRole } from "../../services/tenant-context.js";
|
|
29
|
-
import { aggregateUsagePeriod } from "../../services/usage-aggregation.js";
|
|
30
|
-
import { getCreditStatus } from "../../services/usage-credits.js";
|
|
31
|
-
|
|
32
|
-
function unauthorized(reply: FastifyReply) {
|
|
33
|
-
return reply.status(401).send({
|
|
34
|
-
ok: false,
|
|
35
|
-
error: { code: "unauthorized", message: "Authentication required" },
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function forbidden(reply: FastifyReply) {
|
|
40
|
-
return reply.status(403).send({
|
|
41
|
-
ok: false,
|
|
42
|
-
error: { code: "forbidden", message: "Forbidden" },
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Roles permitted to read the org's usage projection (#3819).
|
|
48
|
-
*
|
|
49
|
-
* Mirrors `BILLING_PORTAL_ROLES` in `portal.ts` (#3766) exactly — same
|
|
50
|
-
* owner/admin floor, same allow-list form. The tenant preHandler proves
|
|
51
|
-
* MEMBERSHIP, not authorization; the usage read exposes the org's actual spend
|
|
52
|
-
* (`totalBilledAmount`) and credit balance, so membership alone is not enough.
|
|
53
|
-
* Expressed as an allow-list rather than a `!==` chain so it is default-deny —
|
|
54
|
-
* a role later added to `MembershipRole` is refused until named here, instead
|
|
55
|
-
* of silently falling through into a privileged read.
|
|
56
|
-
*/
|
|
57
|
-
const BILLING_USAGE_ROLES: ReadonlySet<MembershipRole> = new Set<MembershipRole>([
|
|
58
|
-
"owner",
|
|
59
|
-
"admin",
|
|
60
|
-
]);
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Resolve the read period from the optional `?period=` selector.
|
|
64
|
-
* `{ ok: true, period }` on success (a valid Date, or the current instant when
|
|
65
|
-
* absent); `{ ok: false }` when a supplied value is unparseable (→ 400). The
|
|
66
|
-
* raw value is never echoed back.
|
|
67
|
-
*/
|
|
68
|
-
function resolvePeriod(raw: unknown): { ok: true; period: Date } | { ok: false } {
|
|
69
|
-
if (raw == null || raw === "") return { ok: true, period: new Date() };
|
|
70
|
-
if (typeof raw !== "string") return { ok: false };
|
|
71
|
-
const period = new Date(raw);
|
|
72
|
-
if (!Number.isFinite(period.getTime())) return { ok: false };
|
|
73
|
-
return { ok: true, period };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function billingUsageHandler(
|
|
77
|
-
request: FastifyRequest,
|
|
78
|
-
reply: FastifyReply,
|
|
79
|
-
): Promise<void> {
|
|
80
|
-
const tenant = request.tenant;
|
|
81
|
-
// Defensive fallback — the tenant preHandler denies an unresolvable tenant
|
|
82
|
-
// before this handler runs; `request.tenant` is `TenantContext | undefined`
|
|
83
|
-
// at the type level, so this mirrors the sibling billing routes.
|
|
84
|
-
if (!tenant) return unauthorized(reply);
|
|
85
|
-
|
|
86
|
-
// Role gate (#3819) — BEFORE any read (aggregation / credit) or even the
|
|
87
|
-
// period parse. The tenant preHandler proved membership; reading the org's
|
|
88
|
-
// spend + credit balance additionally requires owner/admin. Default-deny
|
|
89
|
-
// ordering, mirroring `portal.ts`'s #3766 gate: deny before any lookup, so an
|
|
90
|
-
// unauthorized caller learns nothing — not even whether a period is valid.
|
|
91
|
-
if (!BILLING_USAGE_ROLES.has(tenant.role)) {
|
|
92
|
-
request.log.warn(
|
|
93
|
-
{ method: request.method, path: request.url, status: 403 },
|
|
94
|
-
"[billing] usage read denied; insufficient role",
|
|
95
|
-
);
|
|
96
|
-
return forbidden(reply);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const period = resolvePeriod((request.query as { period?: unknown }).period);
|
|
100
|
-
if (!period.ok) {
|
|
101
|
-
return reply.status(400).send({
|
|
102
|
-
ok: false,
|
|
103
|
-
error: {
|
|
104
|
-
code: "validation-error",
|
|
105
|
-
message: "period must be a valid date",
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const [report, credit] = await Promise.all([
|
|
111
|
-
aggregateUsagePeriod(tenant.organizationId, period.period),
|
|
112
|
-
getCreditStatus(tenant.organizationId),
|
|
113
|
-
]);
|
|
114
|
-
|
|
115
|
-
if (!report.ok || !credit.ok) {
|
|
116
|
-
// Log the service detail server-side; NEVER echo it (a service error can
|
|
117
|
-
// embed the org id) — return a generic, id-free client message.
|
|
118
|
-
request.log.error(
|
|
119
|
-
{
|
|
120
|
-
method: request.method,
|
|
121
|
-
path: request.url,
|
|
122
|
-
reportError: report.ok ? undefined : report.error.kind,
|
|
123
|
-
creditError: credit.ok ? undefined : credit.error.kind,
|
|
124
|
-
},
|
|
125
|
-
"[billing] usage read unavailable",
|
|
126
|
-
);
|
|
127
|
-
return reply.status(500).send({
|
|
128
|
-
ok: false,
|
|
129
|
-
error: { code: "usage-unavailable", message: "Usage is unavailable" },
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return reply.status(200).send({
|
|
134
|
-
ok: true,
|
|
135
|
-
data: {
|
|
136
|
-
usage: {
|
|
137
|
-
totalProviderCost: report.totals.totalProviderCost,
|
|
138
|
-
totalMarkupAmount: report.totals.totalMarkupAmount,
|
|
139
|
-
totalBilledAmount: report.totals.totalBilledAmount,
|
|
140
|
-
totalTokens: report.totals.totalTokens,
|
|
141
|
-
eventCount: report.totals.eventCount,
|
|
142
|
-
includedCents: report.totals.includedCents,
|
|
143
|
-
creditBalanceCents: report.totals.creditBalanceCents,
|
|
144
|
-
onDemandCents: report.totals.onDemandCents,
|
|
145
|
-
},
|
|
146
|
-
credit: {
|
|
147
|
-
includedRemaining: credit.includedRemaining,
|
|
148
|
-
creditBalance: credit.creditBalance,
|
|
149
|
-
effectiveRemaining: credit.effectiveRemaining,
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
}
|