tapimo 0.2.0 → 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/README.md +31 -3
- package/dist/ApiKey.d.ts +1 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +5 -2
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +28 -1
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +34 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +7167 -4507
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +8 -0
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +11 -2
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +14 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +128 -118
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +7 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +3 -0
- package/dist/admin/apps/scopes.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +13 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +151 -0
- package/dist/analytics/Analytics.d.ts.map +1 -0
- package/dist/analytics/Analytics.js +212 -0
- package/dist/analytics/Analytics.js.map +1 -0
- package/dist/analytics/tables/requestEvents.d.ts +215 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
- package/dist/analytics/tables/requestEvents.js +264 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -0
- package/dist/apps/data/App.d.ts +1024 -624
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +36 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +765 -0
- package/dist/apps/data/routes/funding.d.ts.map +1 -0
- package/dist/apps/data/routes/funding.js +383 -0
- package/dist/apps/data/routes/funding.js.map +1 -0
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +2 -8
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +29 -36
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +29 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/transfers.d.ts +56 -56
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +45 -45
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +8 -8
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/docs/App.d.ts +4 -2
- package/dist/apps/docs/App.d.ts.map +1 -1
- package/dist/apps/docs/App.js +27 -95
- package/dist/apps/docs/App.js.map +1 -1
- package/dist/apps/docs/reference.d.ts +21 -0
- package/dist/apps/docs/reference.d.ts.map +1 -0
- package/dist/apps/docs/reference.js +112 -0
- package/dist/apps/docs/reference.js.map +1 -0
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/mcp.d.ts +76 -0
- package/dist/apps/mcp.d.ts.map +1 -0
- package/dist/apps/mcp.js +188 -0
- package/dist/apps/mcp.js.map +1 -0
- package/dist/apps/platform/App.d.ts +3005 -728
- package/dist/apps/platform/App.d.ts.map +1 -1
- package/dist/apps/platform/App.js +13 -3
- package/dist/apps/platform/App.js.map +1 -1
- package/dist/apps/platform/Billing.d.ts +279 -0
- package/dist/apps/platform/Billing.d.ts.map +1 -0
- package/dist/apps/platform/Billing.js +477 -0
- package/dist/apps/platform/Billing.js.map +1 -0
- package/dist/apps/platform/index.d.ts +1 -0
- package/dist/apps/platform/index.d.ts.map +1 -1
- package/dist/apps/platform/index.js +1 -0
- package/dist/apps/platform/index.js.map +1 -1
- package/dist/apps/platform/routes/api-keys.d.ts +341 -49
- package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/platform/routes/api-keys.js +115 -13
- package/dist/apps/platform/routes/api-keys.js.map +1 -1
- package/dist/apps/platform/routes/billing.d.ts +1520 -0
- package/dist/apps/platform/routes/billing.d.ts.map +1 -0
- package/dist/apps/platform/routes/billing.js +795 -0
- package/dist/apps/platform/routes/billing.js.map +1 -0
- package/dist/apps/platform/routes/invitations.d.ts +201 -83
- package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
- package/dist/apps/platform/routes/invitations.js +58 -0
- package/dist/apps/platform/routes/invitations.js.map +1 -1
- package/dist/apps/platform/routes/me.d.ts +17 -17
- package/dist/apps/platform/routes/orgs.d.ts +50 -36
- package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
- package/dist/apps/platform/routes/orgs.js +14 -3
- package/dist/apps/platform/routes/orgs.js.map +1 -1
- package/dist/apps/platform/routes/projects.d.ts +97 -97
- package/dist/apps/platform/routes/scopes.d.ts +107 -0
- package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
- package/dist/apps/platform/routes/scopes.js +66 -0
- package/dist/apps/platform/routes/scopes.js.map +1 -0
- package/dist/apps/platform/routes/usage.d.ts +588 -0
- package/dist/apps/platform/routes/usage.d.ts.map +1 -0
- package/dist/apps/platform/routes/usage.js +391 -0
- package/dist/apps/platform/routes/usage.js.map +1 -0
- package/dist/apps/relay/App.d.ts +4 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +118 -10
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +2 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/cli/Cli.d.ts +8 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +11 -3
- package/dist/cli/Cli.js.map +1 -1
- package/dist/cli/commands/admin.d.ts +4 -0
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +80 -1
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/db/Db.d.ts +7 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +126 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +79 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +88 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/billingSettings.d.ts +55 -0
- package/dist/db/tables/billingSettings.d.ts.map +1 -0
- package/dist/db/tables/billingSettings.js +70 -0
- package/dist/db/tables/billingSettings.js.map +1 -0
- package/dist/db/tables/invitations.js +2 -2
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +6 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +10 -4
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.js +2 -2
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/requestUsage.d.ts +121 -0
- package/dist/db/tables/requestUsage.d.ts.map +1 -0
- package/dist/db/tables/requestUsage.js +150 -0
- package/dist/db/tables/requestUsage.js.map +1 -0
- package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +155 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/stripeCustomers.d.ts +81 -0
- package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
- package/dist/db/tables/stripeCustomers.js +120 -0
- package/dist/db/tables/stripeCustomers.js.map +1 -0
- package/dist/db/tables/users.js +2 -2
- package/dist/db/tables/users.js.map +1 -1
- package/dist/handlers/internal/sponsorship.d.ts +5 -2
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
- package/dist/handlers/internal/sponsorship.js +19 -5
- package/dist/handlers/internal/sponsorship.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +10 -3
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +14 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +18 -4
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +148 -76
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +4 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +10 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +39 -8
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Fees.d.ts +27 -0
- package/dist/internal/Fees.d.ts.map +1 -0
- package/dist/internal/Fees.js +48 -0
- package/dist/internal/Fees.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +183 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -0
- package/dist/internal/FundingRoutes.js +490 -0
- package/dist/internal/FundingRoutes.js.map +1 -0
- package/dist/internal/Id.d.ts +15 -0
- package/dist/internal/Id.d.ts.map +1 -0
- package/dist/internal/Id.js +22 -0
- package/dist/internal/Id.js.map +1 -0
- package/dist/internal/Log.d.ts +2 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +3 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +10 -1
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +8 -1
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +25 -0
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +51 -0
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +8 -0
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +7 -0
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +3 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +4 -3
- package/src/ApiKey.ts +9 -2
- package/src/ApiKeys.test.ts +51 -0
- package/src/ApiKeys.ts +50 -1
- package/src/App.test.ts +33 -86718
- package/src/App.ts +18 -0
- package/src/Client.test-d.ts +351 -0
- package/src/Scope.ts +16 -3
- package/src/admin/apps/scopes.test.ts +6 -0
- package/src/admin/apps/scopes.ts +3 -0
- package/src/admin/ui/src/components/Page.tsx +2 -3
- package/src/admin/ui/src/routes/__root.tsx +2 -2
- package/src/admin/ui/src/routes/api-keys.tsx +2 -2
- package/src/admin/ui.generated.ts +13 -13
- package/src/analytics/Analytics.test.ts +527 -0
- package/src/analytics/Analytics.ts +318 -0
- package/src/analytics/tables/requestEvents.ts +523 -0
- package/src/apps/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +35 -0
- package/src/apps/data/routes/funding.test.ts +214 -0
- package/src/apps/data/routes/funding.ts +584 -0
- package/src/apps/data/routes/receipts.ts +2 -9
- package/src/apps/data/routes/rpc.test.ts +78 -2
- package/src/apps/data/routes/rpc.ts +31 -1
- package/src/apps/data/routes/webhooks.test.ts +1 -1
- package/src/apps/data/routes/webhooks.ts +11 -8
- package/src/apps/docs/App.ts +29 -100
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +1 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/platform/App.ts +43 -4
- package/src/apps/platform/Billing.test.ts +530 -0
- package/src/apps/platform/Billing.ts +682 -0
- package/src/apps/platform/index.ts +1 -0
- package/src/apps/platform/routes/api-keys.test.ts +172 -9
- package/src/apps/platform/routes/api-keys.ts +136 -20
- package/src/apps/platform/routes/billing.test.ts +1100 -0
- package/src/apps/platform/routes/billing.ts +966 -0
- package/src/apps/platform/routes/invitations.test.ts +55 -7
- package/src/apps/platform/routes/invitations.ts +68 -0
- package/src/apps/platform/routes/me.test.ts +230 -0
- package/src/apps/platform/routes/orgs.ts +26 -4
- package/src/apps/platform/routes/scopes.test.ts +29 -0
- package/src/apps/platform/routes/scopes.ts +85 -0
- package/src/apps/platform/routes/usage.test.ts +422 -0
- package/src/apps/platform/routes/usage.ts +527 -0
- package/src/apps/relay/App.test.ts +457 -8
- package/src/apps/relay/App.ts +120 -12
- package/src/apps/relay/Sponsorships.test.ts +1 -1
- package/src/apps/relay/Sponsorships.ts +2 -1
- package/src/cli/Cli.test.ts +25 -2
- package/src/cli/Cli.ts +17 -2
- package/src/cli/commands/admin.test.ts +83 -0
- package/src/cli/commands/admin.ts +89 -1
- package/src/db/Db.ts +137 -0
- package/src/db/Schema.ts +104 -2
- package/src/db/tables/billingSettings.test.ts +87 -0
- package/src/db/tables/billingSettings.ts +108 -0
- package/src/db/tables/invitations.ts +2 -2
- package/src/db/tables/organizations.test.ts +1 -1
- package/src/db/tables/organizations.ts +15 -5
- package/src/db/tables/projects.ts +2 -2
- package/src/db/tables/requestUsage.ts +231 -0
- package/src/db/tables/sponsoredTransactions.test.ts +108 -1
- package/src/db/tables/sponsoredTransactions.ts +230 -2
- package/src/db/tables/stripeCustomers.test.ts +109 -0
- package/src/db/tables/stripeCustomers.ts +162 -0
- package/src/db/tables/users.ts +2 -2
- package/src/handlers/internal/sponsorship.ts +34 -7
- package/src/handlers/internal/utils.ts +19 -7
- package/src/index.ts +1 -0
- package/src/internal/Auth.test.ts +175 -25
- package/src/internal/Auth.ts +175 -86
- package/src/internal/Cache.ts +5 -1
- package/src/internal/EdgeCache.test.ts +27 -4
- package/src/internal/EdgeCache.ts +47 -9
- package/src/internal/Fees.test.ts +32 -0
- package/src/internal/Fees.ts +51 -0
- package/src/internal/FundingRoutes.ts +583 -0
- package/src/internal/Id.test.ts +17 -0
- package/src/internal/Id.ts +24 -0
- package/src/internal/Log.test.ts +1 -0
- package/src/internal/Log.ts +5 -0
- package/src/internal/OpenApi.ts +12 -1
- package/src/internal/RateLimit.test.ts +84 -0
- package/src/internal/RateLimit.ts +70 -0
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.ts +8 -0
- package/src/internal/Webhooks.test.ts +3 -3
- package/src/internal/Webhooks.ts +3 -3
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import type Stripe from 'stripe'
|
|
3
|
+
|
|
4
|
+
import { postgresUrl } from '../../../test/containers.js'
|
|
5
|
+
import * as ApiKeys from '../../ApiKeys.js'
|
|
6
|
+
import * as Db from '../../db/Db.js'
|
|
7
|
+
import * as RequestUsage from '../../db/tables/requestUsage.js'
|
|
8
|
+
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
9
|
+
import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
|
|
10
|
+
import * as Store from '../../internal/Store.js'
|
|
11
|
+
import * as Viem from '../../internal/Viem.js'
|
|
12
|
+
import * as Billing from './Billing.js'
|
|
13
|
+
|
|
14
|
+
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
15
|
+
|
|
16
|
+
/** Baseline billable mainnet sponsorship input; tests override per case. */
|
|
17
|
+
const input = {
|
|
18
|
+
apiKeyId: 'key_1',
|
|
19
|
+
billable: true,
|
|
20
|
+
chainId: Viem.chainId.mainnet,
|
|
21
|
+
environment: 'production',
|
|
22
|
+
orgId: 'org_1',
|
|
23
|
+
projectId: 'prj_1',
|
|
24
|
+
transaction: `0x76${'cc'.repeat(16)}`,
|
|
25
|
+
} satisfies Omit<SponsoredTransactions.upsert.Input, 'signPayload'>
|
|
26
|
+
|
|
27
|
+
/** Inserts and finalizes one billable row; `seed` derives a unique sign payload. */
|
|
28
|
+
async function finalized(
|
|
29
|
+
db: Db.Db,
|
|
30
|
+
options: { feeAmount: string; orgId?: string; seed: string },
|
|
31
|
+
): Promise<SponsoredTransactions.Record> {
|
|
32
|
+
const row = await SponsoredTransactions.upsert(db, {
|
|
33
|
+
...input,
|
|
34
|
+
...(options.orgId ? { orgId: options.orgId } : {}),
|
|
35
|
+
signPayload: `0x${options.seed.repeat(32)}`,
|
|
36
|
+
})
|
|
37
|
+
await SponsoredTransactions.finalize(db, row.id, {
|
|
38
|
+
feeAmount: options.feeAmount,
|
|
39
|
+
finalizedAt: new Date().toISOString(),
|
|
40
|
+
})
|
|
41
|
+
return row
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Managed meter/price ids per kind, keyed by lookup key. */
|
|
45
|
+
const priceByLookup: Record<string, string> = {
|
|
46
|
+
[Billing.meters.apiRequests.lookupKey]: 'price_req',
|
|
47
|
+
[Billing.meters.feePayerSpend.lookupKey]: 'price_fee',
|
|
48
|
+
}
|
|
49
|
+
const meterByEvent: Record<string, string> = {
|
|
50
|
+
[Billing.meters.apiRequests.eventName]: 'mtr_req',
|
|
51
|
+
[Billing.meters.feePayerSpend.eventName]: 'mtr_fee',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A stubbed subscription: live status plus which managed kinds it carries as items. */
|
|
55
|
+
type StubSubscription = { items?: readonly Billing.MeterKind[]; status: string }
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A Stripe stub covering the reporter/subscription surface: fixtures resolve
|
|
59
|
+
* per kind, subscriptions carry per-kind items, and meter-event and
|
|
60
|
+
* item/subscription creation are captured for assertions.
|
|
61
|
+
*/
|
|
62
|
+
function stubStripe(options: {
|
|
63
|
+
meterEvent?: ((params: { identifier: string }) => void) | undefined
|
|
64
|
+
subscriptions?: readonly StubSubscription[] | undefined
|
|
65
|
+
}) {
|
|
66
|
+
const meterEvents: {
|
|
67
|
+
event_name: string
|
|
68
|
+
identifier: string
|
|
69
|
+
payload?: { stripe_customer_id: string; value: string }
|
|
70
|
+
timestamp?: number
|
|
71
|
+
}[] = []
|
|
72
|
+
const subscriptionsCreated: { items: { price: string }[] }[] = []
|
|
73
|
+
const itemsCreated: { price: string; subscription: string }[] = []
|
|
74
|
+
const expand = (subscription: StubSubscription) => ({
|
|
75
|
+
id: `sub_${subscriptionsCreated.length}_${Math.random().toString(36).slice(2, 8)}`,
|
|
76
|
+
items: {
|
|
77
|
+
data: (subscription.items ?? ['feePayerSpend']).map((kind) => ({
|
|
78
|
+
price: { id: priceByLookup[Billing.meters[kind].lookupKey] },
|
|
79
|
+
})),
|
|
80
|
+
},
|
|
81
|
+
status: subscription.status,
|
|
82
|
+
})
|
|
83
|
+
const subscriptions = (options.subscriptions ?? []).map(expand)
|
|
84
|
+
const stripe = {
|
|
85
|
+
billing: {
|
|
86
|
+
meterEvents: {
|
|
87
|
+
create: async (params: {
|
|
88
|
+
event_name: string
|
|
89
|
+
identifier: string
|
|
90
|
+
payload?: { stripe_customer_id: string; value: string }
|
|
91
|
+
timestamp?: number
|
|
92
|
+
}) => {
|
|
93
|
+
options.meterEvent?.(params)
|
|
94
|
+
meterEvents.push(params)
|
|
95
|
+
return {}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
meters: {
|
|
99
|
+
create: async (params: { event_name: string }) => ({
|
|
100
|
+
event_name: params.event_name,
|
|
101
|
+
id: meterByEvent[params.event_name] ?? 'mtr_new',
|
|
102
|
+
}),
|
|
103
|
+
list: async () => ({
|
|
104
|
+
data: Object.entries(meterByEvent).map(([event_name, id]) => ({ event_name, id })),
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
prices: {
|
|
109
|
+
create: async (params: { lookup_key: string }) => ({
|
|
110
|
+
id: priceByLookup[params.lookup_key] ?? 'price_new',
|
|
111
|
+
}),
|
|
112
|
+
list: async (params: { lookup_keys: string[] }) => ({
|
|
113
|
+
data: params.lookup_keys
|
|
114
|
+
.map((key) => priceByLookup[key])
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map((id) => ({ id })),
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
subscriptionItems: {
|
|
120
|
+
create: async (params: { price: string; subscription: string }) => {
|
|
121
|
+
itemsCreated.push(params)
|
|
122
|
+
return {}
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
subscriptions: {
|
|
126
|
+
create: async (params: { items: { price: string }[] }) => {
|
|
127
|
+
subscriptionsCreated.push(params)
|
|
128
|
+
return {}
|
|
129
|
+
},
|
|
130
|
+
list: async () => ({ data: subscriptions }),
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
return { itemsCreated, meterEvents, stripe: stripe as never as Stripe, subscriptionsCreated }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe('periodStart', () => {
|
|
137
|
+
test('behavior: anchors to the UTC calendar month start', () => {
|
|
138
|
+
expect(Billing.periodStart('month', new Date('2026-03-15T12:34:56.789Z'))).toBe('2026-03-01T00:00:00.000Z') // prettier-ignore
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('behavior: month boundaries, year rollovers, and leap years resolve in UTC', () => {
|
|
142
|
+
expect(Billing.periodStart('month', new Date('2025-12-31T23:59:59.999Z'))).toBe('2025-12-01T00:00:00.000Z') // prettier-ignore
|
|
143
|
+
expect(Billing.periodStart('month', new Date('2026-01-01T00:00:00.000Z'))).toBe('2026-01-01T00:00:00.000Z') // prettier-ignore
|
|
144
|
+
expect(Billing.periodStart('month', new Date('2028-02-29T10:00:00.000Z'))).toBe('2028-02-01T00:00:00.000Z') // prettier-ignore
|
|
145
|
+
// A UTC month starts while western timezones still sit in the prior one.
|
|
146
|
+
expect(Billing.periodStart('month', new Date('2026-07-01T03:00:00.000Z'))).toBe('2026-07-01T00:00:00.000Z') // prettier-ignore
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('toBaseUnits', () => {
|
|
151
|
+
test('behavior: round-trips decimal strings through fee-token base units', () => {
|
|
152
|
+
expect(Billing.toBaseUnits('0.35')).toBe(350_000n)
|
|
153
|
+
expect(Billing.toBaseUnits('250')).toBe(250_000_000n)
|
|
154
|
+
expect(Billing.fromBaseUnits(350_000n)).toBe('0.35')
|
|
155
|
+
expect(Billing.fromBaseUnits(250_000_000n)).toBe('250')
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('status', () => {
|
|
160
|
+
test('behavior: reflects the stored billing source status', async () => {
|
|
161
|
+
const db = create()
|
|
162
|
+
await db.migrate()
|
|
163
|
+
expect(await Billing.status(db, 'org_1')).toBeUndefined()
|
|
164
|
+
expect(await Billing.active(db, 'org_1')).toBe(false)
|
|
165
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
166
|
+
await StripeCustomers.setStatus(db, 'org_1', 'past_due')
|
|
167
|
+
expect(await Billing.status(db, 'org_1')).toBe('past_due')
|
|
168
|
+
expect(await Billing.active(db, 'org_1')).toBe(false)
|
|
169
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active')
|
|
170
|
+
expect(await Billing.active(db, 'org_1')).toBe(true)
|
|
171
|
+
await db.close()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('createReporter', () => {
|
|
176
|
+
test('behavior: a poisoned row never stalls the queue', async () => {
|
|
177
|
+
const db = create()
|
|
178
|
+
await db.migrate()
|
|
179
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
180
|
+
const poison = await finalized(db, { feeAmount: '100000', seed: '01' })
|
|
181
|
+
const good = await finalized(db, { feeAmount: '250000', seed: '02' })
|
|
182
|
+
|
|
183
|
+
const { stripe } = stubStripe({
|
|
184
|
+
meterEvent: (params) => {
|
|
185
|
+
if (params.identifier === poison.id) throw new Error('boom')
|
|
186
|
+
},
|
|
187
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
188
|
+
})
|
|
189
|
+
const reporter = Billing.createReporter({ db, stripe })
|
|
190
|
+
expect(await reporter.tick()).toStrictEqual({ failed: 1, reported: 1, skipped: 0 })
|
|
191
|
+
// The good row marked; the poisoned row stays unmarked for retry.
|
|
192
|
+
expect((await SponsoredTransactions.get(db, good.id))?.meterReportedAt).not.toBeNull()
|
|
193
|
+
expect((await SponsoredTransactions.get(db, poison.id))?.meterReportedAt).toBeNull()
|
|
194
|
+
expect(await reporter.tick()).toStrictEqual({ failed: 1, reported: 0, skipped: 0 })
|
|
195
|
+
await db.close()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('behavior: canceled customers skip without meter events', async () => {
|
|
199
|
+
const db = create()
|
|
200
|
+
await db.migrate()
|
|
201
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
202
|
+
await StripeCustomers.setStatus(db, 'org_1', 'canceled')
|
|
203
|
+
const row = await finalized(db, { feeAmount: '100000', seed: '03' })
|
|
204
|
+
|
|
205
|
+
const { meterEvents, stripe } = stubStripe({ subscriptions: [{ status: 'active' }] })
|
|
206
|
+
expect(await Billing.createReporter({ db, stripe }).tick()).toStrictEqual({
|
|
207
|
+
failed: 0,
|
|
208
|
+
reported: 0,
|
|
209
|
+
skipped: 1,
|
|
210
|
+
})
|
|
211
|
+
expect(meterEvents).toHaveLength(0)
|
|
212
|
+
// Skipped rows still mark: nobody remains to invoice, ever.
|
|
213
|
+
expect((await SponsoredTransactions.get(db, row.id))?.meterReportedAt).not.toBeNull()
|
|
214
|
+
await db.close()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('behavior: ensures the metered subscription once and stamps its backlog at now', async () => {
|
|
218
|
+
const db = create()
|
|
219
|
+
await db.migrate()
|
|
220
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
221
|
+
await finalized(db, { feeAmount: '100000', seed: '04' })
|
|
222
|
+
await finalized(db, { feeAmount: '250000', seed: '05' })
|
|
223
|
+
|
|
224
|
+
const { meterEvents, stripe, subscriptionsCreated } = stubStripe({ subscriptions: [] })
|
|
225
|
+
expect(await Billing.createReporter({ db, stripe }).tick()).toStrictEqual({
|
|
226
|
+
failed: 0,
|
|
227
|
+
reported: 2,
|
|
228
|
+
skipped: 0,
|
|
229
|
+
})
|
|
230
|
+
// One ensure per org per tick, not per row.
|
|
231
|
+
expect(subscriptionsCreated).toHaveLength(1)
|
|
232
|
+
// The just-created subscription bills nothing before its start, so the
|
|
233
|
+
// backlog carries no timestamp and lands on the first invoice.
|
|
234
|
+
expect(meterEvents).toHaveLength(2)
|
|
235
|
+
for (const event of meterEvents) expect(event.timestamp).toBeUndefined()
|
|
236
|
+
await db.close()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('behavior: an existing subscription keeps finalization timestamps', async () => {
|
|
240
|
+
const db = create()
|
|
241
|
+
await db.migrate()
|
|
242
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
243
|
+
const row = await finalized(db, { feeAmount: '100000', seed: '06' })
|
|
244
|
+
|
|
245
|
+
const { meterEvents, stripe, subscriptionsCreated } = stubStripe({
|
|
246
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
247
|
+
})
|
|
248
|
+
expect(await Billing.createReporter({ db, stripe }).tick()).toStrictEqual({
|
|
249
|
+
failed: 0,
|
|
250
|
+
reported: 1,
|
|
251
|
+
skipped: 0,
|
|
252
|
+
})
|
|
253
|
+
expect(subscriptionsCreated).toHaveLength(0)
|
|
254
|
+
const finalizedAt = new Date((await SponsoredTransactions.get(db, row.id))!.finalizedAt!)
|
|
255
|
+
expect(meterEvents[0]?.timestamp).toBe(Math.floor(finalizedAt.getTime() / 1000))
|
|
256
|
+
await db.close()
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe('createKeyBillingSyncer', () => {
|
|
261
|
+
test('behavior: converges drifted sandbox key snapshots to the billing table', async () => {
|
|
262
|
+
const db = create()
|
|
263
|
+
await db.migrate()
|
|
264
|
+
const kv = Store.memory()
|
|
265
|
+
|
|
266
|
+
// A sandbox key minted while billing was inactive; billing later activates
|
|
267
|
+
// but the webhook re-sync never lands, so the record snapshot is stale.
|
|
268
|
+
const { token } = await ApiKeys.mint(kv, {
|
|
269
|
+
billingActive: false,
|
|
270
|
+
environment: 'sandbox',
|
|
271
|
+
orgId: 'org_1',
|
|
272
|
+
scopes: ['data:read'],
|
|
273
|
+
})
|
|
274
|
+
await StripeCustomers.create(db, {
|
|
275
|
+
environment: 'sandbox',
|
|
276
|
+
orgId: 'org_1',
|
|
277
|
+
stripeCustomerId: 'cus_1',
|
|
278
|
+
})
|
|
279
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active', 'sandbox')
|
|
280
|
+
|
|
281
|
+
const syncer = Billing.createKeyBillingSyncer({ db, kv })
|
|
282
|
+
expect(await syncer.tick()).toStrictEqual({ checked: 1, updated: 1 })
|
|
283
|
+
expect((await ApiKeys.resolve(kv, token))?.billingActive).toBe(true)
|
|
284
|
+
|
|
285
|
+
// A converged fleet re-reads but writes nothing.
|
|
286
|
+
expect(await syncer.tick()).toStrictEqual({ checked: 1, updated: 0 })
|
|
287
|
+
await db.close()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('behavior: reconciles a lapse back to the public quota', async () => {
|
|
291
|
+
const db = create()
|
|
292
|
+
await db.migrate()
|
|
293
|
+
const kv = Store.memory()
|
|
294
|
+
|
|
295
|
+
const { token } = await ApiKeys.mint(kv, {
|
|
296
|
+
billingActive: true,
|
|
297
|
+
environment: 'sandbox',
|
|
298
|
+
orgId: 'org_1',
|
|
299
|
+
scopes: ['data:read'],
|
|
300
|
+
})
|
|
301
|
+
await StripeCustomers.create(db, {
|
|
302
|
+
environment: 'sandbox',
|
|
303
|
+
orgId: 'org_1',
|
|
304
|
+
stripeCustomerId: 'cus_1',
|
|
305
|
+
})
|
|
306
|
+
await StripeCustomers.setStatus(db, 'org_1', 'past_due', 'sandbox')
|
|
307
|
+
|
|
308
|
+
expect(await Billing.createKeyBillingSyncer({ db, kv }).tick()).toStrictEqual({
|
|
309
|
+
checked: 1,
|
|
310
|
+
updated: 1,
|
|
311
|
+
})
|
|
312
|
+
// Non-`active` billing throttles: the snapshot flips back to false.
|
|
313
|
+
expect((await ApiKeys.resolve(kv, token))?.billingActive).toBe(false)
|
|
314
|
+
await db.close()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test('behavior: leaves production billing sources untouched', async () => {
|
|
318
|
+
const db = create()
|
|
319
|
+
await db.migrate()
|
|
320
|
+
const kv = Store.memory()
|
|
321
|
+
|
|
322
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
323
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active')
|
|
324
|
+
|
|
325
|
+
// The default syncer reconciles sandbox only; a production-only org is
|
|
326
|
+
// outside its driver set.
|
|
327
|
+
expect(await Billing.createKeyBillingSyncer({ db, kv }).tick()).toStrictEqual({
|
|
328
|
+
checked: 0,
|
|
329
|
+
updated: 0,
|
|
330
|
+
})
|
|
331
|
+
await db.close()
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('ensureSubscription', () => {
|
|
336
|
+
test('behavior: adds the missing item to a live subscription', async () => {
|
|
337
|
+
const { itemsCreated, stripe, subscriptionsCreated } = stubStripe({
|
|
338
|
+
subscriptions: [{ items: ['feePayerSpend'], status: 'active' }],
|
|
339
|
+
})
|
|
340
|
+
const result = await Billing.ensureSubscription(stripe, 'cus_1', [
|
|
341
|
+
'feePayerSpend',
|
|
342
|
+
'apiRequests',
|
|
343
|
+
])
|
|
344
|
+
expect(result.created).toBe(false)
|
|
345
|
+
expect(subscriptionsCreated).toHaveLength(0)
|
|
346
|
+
expect(itemsCreated).toEqual([{ price: 'price_req', subscription: expect.any(String) }])
|
|
347
|
+
expect(result.items.apiRequests?.created).toBe(true)
|
|
348
|
+
expect(result.items.feePayerSpend?.created).toBe(false)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('behavior: creates a subscription carrying every requested item', async () => {
|
|
352
|
+
const { stripe, subscriptionsCreated } = stubStripe({ subscriptions: [] })
|
|
353
|
+
const result = await Billing.ensureSubscription(stripe, 'cus_1', [
|
|
354
|
+
'feePayerSpend',
|
|
355
|
+
'apiRequests',
|
|
356
|
+
])
|
|
357
|
+
expect(result.created).toBe(true)
|
|
358
|
+
expect(subscriptionsCreated).toHaveLength(1)
|
|
359
|
+
expect(subscriptionsCreated[0]!.items.map((item) => item.price).sort()).toEqual([
|
|
360
|
+
'price_fee',
|
|
361
|
+
'price_req',
|
|
362
|
+
])
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('behavior: fails closed on multiple live managed subscriptions', async () => {
|
|
366
|
+
const { stripe } = stubStripe({
|
|
367
|
+
subscriptions: [
|
|
368
|
+
{ items: ['feePayerSpend'], status: 'active' },
|
|
369
|
+
{ items: ['apiRequests'], status: 'active' },
|
|
370
|
+
],
|
|
371
|
+
})
|
|
372
|
+
await expect(
|
|
373
|
+
Billing.ensureSubscription(stripe, 'cus_1', ['feePayerSpend', 'apiRequests']),
|
|
374
|
+
).rejects.toBeInstanceOf(Billing.DuplicateSubscriptionError)
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
describe('createRequestUsageReporter', () => {
|
|
379
|
+
// A closed hour inside the settlement window for the fixed clock below.
|
|
380
|
+
const bucketStart = '2026-01-01T12:00:00.000Z'
|
|
381
|
+
const now = () => Date.parse('2026-01-02T00:20:00.000Z')
|
|
382
|
+
|
|
383
|
+
test('behavior: seals the initial count, then bills only the delta on growth', async () => {
|
|
384
|
+
const db = create()
|
|
385
|
+
await db.migrate()
|
|
386
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
387
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active')
|
|
388
|
+
|
|
389
|
+
// Tick 1: fresh customer, initial seal of 5. The just-created item bills at
|
|
390
|
+
// now (no backdate).
|
|
391
|
+
const first = stubStripe({ subscriptions: [] })
|
|
392
|
+
const reporter1 = Billing.createRequestUsageReporter({
|
|
393
|
+
db,
|
|
394
|
+
now,
|
|
395
|
+
readCounts: async () => [{ bucketStart, count: 5, orgId: 'org_1' }],
|
|
396
|
+
stripe: first.stripe,
|
|
397
|
+
})
|
|
398
|
+
expect(await reporter1.tick()).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
|
|
399
|
+
expect(first.meterEvents).toEqual([
|
|
400
|
+
{ event_name: 'api_request_count', identifier: expect.any(String), payload: expect.objectContaining({ value: '5' }) }, // prettier-ignore
|
|
401
|
+
])
|
|
402
|
+
expect(first.meterEvents[0]!.timestamp).toBeUndefined()
|
|
403
|
+
expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' }))?.reportedCount).toBe(5) // prettier-ignore
|
|
404
|
+
|
|
405
|
+
// Tick 2: count grows to 8; only the delta of 3 bills, backdated (item exists).
|
|
406
|
+
const second = stubStripe({
|
|
407
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
408
|
+
})
|
|
409
|
+
const reporter2 = Billing.createRequestUsageReporter({
|
|
410
|
+
db,
|
|
411
|
+
now,
|
|
412
|
+
readCounts: async () => [{ bucketStart, count: 8, orgId: 'org_1' }],
|
|
413
|
+
stripe: second.stripe,
|
|
414
|
+
})
|
|
415
|
+
expect(await reporter2.tick()).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
|
|
416
|
+
expect(second.meterEvents[0]?.payload).toMatchObject({ value: '3' })
|
|
417
|
+
expect(second.meterEvents[0]?.timestamp).toBe(Math.floor(Date.parse(bucketStart) / 1000))
|
|
418
|
+
expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' }))?.reportedCount).toBe(8) // prettier-ignore
|
|
419
|
+
|
|
420
|
+
// Tick 3: no growth; nothing bills.
|
|
421
|
+
const third = stubStripe({
|
|
422
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
423
|
+
})
|
|
424
|
+
expect(
|
|
425
|
+
await Billing.createRequestUsageReporter({
|
|
426
|
+
db,
|
|
427
|
+
now,
|
|
428
|
+
readCounts: async () => [{ bucketStart, count: 8, orgId: 'org_1' }],
|
|
429
|
+
stripe: third.stripe,
|
|
430
|
+
}).tick(),
|
|
431
|
+
).toStrictEqual({ failed: 0, reported: 0, skipped: 0 })
|
|
432
|
+
expect(third.meterEvents).toHaveLength(0)
|
|
433
|
+
await db.close()
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
test('behavior: seals only hours older than the settlement delay', async () => {
|
|
437
|
+
const db = create()
|
|
438
|
+
await db.migrate()
|
|
439
|
+
const readCounts = vi.fn(async () => [] as { bucketStart: string; count: number; orgId: string }[]) // prettier-ignore
|
|
440
|
+
const { stripe } = stubStripe({ subscriptions: [] })
|
|
441
|
+
await Billing.createRequestUsageReporter({ db, now, readCounts, stripe }).tick()
|
|
442
|
+
// Cutoff is the start of the hour 15 minutes before `now`.
|
|
443
|
+
expect(readCounts).toHaveBeenCalledWith({
|
|
444
|
+
environment: 'production',
|
|
445
|
+
from: '2025-12-31T00:00:00.000Z',
|
|
446
|
+
to: '2026-01-02T00:00:00.000Z',
|
|
447
|
+
})
|
|
448
|
+
await db.close()
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test('behavior: an org without a billing source is skipped and never sealed', async () => {
|
|
452
|
+
const db = create()
|
|
453
|
+
await db.migrate()
|
|
454
|
+
const { meterEvents, stripe } = stubStripe({ subscriptions: [] })
|
|
455
|
+
expect(
|
|
456
|
+
await Billing.createRequestUsageReporter({
|
|
457
|
+
db,
|
|
458
|
+
now,
|
|
459
|
+
readCounts: async () => [{ bucketStart, count: 4, orgId: 'org_1' }],
|
|
460
|
+
stripe,
|
|
461
|
+
}).tick(),
|
|
462
|
+
).toStrictEqual({ failed: 0, reported: 0, skipped: 1 })
|
|
463
|
+
expect(meterEvents).toHaveLength(0)
|
|
464
|
+
expect(await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' })).toBeUndefined() // prettier-ignore
|
|
465
|
+
await db.close()
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test('behavior: bills sandbox usage on the sandbox client', async () => {
|
|
469
|
+
const db = create()
|
|
470
|
+
await db.migrate()
|
|
471
|
+
await StripeCustomers.create(db, {
|
|
472
|
+
environment: 'sandbox',
|
|
473
|
+
orgId: 'org_1',
|
|
474
|
+
stripeCustomerId: 'cus_sandbox',
|
|
475
|
+
})
|
|
476
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active', 'sandbox')
|
|
477
|
+
|
|
478
|
+
const production = stubStripe({ subscriptions: [] })
|
|
479
|
+
const sandbox = stubStripe({ subscriptions: [] })
|
|
480
|
+
const reporter = Billing.createRequestUsageReporter({
|
|
481
|
+
db,
|
|
482
|
+
now,
|
|
483
|
+
readCounts: async ({ environment }) =>
|
|
484
|
+
environment === 'sandbox' ? [{ bucketStart, count: 7, orgId: 'org_1' }] : [],
|
|
485
|
+
sandboxStripe: sandbox.stripe,
|
|
486
|
+
stripe: production.stripe,
|
|
487
|
+
})
|
|
488
|
+
expect(await reporter.tick()).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
|
|
489
|
+
expect(production.meterEvents).toHaveLength(0)
|
|
490
|
+
expect(sandbox.meterEvents[0]?.payload).toMatchObject({
|
|
491
|
+
stripe_customer_id: 'cus_sandbox',
|
|
492
|
+
value: '7',
|
|
493
|
+
})
|
|
494
|
+
expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'sandbox', orgId: 'org_1' }))?.reportedCount).toBe(7) // prettier-ignore
|
|
495
|
+
await db.close()
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
test('behavior: re-sends a stranded pending event and advances the watermark', async () => {
|
|
499
|
+
const db = create()
|
|
500
|
+
await db.migrate()
|
|
501
|
+
await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
|
|
502
|
+
await StripeCustomers.setStatus(db, 'org_1', 'active')
|
|
503
|
+
// Simulate a crash after sealing but before the Stripe ack.
|
|
504
|
+
await RequestUsage.insertPending(db, {
|
|
505
|
+
bucketStart,
|
|
506
|
+
deltaCount: 6,
|
|
507
|
+
environment: 'production',
|
|
508
|
+
identifier: 'rq_production_deadbeef0001_0_0',
|
|
509
|
+
orgId: 'org_1',
|
|
510
|
+
sequence: 0,
|
|
511
|
+
stripeCustomerId: 'cus_1',
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
const { meterEvents, stripe } = stubStripe({
|
|
515
|
+
subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
|
|
516
|
+
})
|
|
517
|
+
expect(
|
|
518
|
+
await Billing.createRequestUsageReporter({
|
|
519
|
+
db,
|
|
520
|
+
now,
|
|
521
|
+
// No new counts; recovery must still drain the pending event.
|
|
522
|
+
readCounts: async () => [],
|
|
523
|
+
stripe,
|
|
524
|
+
}).tick(),
|
|
525
|
+
).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
|
|
526
|
+
expect(meterEvents[0]?.payload).toMatchObject({ value: '6' })
|
|
527
|
+
expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' }))?.reportedCount).toBe(6) // prettier-ignore
|
|
528
|
+
await db.close()
|
|
529
|
+
})
|
|
530
|
+
})
|