@build-astron-co/nimbus 0.2.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/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Billing - Subscription management and usage tracking.
|
|
3
|
+
*
|
|
4
|
+
* Embedded replacement for services/billing-service.
|
|
5
|
+
* All business logic is preserved verbatim from:
|
|
6
|
+
* - services/billing-service/src/routes/subscriptions.ts
|
|
7
|
+
* - services/billing-service/src/routes/usage.ts
|
|
8
|
+
*
|
|
9
|
+
* HTTP handlers, routes, and per-service SQLite are stripped.
|
|
10
|
+
* State is read/written through the unified database via ../state/billing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createSubscription as stateCreateSubscription,
|
|
15
|
+
getSubscription as stateGetSubscription,
|
|
16
|
+
updateSubscription as stateUpdateSubscription,
|
|
17
|
+
recordUsage as stateRecordUsage,
|
|
18
|
+
getUsage as stateGetUsage,
|
|
19
|
+
getUsageSummary as stateGetUsageSummary,
|
|
20
|
+
type SubscriptionRecord,
|
|
21
|
+
type UsageRecord,
|
|
22
|
+
type UsageSummary as StateUsageSummary,
|
|
23
|
+
} from '../state/billing';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Plan catalog (preserved verbatim from billing-service/src/routes/subscriptions.ts)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
interface PlanDetails {
|
|
30
|
+
name: string;
|
|
31
|
+
stripe_price_id: string;
|
|
32
|
+
amount_cents: number;
|
|
33
|
+
currency: string;
|
|
34
|
+
interval: 'month' | 'year';
|
|
35
|
+
seats_included: number;
|
|
36
|
+
features: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const PLAN_CATALOG: Record<string, PlanDetails> = {
|
|
40
|
+
free: {
|
|
41
|
+
name: 'Nimbus Free',
|
|
42
|
+
stripe_price_id: 'price_demo_free_monthly',
|
|
43
|
+
amount_cents: 0,
|
|
44
|
+
currency: 'usd',
|
|
45
|
+
interval: 'month',
|
|
46
|
+
seats_included: 5,
|
|
47
|
+
features: [
|
|
48
|
+
'5 team members',
|
|
49
|
+
'100K tokens/month',
|
|
50
|
+
'Community support',
|
|
51
|
+
'Basic Terraform generation',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
pro: {
|
|
55
|
+
name: 'Nimbus Pro',
|
|
56
|
+
stripe_price_id: 'price_demo_pro_monthly',
|
|
57
|
+
amount_cents: 4900,
|
|
58
|
+
currency: 'usd',
|
|
59
|
+
interval: 'month',
|
|
60
|
+
seats_included: 25,
|
|
61
|
+
features: [
|
|
62
|
+
'25 team members',
|
|
63
|
+
'5M tokens/month',
|
|
64
|
+
'Priority support',
|
|
65
|
+
'Multi-cloud generation',
|
|
66
|
+
'Drift detection',
|
|
67
|
+
'Cost optimization',
|
|
68
|
+
'Helm & K8s generation',
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
enterprise: {
|
|
72
|
+
name: 'Nimbus Enterprise',
|
|
73
|
+
stripe_price_id: 'price_demo_enterprise_monthly',
|
|
74
|
+
amount_cents: 19900,
|
|
75
|
+
currency: 'usd',
|
|
76
|
+
interval: 'month',
|
|
77
|
+
seats_included: 100,
|
|
78
|
+
features: [
|
|
79
|
+
'Unlimited team members',
|
|
80
|
+
'Unlimited tokens',
|
|
81
|
+
'Dedicated support & SLA',
|
|
82
|
+
'SSO / SAML integration',
|
|
83
|
+
'Audit log export',
|
|
84
|
+
'Custom policy engine',
|
|
85
|
+
'Private deployment option',
|
|
86
|
+
'Advanced RBAC',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const VALID_PLANS = ['free', 'pro', 'enterprise'] as const;
|
|
92
|
+
type ValidPlan = (typeof VALID_PLANS)[number];
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Plan quota definitions (preserved verbatim from billing-service/src/routes/usage.ts)
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
interface PlanQuota {
|
|
99
|
+
tokensPerMonth: number;
|
|
100
|
+
operationsPerMonth: number;
|
|
101
|
+
costCapUsd: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const PLAN_QUOTAS: Record<string, PlanQuota> = {
|
|
105
|
+
free: {
|
|
106
|
+
tokensPerMonth: 100_000,
|
|
107
|
+
operationsPerMonth: 500,
|
|
108
|
+
costCapUsd: 0,
|
|
109
|
+
},
|
|
110
|
+
pro: {
|
|
111
|
+
tokensPerMonth: 5_000_000,
|
|
112
|
+
operationsPerMonth: 25_000,
|
|
113
|
+
costCapUsd: 100,
|
|
114
|
+
},
|
|
115
|
+
enterprise: {
|
|
116
|
+
tokensPerMonth: -1, // unlimited
|
|
117
|
+
operationsPerMonth: -1,
|
|
118
|
+
costCapUsd: -1,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Response type definitions (mirrors @nimbus/shared-types shapes)
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/** Alias for the billing-specific plan type, re-exported from teams.ts */
|
|
127
|
+
type TeamPlan = ValidPlan;
|
|
128
|
+
|
|
129
|
+
export interface BillingStatus {
|
|
130
|
+
plan: TeamPlan;
|
|
131
|
+
status: 'active' | 'canceled' | 'past_due' | 'trialing';
|
|
132
|
+
currentPeriodStart: string;
|
|
133
|
+
currentPeriodEnd: string;
|
|
134
|
+
cancelAtPeriodEnd: boolean;
|
|
135
|
+
seats: {
|
|
136
|
+
used: number;
|
|
137
|
+
total: number;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface StripeSubscriptionResponse {
|
|
142
|
+
billing: BillingStatus;
|
|
143
|
+
stripe: {
|
|
144
|
+
id: string;
|
|
145
|
+
object: 'subscription';
|
|
146
|
+
customer: string;
|
|
147
|
+
status: string;
|
|
148
|
+
current_period_start: number;
|
|
149
|
+
current_period_end: number;
|
|
150
|
+
created: number;
|
|
151
|
+
cancel_at_period_end: boolean;
|
|
152
|
+
canceled_at: number | null;
|
|
153
|
+
plan: {
|
|
154
|
+
id: string;
|
|
155
|
+
object: 'plan';
|
|
156
|
+
product: string;
|
|
157
|
+
nickname: string;
|
|
158
|
+
amount: number;
|
|
159
|
+
currency: string;
|
|
160
|
+
interval: string;
|
|
161
|
+
interval_count: number;
|
|
162
|
+
active: boolean;
|
|
163
|
+
};
|
|
164
|
+
items: {
|
|
165
|
+
object: 'list';
|
|
166
|
+
data: Array<{
|
|
167
|
+
id: string;
|
|
168
|
+
object: 'subscription_item';
|
|
169
|
+
price: {
|
|
170
|
+
id: string;
|
|
171
|
+
object: 'price';
|
|
172
|
+
unit_amount: number;
|
|
173
|
+
currency: string;
|
|
174
|
+
recurring: { interval: string; interval_count: number };
|
|
175
|
+
product: string;
|
|
176
|
+
};
|
|
177
|
+
quantity: number;
|
|
178
|
+
}>;
|
|
179
|
+
total_count: number;
|
|
180
|
+
};
|
|
181
|
+
latest_invoice: {
|
|
182
|
+
id: string;
|
|
183
|
+
object: 'invoice';
|
|
184
|
+
number: string;
|
|
185
|
+
status: 'draft' | 'open' | 'paid' | 'void';
|
|
186
|
+
amount_due: number;
|
|
187
|
+
amount_paid: number;
|
|
188
|
+
currency: string;
|
|
189
|
+
customer_email: string;
|
|
190
|
+
period_start: number;
|
|
191
|
+
period_end: number;
|
|
192
|
+
subtotal: number;
|
|
193
|
+
tax: number;
|
|
194
|
+
total: number;
|
|
195
|
+
lines: {
|
|
196
|
+
object: 'list';
|
|
197
|
+
data: Array<{
|
|
198
|
+
id: string;
|
|
199
|
+
object: 'line_item';
|
|
200
|
+
description: string;
|
|
201
|
+
amount: number;
|
|
202
|
+
currency: string;
|
|
203
|
+
quantity: number;
|
|
204
|
+
period: { start: number; end: number };
|
|
205
|
+
}>;
|
|
206
|
+
total_count: number;
|
|
207
|
+
};
|
|
208
|
+
payment_intent: {
|
|
209
|
+
id: string;
|
|
210
|
+
object: 'payment_intent';
|
|
211
|
+
status: 'succeeded' | 'requires_payment_method';
|
|
212
|
+
amount: number;
|
|
213
|
+
currency: string;
|
|
214
|
+
};
|
|
215
|
+
hosted_invoice_url: string;
|
|
216
|
+
invoice_pdf: string;
|
|
217
|
+
created: number;
|
|
218
|
+
};
|
|
219
|
+
metadata: { team_id: string; plan_name: string; provisioned_by: string };
|
|
220
|
+
default_payment_method: {
|
|
221
|
+
id: string;
|
|
222
|
+
object: 'payment_method';
|
|
223
|
+
type: 'card';
|
|
224
|
+
card: { brand: string; last4: string; exp_month: number; exp_year: number; funding: string };
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface StripeCancellationResponse {
|
|
230
|
+
billing: BillingStatus;
|
|
231
|
+
cancellation: {
|
|
232
|
+
id: string;
|
|
233
|
+
object: 'subscription';
|
|
234
|
+
status: 'canceled';
|
|
235
|
+
canceled_at: number;
|
|
236
|
+
cancel_at_period_end: boolean;
|
|
237
|
+
current_period_end: number;
|
|
238
|
+
ended_at: number | null;
|
|
239
|
+
cancellation_details: {
|
|
240
|
+
comment: string | null;
|
|
241
|
+
feedback: string | null;
|
|
242
|
+
reason: 'cancellation_requested';
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface SubscribeRequest {
|
|
248
|
+
teamId: string;
|
|
249
|
+
plan: string;
|
|
250
|
+
paymentMethodId?: string;
|
|
251
|
+
seats?: number;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface RecordUsageRequest {
|
|
255
|
+
teamId: string;
|
|
256
|
+
userId?: string;
|
|
257
|
+
operationType: string;
|
|
258
|
+
tokensUsed: number;
|
|
259
|
+
costUsd: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface UsageRecordConfirmation {
|
|
263
|
+
recorded: true;
|
|
264
|
+
id: string;
|
|
265
|
+
timestamp: string;
|
|
266
|
+
teamId: string;
|
|
267
|
+
operationType: string;
|
|
268
|
+
tokensUsed: number;
|
|
269
|
+
costUsd: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface EnhancedUsageSummary {
|
|
273
|
+
period: { start: string; end: string };
|
|
274
|
+
totals: { operations: number; tokensUsed: number; costUsd: number };
|
|
275
|
+
byOperationType: Record<string, { count: number; tokensUsed: number; costUsd: number }>;
|
|
276
|
+
byUser?: Record<string, { count: number; tokensUsed: number; costUsd: number }>;
|
|
277
|
+
dailyBreakdown: Array<{ date: string; operations: number; tokensUsed: number; costUsd: number }>;
|
|
278
|
+
quota: {
|
|
279
|
+
plan: string;
|
|
280
|
+
tokens: {
|
|
281
|
+
used: number;
|
|
282
|
+
limit: number;
|
|
283
|
+
remaining: number;
|
|
284
|
+
percentUsed: number;
|
|
285
|
+
unlimited: boolean;
|
|
286
|
+
};
|
|
287
|
+
operations: {
|
|
288
|
+
used: number;
|
|
289
|
+
limit: number;
|
|
290
|
+
remaining: number;
|
|
291
|
+
percentUsed: number;
|
|
292
|
+
unlimited: boolean;
|
|
293
|
+
};
|
|
294
|
+
cost: {
|
|
295
|
+
accrued: number;
|
|
296
|
+
cap: number;
|
|
297
|
+
remaining: number;
|
|
298
|
+
percentUsed: number;
|
|
299
|
+
unlimited: boolean;
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
rateLimit: {
|
|
303
|
+
requestsPerMinute: number;
|
|
304
|
+
tokensPerMinute: number;
|
|
305
|
+
currentMinuteRequests: number;
|
|
306
|
+
currentMinuteTokens: number;
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Private helpers
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Generate a random alphanumeric demo ID of the given length.
|
|
316
|
+
* Preserved verbatim from billing-service/src/db/adapter.ts.
|
|
317
|
+
*/
|
|
318
|
+
function generateDemoId(length: number): string {
|
|
319
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
320
|
+
let result = '';
|
|
321
|
+
for (let i = 0; i < length; i++) {
|
|
322
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Build a realistic Stripe-format subscription object for demo presentations.
|
|
329
|
+
* Preserved verbatim from billing-service/src/routes/subscriptions.ts.
|
|
330
|
+
*/
|
|
331
|
+
function buildStripeSubscription(
|
|
332
|
+
teamId: string,
|
|
333
|
+
plan: string,
|
|
334
|
+
stripeSubId: string,
|
|
335
|
+
stripeCustomerId: string,
|
|
336
|
+
periodStart: Date,
|
|
337
|
+
periodEnd: Date,
|
|
338
|
+
seats: number,
|
|
339
|
+
canceled: boolean = false,
|
|
340
|
+
canceledAt: Date | null = null
|
|
341
|
+
): StripeSubscriptionResponse['stripe'] {
|
|
342
|
+
const planDetails = PLAN_CATALOG[plan] || PLAN_CATALOG.pro;
|
|
343
|
+
const createdTimestamp = Math.floor((periodStart.getTime() - 5000) / 1000);
|
|
344
|
+
const periodStartUnix = Math.floor(periodStart.getTime() / 1000);
|
|
345
|
+
const periodEndUnix = Math.floor(periodEnd.getTime() / 1000);
|
|
346
|
+
const invoiceId = `in_demo_${generateDemoId(24)}`;
|
|
347
|
+
const invoiceNumber = `NIM-${periodStart.getFullYear()}${String(periodStart.getMonth() + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 9999) + 1).padStart(4, '0')}`;
|
|
348
|
+
const seatAmount = planDetails.amount_cents * seats;
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
id: stripeSubId,
|
|
352
|
+
object: 'subscription',
|
|
353
|
+
customer: stripeCustomerId,
|
|
354
|
+
status: canceled ? 'canceled' : 'active',
|
|
355
|
+
current_period_start: periodStartUnix,
|
|
356
|
+
current_period_end: periodEndUnix,
|
|
357
|
+
created: createdTimestamp,
|
|
358
|
+
cancel_at_period_end: canceled,
|
|
359
|
+
canceled_at: canceledAt ? Math.floor(canceledAt.getTime() / 1000) : null,
|
|
360
|
+
plan: {
|
|
361
|
+
id: planDetails.stripe_price_id,
|
|
362
|
+
object: 'plan',
|
|
363
|
+
product: `prod_demo_nimbus_${plan}`,
|
|
364
|
+
nickname: planDetails.name,
|
|
365
|
+
amount: planDetails.amount_cents,
|
|
366
|
+
currency: planDetails.currency,
|
|
367
|
+
interval: planDetails.interval,
|
|
368
|
+
interval_count: 1,
|
|
369
|
+
active: true,
|
|
370
|
+
},
|
|
371
|
+
items: {
|
|
372
|
+
object: 'list',
|
|
373
|
+
data: [
|
|
374
|
+
{
|
|
375
|
+
id: `si_demo_${generateDemoId(14)}`,
|
|
376
|
+
object: 'subscription_item',
|
|
377
|
+
price: {
|
|
378
|
+
id: planDetails.stripe_price_id,
|
|
379
|
+
object: 'price',
|
|
380
|
+
unit_amount: planDetails.amount_cents,
|
|
381
|
+
currency: planDetails.currency,
|
|
382
|
+
recurring: { interval: planDetails.interval, interval_count: 1 },
|
|
383
|
+
product: `prod_demo_nimbus_${plan}`,
|
|
384
|
+
},
|
|
385
|
+
quantity: seats,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
total_count: 1,
|
|
389
|
+
},
|
|
390
|
+
latest_invoice: {
|
|
391
|
+
id: invoiceId,
|
|
392
|
+
object: 'invoice',
|
|
393
|
+
number: invoiceNumber,
|
|
394
|
+
status: 'paid',
|
|
395
|
+
amount_due: seatAmount,
|
|
396
|
+
amount_paid: seatAmount,
|
|
397
|
+
currency: planDetails.currency,
|
|
398
|
+
customer_email: `billing+${teamId}@nimbus.dev`,
|
|
399
|
+
period_start: periodStartUnix,
|
|
400
|
+
period_end: periodEndUnix,
|
|
401
|
+
subtotal: seatAmount,
|
|
402
|
+
tax: 0,
|
|
403
|
+
total: seatAmount,
|
|
404
|
+
lines: {
|
|
405
|
+
object: 'list',
|
|
406
|
+
data: [
|
|
407
|
+
{
|
|
408
|
+
id: `il_demo_${generateDemoId(14)}`,
|
|
409
|
+
object: 'line_item',
|
|
410
|
+
description: `${planDetails.name} (${seats} seat${seats !== 1 ? 's' : ''} x $${(planDetails.amount_cents / 100).toFixed(2)}/mo)`,
|
|
411
|
+
amount: seatAmount,
|
|
412
|
+
currency: planDetails.currency,
|
|
413
|
+
quantity: seats,
|
|
414
|
+
period: { start: periodStartUnix, end: periodEndUnix },
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
total_count: 1,
|
|
418
|
+
},
|
|
419
|
+
payment_intent: {
|
|
420
|
+
id: `pi_demo_${generateDemoId(24)}`,
|
|
421
|
+
object: 'payment_intent',
|
|
422
|
+
status: 'succeeded',
|
|
423
|
+
amount: seatAmount,
|
|
424
|
+
currency: planDetails.currency,
|
|
425
|
+
},
|
|
426
|
+
hosted_invoice_url: `https://invoice.stripe.com/i/demo/${invoiceId}`,
|
|
427
|
+
invoice_pdf: `https://pay.stripe.com/invoice/${invoiceId}/pdf`,
|
|
428
|
+
created: createdTimestamp,
|
|
429
|
+
},
|
|
430
|
+
metadata: {
|
|
431
|
+
team_id: teamId,
|
|
432
|
+
plan_name: planDetails.name,
|
|
433
|
+
provisioned_by: 'nimbus-billing-service',
|
|
434
|
+
},
|
|
435
|
+
default_payment_method: {
|
|
436
|
+
id: `pm_demo_${generateDemoId(14)}`,
|
|
437
|
+
object: 'payment_method',
|
|
438
|
+
type: 'card',
|
|
439
|
+
card: {
|
|
440
|
+
brand: 'visa',
|
|
441
|
+
last4: '4242',
|
|
442
|
+
exp_month: 12,
|
|
443
|
+
exp_year: new Date().getFullYear() + 2,
|
|
444
|
+
funding: 'credit',
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Build quota status relative to the team's plan.
|
|
452
|
+
* Preserved verbatim from billing-service/src/routes/usage.ts.
|
|
453
|
+
*/
|
|
454
|
+
function buildQuota(
|
|
455
|
+
plan: string,
|
|
456
|
+
totalTokens: number,
|
|
457
|
+
totalOperations: number,
|
|
458
|
+
totalCost: number
|
|
459
|
+
): EnhancedUsageSummary['quota'] {
|
|
460
|
+
const quota = PLAN_QUOTAS[plan] || PLAN_QUOTAS.free;
|
|
461
|
+
const unlimited = plan === 'enterprise';
|
|
462
|
+
|
|
463
|
+
const tokensLimit = quota.tokensPerMonth;
|
|
464
|
+
const opsLimit = quota.operationsPerMonth;
|
|
465
|
+
const costCap = quota.costCapUsd;
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
plan,
|
|
469
|
+
tokens: {
|
|
470
|
+
used: totalTokens,
|
|
471
|
+
limit: unlimited ? -1 : tokensLimit,
|
|
472
|
+
remaining: unlimited ? -1 : Math.max(0, tokensLimit - totalTokens),
|
|
473
|
+
percentUsed: unlimited
|
|
474
|
+
? 0
|
|
475
|
+
: tokensLimit > 0
|
|
476
|
+
? Math.min(100, Math.round((totalTokens / tokensLimit) * 100 * 100) / 100)
|
|
477
|
+
: 0,
|
|
478
|
+
unlimited,
|
|
479
|
+
},
|
|
480
|
+
operations: {
|
|
481
|
+
used: totalOperations,
|
|
482
|
+
limit: unlimited ? -1 : opsLimit,
|
|
483
|
+
remaining: unlimited ? -1 : Math.max(0, opsLimit - totalOperations),
|
|
484
|
+
percentUsed: unlimited
|
|
485
|
+
? 0
|
|
486
|
+
: opsLimit > 0
|
|
487
|
+
? Math.min(100, Math.round((totalOperations / opsLimit) * 100 * 100) / 100)
|
|
488
|
+
: 0,
|
|
489
|
+
unlimited,
|
|
490
|
+
},
|
|
491
|
+
cost: {
|
|
492
|
+
accrued: Math.round(totalCost * 100) / 100,
|
|
493
|
+
cap: unlimited ? -1 : costCap,
|
|
494
|
+
remaining: unlimited ? -1 : Math.max(0, Math.round((costCap - totalCost) * 100) / 100),
|
|
495
|
+
percentUsed: unlimited
|
|
496
|
+
? 0
|
|
497
|
+
: costCap > 0
|
|
498
|
+
? Math.min(100, Math.round((totalCost / costCap) * 100 * 100) / 100)
|
|
499
|
+
: 0,
|
|
500
|
+
unlimited,
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Build rate limit metadata for the current billing window.
|
|
507
|
+
* Preserved verbatim from billing-service/src/routes/usage.ts.
|
|
508
|
+
*/
|
|
509
|
+
function buildRateLimit(plan: string): EnhancedUsageSummary['rateLimit'] {
|
|
510
|
+
const limits: Record<string, { rpm: number; tpm: number }> = {
|
|
511
|
+
free: { rpm: 30, tpm: 10_000 },
|
|
512
|
+
pro: { rpm: 120, tpm: 100_000 },
|
|
513
|
+
enterprise: { rpm: 600, tpm: 1_000_000 },
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const planLimits = limits[plan] || limits.free;
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
requestsPerMinute: planLimits.rpm,
|
|
520
|
+
tokensPerMinute: planLimits.tpm,
|
|
521
|
+
// Simulated current-minute counters (low values for demo)
|
|
522
|
+
currentMinuteRequests: Math.floor(Math.random() * 5),
|
|
523
|
+
currentMinuteTokens: Math.floor(Math.random() * 2000),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
// Public API - Subscriptions
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Get the current billing status for a team.
|
|
533
|
+
*
|
|
534
|
+
* Returns free-plan defaults when no subscription record exists.
|
|
535
|
+
*/
|
|
536
|
+
export async function getBillingStatus(teamId: string): Promise<BillingStatus> {
|
|
537
|
+
const subscription: SubscriptionRecord | null = stateGetSubscription(teamId);
|
|
538
|
+
|
|
539
|
+
if (!subscription) {
|
|
540
|
+
return {
|
|
541
|
+
plan: 'free',
|
|
542
|
+
status: 'active',
|
|
543
|
+
currentPeriodStart: new Date().toISOString(),
|
|
544
|
+
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
545
|
+
cancelAtPeriodEnd: false,
|
|
546
|
+
seats: {
|
|
547
|
+
used: 1,
|
|
548
|
+
total: 5,
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// The unified billing schema (src/state/billing.ts) stores status directly
|
|
554
|
+
// and does not have cancel_at_period_end or seats_used/seats_total columns.
|
|
555
|
+
// We derive cancelAtPeriodEnd from status === 'canceled' for backward
|
|
556
|
+
// compatibility with the original service response shape.
|
|
557
|
+
return {
|
|
558
|
+
plan: subscription.plan as TeamPlan,
|
|
559
|
+
status: subscription.status as BillingStatus['status'],
|
|
560
|
+
currentPeriodStart: subscription.currentPeriodStart || new Date().toISOString(),
|
|
561
|
+
currentPeriodEnd:
|
|
562
|
+
subscription.currentPeriodEnd ||
|
|
563
|
+
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
564
|
+
cancelAtPeriodEnd: subscription.status === 'canceled',
|
|
565
|
+
seats: {
|
|
566
|
+
used: 1, // seat tracking not available in the unified schema; caller can augment
|
|
567
|
+
total: PLAN_CATALOG[subscription.plan]?.seats_included ?? 5,
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Subscribe a team to a plan.
|
|
574
|
+
*
|
|
575
|
+
* Creates or updates the subscription record in the unified database and
|
|
576
|
+
* returns a rich Stripe-like subscription object for demo presentations.
|
|
577
|
+
*/
|
|
578
|
+
export async function subscribe(request: SubscribeRequest): Promise<StripeSubscriptionResponse> {
|
|
579
|
+
const { teamId, plan, seats } = request;
|
|
580
|
+
|
|
581
|
+
if (!teamId) {
|
|
582
|
+
throw new Error('Team ID is required');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (!VALID_PLANS.includes(plan as ValidPlan)) {
|
|
586
|
+
throw new Error(`Invalid plan: ${plan}. Must be one of: ${VALID_PLANS.join(', ')}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const stripeSubscriptionId = `sub_demo_${generateDemoId(24)}`;
|
|
590
|
+
const periodStart = new Date();
|
|
591
|
+
const periodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
592
|
+
const seatsTotal = seats || (plan === 'enterprise' ? 100 : plan === 'pro' ? 25 : 5);
|
|
593
|
+
const stripeCustomerId = `cus_demo_${generateDemoId(14)}`;
|
|
594
|
+
|
|
595
|
+
const existing = stateGetSubscription(teamId);
|
|
596
|
+
|
|
597
|
+
if (existing) {
|
|
598
|
+
stateUpdateSubscription(teamId, {
|
|
599
|
+
plan,
|
|
600
|
+
status: 'active',
|
|
601
|
+
currentPeriodStart: periodStart.toISOString(),
|
|
602
|
+
currentPeriodEnd: periodEnd.toISOString(),
|
|
603
|
+
});
|
|
604
|
+
} else {
|
|
605
|
+
stateCreateSubscription(
|
|
606
|
+
crypto.randomUUID(),
|
|
607
|
+
teamId,
|
|
608
|
+
plan,
|
|
609
|
+
'active',
|
|
610
|
+
periodStart.toISOString(),
|
|
611
|
+
periodEnd.toISOString()
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const billingStatus = await getBillingStatus(teamId);
|
|
616
|
+
|
|
617
|
+
const stripeObject = buildStripeSubscription(
|
|
618
|
+
teamId,
|
|
619
|
+
plan,
|
|
620
|
+
stripeSubscriptionId,
|
|
621
|
+
stripeCustomerId,
|
|
622
|
+
periodStart,
|
|
623
|
+
periodEnd,
|
|
624
|
+
seatsTotal
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
billing: billingStatus,
|
|
629
|
+
stripe: stripeObject,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Cancel a team's active subscription.
|
|
635
|
+
*
|
|
636
|
+
* Marks the subscription as canceled in the unified database and returns a
|
|
637
|
+
* Stripe-like cancellation confirmation object.
|
|
638
|
+
*/
|
|
639
|
+
export async function cancelSubscription(teamId: string): Promise<StripeCancellationResponse> {
|
|
640
|
+
if (!teamId) {
|
|
641
|
+
throw new Error('Team ID is required');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const subscription = stateGetSubscription(teamId);
|
|
645
|
+
if (!subscription) {
|
|
646
|
+
throw new Error('No subscription found');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (subscription.plan === 'free') {
|
|
650
|
+
throw new Error('Cannot cancel free plan');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const canceledAt = new Date();
|
|
654
|
+
|
|
655
|
+
stateUpdateSubscription(teamId, { status: 'canceled' });
|
|
656
|
+
|
|
657
|
+
const billingStatus = await getBillingStatus(teamId);
|
|
658
|
+
|
|
659
|
+
const periodEnd = subscription.currentPeriodEnd
|
|
660
|
+
? new Date(subscription.currentPeriodEnd)
|
|
661
|
+
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
billing: billingStatus,
|
|
665
|
+
cancellation: {
|
|
666
|
+
id: `sub_demo_${generateDemoId(24)}`,
|
|
667
|
+
object: 'subscription',
|
|
668
|
+
status: 'canceled',
|
|
669
|
+
canceled_at: Math.floor(canceledAt.getTime() / 1000),
|
|
670
|
+
cancel_at_period_end: true,
|
|
671
|
+
current_period_end: Math.floor(periodEnd.getTime() / 1000),
|
|
672
|
+
ended_at: null,
|
|
673
|
+
cancellation_details: {
|
|
674
|
+
comment: null,
|
|
675
|
+
feedback: null,
|
|
676
|
+
reason: 'cancellation_requested',
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
// Public API - Usage tracking
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Record a usage event for a team.
|
|
688
|
+
*
|
|
689
|
+
* Validates numeric inputs and returns a confirmation receipt with the
|
|
690
|
+
* generated record ID.
|
|
691
|
+
*/
|
|
692
|
+
export async function recordUsage(request: RecordUsageRequest): Promise<UsageRecordConfirmation> {
|
|
693
|
+
const { teamId, userId, operationType, tokensUsed, costUsd } = request;
|
|
694
|
+
|
|
695
|
+
if (!teamId || !operationType) {
|
|
696
|
+
throw new Error('Team ID and operation type are required');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (!Number.isFinite(tokensUsed) || tokensUsed < 0) {
|
|
700
|
+
throw new Error('tokensUsed must be a non-negative number');
|
|
701
|
+
}
|
|
702
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
703
|
+
throw new Error('costUsd must be a non-negative number');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const id = crypto.randomUUID();
|
|
707
|
+
|
|
708
|
+
// stateRecordUsage(id, type, quantity, unit, costUsd, teamId, userId, metadata?)
|
|
709
|
+
stateRecordUsage(id, operationType, tokensUsed, 'tokens', costUsd, teamId, userId);
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
recorded: true,
|
|
713
|
+
id,
|
|
714
|
+
timestamp: new Date().toISOString(),
|
|
715
|
+
teamId,
|
|
716
|
+
operationType,
|
|
717
|
+
tokensUsed,
|
|
718
|
+
costUsd,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Get a usage summary for a team over a given period.
|
|
724
|
+
*
|
|
725
|
+
* Returns an enhanced summary with daily breakdown, quota status relative to
|
|
726
|
+
* the team's current plan, and rate limit metadata.
|
|
727
|
+
*/
|
|
728
|
+
export async function getUsage(
|
|
729
|
+
teamId: string,
|
|
730
|
+
period: 'day' | 'week' | 'month' = 'month'
|
|
731
|
+
): Promise<EnhancedUsageSummary> {
|
|
732
|
+
const now = new Date();
|
|
733
|
+
let since: Date;
|
|
734
|
+
|
|
735
|
+
switch (period) {
|
|
736
|
+
case 'day':
|
|
737
|
+
since = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
738
|
+
break;
|
|
739
|
+
case 'week':
|
|
740
|
+
since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
741
|
+
break;
|
|
742
|
+
case 'month':
|
|
743
|
+
default:
|
|
744
|
+
since = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// The unified billing state module provides getUsageSummary() aggregated by
|
|
749
|
+
// type, and getUsage() for raw records. We use getUsageSummary() for
|
|
750
|
+
// byOperationType totals and getUsage() for per-user and daily breakdowns.
|
|
751
|
+
const summaryRows: StateUsageSummary[] = stateGetUsageSummary(teamId, since, now);
|
|
752
|
+
const rawRecords: UsageRecord[] = stateGetUsage(teamId, since, now, 10000, 0);
|
|
753
|
+
|
|
754
|
+
// Aggregate totals and byOperationType from summary rows
|
|
755
|
+
let totalOperations = 0;
|
|
756
|
+
let totalTokens = 0;
|
|
757
|
+
let totalCost = 0;
|
|
758
|
+
const byOperationType: Record<string, { count: number; tokensUsed: number; costUsd: number }> =
|
|
759
|
+
{};
|
|
760
|
+
|
|
761
|
+
for (const row of summaryRows) {
|
|
762
|
+
totalOperations += row.count;
|
|
763
|
+
totalTokens += row.totalQuantity;
|
|
764
|
+
totalCost += row.totalCost;
|
|
765
|
+
|
|
766
|
+
byOperationType[row.type] = {
|
|
767
|
+
count: row.count,
|
|
768
|
+
tokensUsed: row.totalQuantity,
|
|
769
|
+
costUsd: row.totalCost,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Aggregate byUser from raw records
|
|
774
|
+
const byUser: Record<string, { count: number; tokensUsed: number; costUsd: number }> = {};
|
|
775
|
+
for (const rec of rawRecords) {
|
|
776
|
+
if (rec.userId) {
|
|
777
|
+
const existing = byUser[rec.userId] ?? { count: 0, tokensUsed: 0, costUsd: 0 };
|
|
778
|
+
byUser[rec.userId] = {
|
|
779
|
+
count: existing.count + 1,
|
|
780
|
+
tokensUsed: existing.tokensUsed + rec.quantity,
|
|
781
|
+
costUsd: existing.costUsd + (rec.costUsd ?? 0),
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Build daily breakdown from raw records (group by calendar date)
|
|
787
|
+
const dailyMap: Record<string, { operations: number; tokensUsed: number; costUsd: number }> = {};
|
|
788
|
+
for (const rec of rawRecords) {
|
|
789
|
+
const date = rec.createdAt.slice(0, 10); // "YYYY-MM-DD"
|
|
790
|
+
const existing = dailyMap[date] ?? { operations: 0, tokensUsed: 0, costUsd: 0 };
|
|
791
|
+
dailyMap[date] = {
|
|
792
|
+
operations: existing.operations + 1,
|
|
793
|
+
tokensUsed: existing.tokensUsed + rec.quantity,
|
|
794
|
+
costUsd: existing.costUsd + (rec.costUsd ?? 0),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const dailyBreakdown = Object.entries(dailyMap)
|
|
799
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
800
|
+
.map(([date, agg]) => ({ date, ...agg }));
|
|
801
|
+
|
|
802
|
+
// Resolve the team's current plan for quota calculation
|
|
803
|
+
const subscription = stateGetSubscription(teamId);
|
|
804
|
+
const plan = subscription?.plan || 'free';
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
period: {
|
|
808
|
+
start: since.toISOString(),
|
|
809
|
+
end: now.toISOString(),
|
|
810
|
+
},
|
|
811
|
+
totals: {
|
|
812
|
+
operations: totalOperations,
|
|
813
|
+
tokensUsed: totalTokens,
|
|
814
|
+
costUsd: Math.round(totalCost * 100) / 100,
|
|
815
|
+
},
|
|
816
|
+
byOperationType,
|
|
817
|
+
byUser: Object.keys(byUser).length > 0 ? byUser : undefined,
|
|
818
|
+
dailyBreakdown,
|
|
819
|
+
quota: buildQuota(plan, totalTokens, totalOperations, totalCost),
|
|
820
|
+
rateLimit: buildRateLimit(plan),
|
|
821
|
+
};
|
|
822
|
+
}
|