tapimo 0.6.0 → 0.7.1
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/dist/App.d.ts +8641 -8263
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +9 -1
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +4 -0
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +5 -0
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +112 -11
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +2 -1
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/early-access.d.ts +2 -2
- package/dist/admin/apps/early-access.d.ts.map +1 -1
- package/dist/admin/apps/early-access.js +5 -5
- package/dist/admin/apps/early-access.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +124 -1
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/organizations.js +97 -6
- package/dist/admin/apps/organizations.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +47 -39
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +20 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +18 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +36 -7
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +8050 -8019
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +2 -15
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +1 -1
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.js +1 -1
- package/dist/apps/data/FxOracle.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +27 -27
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +21 -1
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +58 -58
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +669 -669
- package/dist/apps/data/routes/fee-amm.d.ts +157 -143
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +76 -91
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +72 -34
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +34 -26
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +30 -30
- package/dist/apps/data/routes/receipts.d.ts +8 -8
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
- package/dist/apps/data/routes/tokenlist.js +12 -3
- package/dist/apps/data/routes/tokenlist.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +4 -4
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +5 -1
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +6 -6
- package/dist/apps/data/routes/transfers.d.ts +101 -90
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +226 -141
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +2 -2
- package/dist/apps/data/routes/valuation.js +3 -3
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/verified-tokens.js +34 -9
- package/dist/apps/data/routes/verified-tokens.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +2 -2
- package/dist/apps/data/routes/webhooks.js +14 -6
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- 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/management/App.d.ts +460 -523
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +7 -23
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +20 -8
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +106 -151
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +28 -28
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +48 -34
- package/dist/apps/management/routes/faucet.d.ts.map +1 -1
- package/dist/apps/management/routes/faucet.js +33 -18
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +131 -131
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +0 -3
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +6 -8
- package/dist/apps/management/routes/me.d.ts.map +1 -1
- package/dist/apps/management/routes/me.js +0 -4
- package/dist/apps/management/routes/me.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +73 -73
- package/dist/apps/management/routes/orgs.d.ts +94 -94
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +5 -0
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +72 -72
- package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
- package/dist/apps/management/routes/webhooks.d.ts +1 -1
- package/dist/apps/mpp/App.d.ts +363 -0
- package/dist/apps/mpp/App.d.ts.map +1 -0
- package/dist/apps/mpp/App.js +72 -0
- package/dist/apps/mpp/App.js.map +1 -0
- package/dist/apps/mpp/Mpp.d.ts +178 -0
- package/dist/apps/mpp/Mpp.d.ts.map +1 -0
- package/dist/apps/mpp/Mpp.js +183 -0
- package/dist/apps/mpp/Mpp.js.map +1 -0
- package/dist/apps/mpp/index.d.ts +3 -0
- package/dist/apps/mpp/index.d.ts.map +1 -0
- package/dist/apps/mpp/index.js +3 -0
- package/dist/apps/mpp/index.js.map +1 -0
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +57 -10
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.js +1 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +27 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +12 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +11 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/enabledBillingSources.d.ts +73 -0
- package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
- package/dist/db/tables/enabledBillingSources.js +69 -0
- package/dist/db/tables/enabledBillingSources.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -12
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/invitations.js +3 -23
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +5 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +32 -2
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +13 -3
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +1 -16
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +17 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +37 -5
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +0 -32
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +3 -51
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +3 -2
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +78 -32
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +45 -8
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +18 -4
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +50 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +15 -0
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/Response.d.ts +2 -2
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Response.js +12 -4
- package/dist/internal/Response.js.map +1 -1
- package/dist/internal/Schema.d.ts +2 -2
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +4 -2
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/package.json +7 -2
- package/src/App.test.ts +9 -4
- package/src/App.ts +14 -2
- package/src/Client.test-d.ts +109 -28
- package/src/Scope.ts +5 -0
- package/src/admin/App.test.ts +4 -0
- package/src/admin/apps/early-access.ts +6 -5
- package/src/admin/apps/organizations.test.ts +86 -2
- package/src/admin/apps/organizations.ts +155 -31
- package/src/admin/apps/scopes.test.ts +5 -0
- package/src/admin/ui/src/lib/organizations.test.ts +81 -1
- package/src/admin/ui/src/lib/organizations.ts +92 -0
- package/src/admin/ui/src/routes/api-keys.tsx +1 -1
- package/src/admin/ui/src/routes/early-access.tsx +8 -3
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
- package/src/admin/ui.generated.ts +49 -39
- package/src/analytics/Analytics.test.ts +83 -0
- package/src/analytics/Analytics.ts +20 -0
- package/src/analytics/tables/requestEvents.ts +60 -7
- package/src/apps/composition.test.ts +11 -2
- package/src/apps/data/App.test.ts +14 -2
- package/src/apps/data/FundingQuoteProviders.test.ts +0 -5
- package/src/apps/data/FundingQuoteProviders.ts +2 -29
- package/src/apps/data/FxOracle.test.ts +2 -2
- package/src/apps/data/FxOracle.ts +2 -2
- package/src/apps/data/routes/activities.test.ts +6 -2
- package/src/apps/data/routes/activities.ts +39 -1
- package/src/apps/data/routes/balances.test.ts +6 -2
- package/src/apps/data/routes/exchanges.test.ts +7 -3
- package/src/apps/data/routes/fee-amm.test.ts +47 -23
- package/src/apps/data/routes/fee-amm.ts +98 -119
- package/src/apps/data/routes/funding.test.ts +29 -8
- package/src/apps/data/routes/funding.ts +42 -48
- package/src/apps/data/routes/receipts.test.ts +6 -2
- package/src/apps/data/routes/tokenlist.test.ts +15 -0
- package/src/apps/data/routes/tokenlist.ts +18 -5
- package/src/apps/data/routes/tokens.ts +5 -1
- package/src/apps/data/routes/transfers.test.ts +203 -35
- package/src/apps/data/routes/transfers.ts +294 -172
- package/src/apps/data/routes/valuation.test.ts +18 -3
- package/src/apps/data/routes/valuation.ts +4 -4
- package/src/apps/data/routes/verified-tokens.test.ts +51 -0
- package/src/apps/data/routes/verified-tokens.ts +52 -15
- package/src/apps/data/routes/webhooks.test.ts +56 -0
- package/src/apps/data/routes/webhooks.ts +17 -8
- package/src/apps/index.ts +1 -0
- package/src/apps/management/routes/api-keys.test.ts +2 -75
- package/src/apps/management/routes/api-keys.ts +30 -7
- package/src/apps/management/routes/billing.test.ts +106 -85
- package/src/apps/management/routes/billing.ts +32 -21
- package/src/apps/management/routes/faucet.test.ts +11 -3
- package/src/apps/management/routes/faucet.ts +46 -30
- package/src/apps/management/routes/invitations.test.ts +0 -172
- package/src/apps/management/routes/invitations.ts +0 -3
- package/src/apps/management/routes/me.test.ts +3 -145
- package/src/apps/management/routes/me.ts +0 -6
- package/src/apps/management/routes/orgs.test.ts +45 -2
- package/src/apps/management/routes/orgs.ts +6 -0
- package/src/apps/management/routes/scopes.test.ts +1 -0
- package/src/apps/mpp/App.ts +86 -0
- package/src/apps/mpp/Mpp.test.ts +193 -0
- package/src/apps/mpp/Mpp.ts +353 -0
- package/src/apps/mpp/index.ts +2 -0
- package/src/apps/mpp/internal/Adapter.ts +11 -0
- package/src/apps/mpp/internal/Error.ts +9 -0
- package/src/apps/mpp/internal/Idempotency.ts +7 -0
- package/src/apps/mpp/internal/Screening.ts +10 -0
- package/src/apps/relay/App.test.ts +90 -8
- package/src/apps/relay/App.ts +62 -10
- package/src/apps/relay/Sponsorships.test.ts +2 -0
- package/src/apps/relay/Sponsorships.ts +1 -1
- package/src/db/Db.test.ts +2 -2
- package/src/db/Db.ts +30 -0
- package/src/db/Schema.ts +15 -0
- package/src/db/tables/enabledBillingSources.test.ts +303 -0
- package/src/db/tables/enabledBillingSources.ts +118 -0
- package/src/db/tables/invitations.ts +4 -26
- package/src/db/tables/organizations.test.ts +191 -0
- package/src/db/tables/organizations.ts +44 -3
- package/src/db/tables/sponsoredTransactions.test.ts +41 -2
- package/src/db/tables/sponsoredTransactions.ts +15 -3
- package/src/handlers/internal/utils.ts +22 -6
- package/src/handlers/relay.errors.test.ts +153 -0
- package/src/handlers/relay.ts +42 -7
- package/src/internal/Auth.test.ts +3 -3
- package/src/internal/Auth.ts +3 -63
- package/src/internal/EdgeCache.test.ts +18 -5
- package/src/internal/EdgeCache.ts +4 -2
- package/src/internal/FundingQuotes.test.ts +18 -5
- package/src/internal/FundingQuotes.ts +46 -11
- package/src/internal/Log.test.ts +91 -0
- package/src/internal/Log.ts +69 -5
- package/src/internal/MetricSink.test.ts +55 -0
- package/src/internal/MetricSink.ts +14 -0
- package/src/internal/Response.test.ts +28 -0
- package/src/internal/Response.ts +12 -4
- package/src/internal/Schema.test.ts +13 -0
- package/src/internal/Schema.ts +14 -7
|
@@ -5,11 +5,10 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
|
5
5
|
import * as ApiKeys from '../../../ApiKeys.js'
|
|
6
6
|
import * as TestApp from '../../../../test/App.js'
|
|
7
7
|
import * as TestStripe from '../../../../test/Stripe.js'
|
|
8
|
-
import * as
|
|
8
|
+
import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js'
|
|
9
9
|
import * as Memberships from '../../../db/tables/memberships.js'
|
|
10
10
|
import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js'
|
|
11
11
|
import * as StripeCustomers from '../../../db/tables/stripeCustomers.js'
|
|
12
|
-
import * as Users from '../../../db/tables/users.js'
|
|
13
12
|
import * as Viem from '../../../internal/Viem.js'
|
|
14
13
|
import * as core_Billing from '../Billing.js'
|
|
15
14
|
import type * as App from '../../../App.js'
|
|
@@ -66,7 +65,6 @@ function createApp(
|
|
|
66
65
|
billing?: App.Billing | undefined
|
|
67
66
|
db?: TestApp.create.Options['db']
|
|
68
67
|
kv?: { store: ReturnType<typeof TestApp.kvStore> }
|
|
69
|
-
tmp_earlyAccess?: boolean
|
|
70
68
|
} = {},
|
|
71
69
|
) {
|
|
72
70
|
// prettier-ignore
|
|
@@ -76,7 +74,6 @@ function createApp(
|
|
|
76
74
|
db: options.db ?? TestApp.database(),
|
|
77
75
|
...(options.kv ? { kv: options.kv } : {}),
|
|
78
76
|
session: { wallet: { origin } },
|
|
79
|
-
...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
|
|
80
77
|
})
|
|
81
78
|
}
|
|
82
79
|
|
|
@@ -108,6 +105,20 @@ async function seeded(app: ReturnType<typeof createApp>) {
|
|
|
108
105
|
return { cookie, orgId: id }
|
|
109
106
|
}
|
|
110
107
|
|
|
108
|
+
/** Seeds an owned organization with Stripe setup enabled. */
|
|
109
|
+
async function seededWithStripe(
|
|
110
|
+
app: ReturnType<typeof createApp>,
|
|
111
|
+
db: ReturnType<typeof TestApp.database>,
|
|
112
|
+
) {
|
|
113
|
+
const result = await seeded(app)
|
|
114
|
+
await EnabledBillingSources.add(db, {
|
|
115
|
+
createdBy: 'test',
|
|
116
|
+
orgId: result.orgId,
|
|
117
|
+
source: 'stripe',
|
|
118
|
+
})
|
|
119
|
+
return result
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
describe('GET /orgs/:orgId/billing', () => {
|
|
112
123
|
test('behavior: synthesizes none for orgs without a billing source', async () => {
|
|
113
124
|
const app = createApp()
|
|
@@ -116,6 +127,7 @@ describe('GET /orgs/:orgId/billing', () => {
|
|
|
116
127
|
expect(response.status).toBe(200)
|
|
117
128
|
expect(await response.json()).toMatchInlineSnapshot(`
|
|
118
129
|
{
|
|
130
|
+
"enabledSources": [],
|
|
119
131
|
"spend": {
|
|
120
132
|
"amount": "0",
|
|
121
133
|
"currency": "usd",
|
|
@@ -139,6 +151,28 @@ describe('GET /orgs/:orgId/billing', () => {
|
|
|
139
151
|
expect(body.updatedAt).toBeDefined()
|
|
140
152
|
})
|
|
141
153
|
|
|
154
|
+
test('behavior: lists organization-wide sources in canonical order', async () => {
|
|
155
|
+
const db = TestApp.database()
|
|
156
|
+
const app = createApp({ billing: billingWithSandbox(), db })
|
|
157
|
+
const { cookie, orgId } = await seeded(app)
|
|
158
|
+
await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'tempo' })
|
|
159
|
+
await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' })
|
|
160
|
+
|
|
161
|
+
const production = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } })
|
|
162
|
+
const sandbox = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, {
|
|
163
|
+
headers: { cookie },
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(await production.json()).toMatchObject({
|
|
167
|
+
enabledSources: ['stripe', 'tempo'],
|
|
168
|
+
status: 'none',
|
|
169
|
+
})
|
|
170
|
+
expect(await sandbox.json()).toMatchObject({
|
|
171
|
+
enabledSources: ['stripe', 'tempo'],
|
|
172
|
+
status: 'none',
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
142
176
|
test('behavior: the super admin reads any org', async () => {
|
|
143
177
|
const db = TestApp.database()
|
|
144
178
|
const app = createApp({ db })
|
|
@@ -165,7 +199,7 @@ describe('GET /orgs/:orgId/billing', () => {
|
|
|
165
199
|
expect(response.status).toBe(401)
|
|
166
200
|
})
|
|
167
201
|
|
|
168
|
-
test('behavior:
|
|
202
|
+
test('behavior: reads database-backed state when Stripe is unconfigured', async () => {
|
|
169
203
|
const app = TestApp.create({
|
|
170
204
|
auth: { superAdmin: { secret } },
|
|
171
205
|
db: TestApp.database(),
|
|
@@ -173,9 +207,18 @@ describe('GET /orgs/:orgId/billing', () => {
|
|
|
173
207
|
})
|
|
174
208
|
const { cookie, orgId } = await seeded(app)
|
|
175
209
|
const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } })
|
|
176
|
-
expect(response.status).toBe(
|
|
177
|
-
|
|
178
|
-
|
|
210
|
+
expect(response.status).toBe(200)
|
|
211
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
212
|
+
{
|
|
213
|
+
"enabledSources": [],
|
|
214
|
+
"spend": {
|
|
215
|
+
"amount": "0",
|
|
216
|
+
"currency": "usd",
|
|
217
|
+
"period": "month",
|
|
218
|
+
},
|
|
219
|
+
"status": "none",
|
|
220
|
+
}
|
|
221
|
+
`)
|
|
179
222
|
})
|
|
180
223
|
})
|
|
181
224
|
|
|
@@ -202,6 +245,7 @@ describe('PATCH /orgs/:orgId/billing', () => {
|
|
|
202
245
|
expect(response.status).toBe(200)
|
|
203
246
|
expect(await response.json()).toMatchInlineSnapshot(`
|
|
204
247
|
{
|
|
248
|
+
"enabledSources": [],
|
|
205
249
|
"spend": {
|
|
206
250
|
"amount": "0",
|
|
207
251
|
"currency": "usd",
|
|
@@ -294,7 +338,7 @@ describe('PATCH /orgs/:orgId/billing', () => {
|
|
|
294
338
|
expect(response.status).toBe(404)
|
|
295
339
|
})
|
|
296
340
|
|
|
297
|
-
test('behavior:
|
|
341
|
+
test('behavior: updates database-backed state when Stripe is unconfigured', async () => {
|
|
298
342
|
const app = TestApp.create({
|
|
299
343
|
auth: { superAdmin: { secret } },
|
|
300
344
|
db: TestApp.database(),
|
|
@@ -303,9 +347,12 @@ describe('PATCH /orgs/:orgId/billing', () => {
|
|
|
303
347
|
const { cookie, orgId } = await seeded(app)
|
|
304
348
|
const response = await app.request(
|
|
305
349
|
`/v1/orgs/${orgId}/billing`,
|
|
306
|
-
patch(cookie, { spendLimit:
|
|
350
|
+
patch(cookie, { spendLimit: { amount: '250' } }),
|
|
307
351
|
)
|
|
308
|
-
expect(response.status).toBe(
|
|
352
|
+
expect(response.status).toBe(200)
|
|
353
|
+
expect(
|
|
354
|
+
((await response.json()) as { spendLimit?: { amount: string } }).spendLimit?.amount,
|
|
355
|
+
).toBe('250')
|
|
309
356
|
})
|
|
310
357
|
})
|
|
311
358
|
|
|
@@ -355,8 +402,9 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
|
|
|
355
402
|
})
|
|
356
403
|
|
|
357
404
|
test('behavior: an unreachable Stripe maps to the upstream error envelope', async () => {
|
|
358
|
-
const
|
|
359
|
-
const {
|
|
405
|
+
const db = TestApp.database()
|
|
406
|
+
const app = createApp({ db })
|
|
407
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
360
408
|
// First Stripe call the route makes is the customer create; the connection
|
|
361
409
|
// is refused for real (no transport fake).
|
|
362
410
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
@@ -366,9 +414,13 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
|
|
|
366
414
|
})
|
|
367
415
|
})
|
|
368
416
|
|
|
369
|
-
describe('
|
|
370
|
-
test('behavior: checkout requires
|
|
371
|
-
const app =
|
|
417
|
+
describe('billing source access', () => {
|
|
418
|
+
test('behavior: checkout requires Stripe to be enabled', async () => {
|
|
419
|
+
const app = TestApp.create({
|
|
420
|
+
auth: { superAdmin: { secret } },
|
|
421
|
+
db: TestApp.database(),
|
|
422
|
+
session: { wallet: { origin } },
|
|
423
|
+
})
|
|
372
424
|
const { cookie, orgId } = await seeded(app)
|
|
373
425
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
374
426
|
expect(response.status).toBe(403)
|
|
@@ -377,75 +429,29 @@ describe('early access', () => {
|
|
|
377
429
|
expect(body).toMatchInlineSnapshot(`
|
|
378
430
|
{
|
|
379
431
|
"error": {
|
|
380
|
-
"code": "
|
|
381
|
-
"message": "
|
|
432
|
+
"code": "billing_source_disabled",
|
|
433
|
+
"message": "Stripe billing is not enabled for this organization",
|
|
382
434
|
},
|
|
383
435
|
}
|
|
384
436
|
`)
|
|
385
437
|
})
|
|
386
438
|
|
|
387
|
-
test('behavior: the gate
|
|
388
|
-
// The gate depends only on the caller, so it leaks nothing about the org;
|
|
389
|
-
// early-access strangers still get the membership 404.
|
|
439
|
+
test('behavior: the source gate replaces temporary billing early access', async () => {
|
|
390
440
|
const db = TestApp.database()
|
|
391
|
-
const app = createApp({ db
|
|
392
|
-
|
|
393
|
-
const { orgId } = await seeded(app)
|
|
394
|
-
|
|
395
|
-
const other = await session(app)
|
|
396
|
-
const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(other))
|
|
397
|
-
expect(gated.status).toBe(403)
|
|
398
|
-
expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
|
|
399
|
-
|
|
400
|
-
const granted = await session(app)
|
|
401
|
-
const me = await app.request('/v1/me', { headers: { cookie: granted } })
|
|
402
|
-
const { id: userId } = (await me.json()) as { id: string }
|
|
403
|
-
await Users.setEmail(db, userId, 'stranger@example.org')
|
|
404
|
-
expect(
|
|
405
|
-
(await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(granted))).status,
|
|
406
|
-
).toBe(404)
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
test('behavior: the gate precedes the owner-role check', async () => {
|
|
410
|
-
const db = TestApp.database()
|
|
411
|
-
const app = createApp({ db, tmp_earlyAccess: true })
|
|
412
|
-
await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
|
|
413
|
-
const { orgId } = await seeded(app)
|
|
414
|
-
const member = await session(app)
|
|
415
|
-
const me = await app.request('/v1/me', { headers: { cookie: member } })
|
|
416
|
-
const { id: userId } = (await me.json()) as { id: string }
|
|
417
|
-
await Memberships.create(db, { orgId, role: 'member', userId })
|
|
418
|
-
|
|
419
|
-
const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
|
|
420
|
-
expect(gated.status).toBe(403)
|
|
421
|
-
expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
|
|
422
|
-
|
|
423
|
-
// Early-access members still fail the owner-role check.
|
|
424
|
-
await Users.setEmail(db, userId, 'member@example.org')
|
|
425
|
-
const write = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
|
|
426
|
-
expect(write.status).toBe(403)
|
|
427
|
-
expect(((await write.json()) as { error: { code: string } }).error.code).toBe('forbidden')
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
test('behavior: early-access owners pass the gate', async () => {
|
|
431
|
-
const db = TestApp.database()
|
|
432
|
-
const app = createApp({ db, tmp_earlyAccess: true })
|
|
433
|
-
await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
|
|
434
|
-
const { cookie, orgId } = await seeded(app)
|
|
435
|
-
const me = await app.request('/v1/me', { headers: { cookie } })
|
|
436
|
-
const { id: userId } = (await me.json()) as { id: string }
|
|
437
|
-
await Users.setEmail(db, userId, 'dev@example.org')
|
|
441
|
+
const app = createApp({ db })
|
|
442
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
438
443
|
|
|
439
|
-
// The unreachable Stripe upstream
|
|
444
|
+
// The unreachable Stripe upstream proves source access, not user early access, is the gate.
|
|
440
445
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
441
446
|
expect(response.status).toBe(502)
|
|
442
447
|
expect(((await response.json()) as { error: { code: string } }).error.code).toBe('upstream_error') // prettier-ignore
|
|
443
448
|
})
|
|
444
449
|
|
|
445
|
-
test('behavior: management
|
|
450
|
+
test('behavior: an enabled management key reaches Stripe', async () => {
|
|
451
|
+
const db = TestApp.database()
|
|
446
452
|
const store = TestApp.kvStore({ keys: [] })
|
|
447
|
-
const app = createApp({
|
|
448
|
-
const { orgId } = await
|
|
453
|
+
const app = createApp({ db, kv: { store } })
|
|
454
|
+
const { orgId } = await seededWithStripe(app, db)
|
|
449
455
|
const { token } = await ApiKeys.mint(store, { orgId, scopes: ['management:write'] })
|
|
450
456
|
|
|
451
457
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
|
|
@@ -455,9 +461,10 @@ describe('early access', () => {
|
|
|
455
461
|
expect(response.status).toBe(502)
|
|
456
462
|
})
|
|
457
463
|
|
|
458
|
-
test('behavior:
|
|
459
|
-
const
|
|
460
|
-
const {
|
|
464
|
+
test('behavior: an enabled super admin reaches Stripe', async () => {
|
|
465
|
+
const db = TestApp.database()
|
|
466
|
+
const app = createApp({ db })
|
|
467
|
+
const { orgId } = await seededWithStripe(app, db)
|
|
461
468
|
|
|
462
469
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
|
|
463
470
|
...as(secret),
|
|
@@ -551,6 +558,7 @@ describe('DELETE /orgs/:orgId', () => {
|
|
|
551
558
|
const db = TestApp.database()
|
|
552
559
|
const app = createApp({ db })
|
|
553
560
|
const { cookie, orgId } = await seeded(app)
|
|
561
|
+
await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' })
|
|
554
562
|
await StripeCustomers.create(db, { orgId, stripeCustomerId: 'cus_cascade' })
|
|
555
563
|
|
|
556
564
|
const response = await app.request(`/v1/orgs/${orgId}`, {
|
|
@@ -558,19 +566,30 @@ describe('DELETE /orgs/:orgId', () => {
|
|
|
558
566
|
method: 'DELETE',
|
|
559
567
|
})
|
|
560
568
|
expect(response.status).toBe(200)
|
|
569
|
+
expect(await EnabledBillingSources.listByOrg(db, orgId)).toEqual([])
|
|
561
570
|
expect(await StripeCustomers.get(db, orgId)).toBeUndefined()
|
|
562
571
|
})
|
|
563
572
|
})
|
|
564
573
|
|
|
565
574
|
describe('environment scoping', () => {
|
|
566
|
-
test('behavior: sandbox
|
|
575
|
+
test('behavior: sandbox billing state remains readable without a Stripe source', async () => {
|
|
567
576
|
const app = createApp()
|
|
568
577
|
const { cookie, orgId } = await seeded(app)
|
|
569
578
|
const response = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, {
|
|
570
579
|
headers: { cookie },
|
|
571
580
|
})
|
|
572
|
-
expect(response.status).toBe(
|
|
573
|
-
expect(
|
|
581
|
+
expect(response.status).toBe(200)
|
|
582
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
583
|
+
{
|
|
584
|
+
"enabledSources": [],
|
|
585
|
+
"spend": {
|
|
586
|
+
"amount": "0",
|
|
587
|
+
"currency": "usd",
|
|
588
|
+
"period": "month",
|
|
589
|
+
},
|
|
590
|
+
"status": "none",
|
|
591
|
+
}
|
|
592
|
+
`)
|
|
574
593
|
})
|
|
575
594
|
|
|
576
595
|
test('behavior: rejects an unknown environment', async () => {
|
|
@@ -756,7 +775,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
756
775
|
test('checkout provisions distinct customers per environment', { timeout: 30_000 }, async () => {
|
|
757
776
|
const db = TestApp.database()
|
|
758
777
|
const app = createApp({ billing: sandboxBillingBoth(), db })
|
|
759
|
-
const { cookie, orgId } = await
|
|
778
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
760
779
|
|
|
761
780
|
const prod = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
762
781
|
const sbx = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout?environment=sandbox`, post(cookie)) // prettier-ignore
|
|
@@ -783,7 +802,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
783
802
|
test('checkout provisions a real customer and setup session', { timeout: 30_000 }, async () => {
|
|
784
803
|
const db = TestApp.database()
|
|
785
804
|
const app = createApp({ billing: sandboxBilling(), db })
|
|
786
|
-
const { cookie, orgId } = await
|
|
805
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
787
806
|
|
|
788
807
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
789
808
|
// Track for teardown before asserting: a failed assertion must not leak
|
|
@@ -831,7 +850,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
831
850
|
// prettier-ignore
|
|
832
851
|
const db = TestApp.database()
|
|
833
852
|
const app = createApp({ billing: sandboxBilling(), db })
|
|
834
|
-
const { cookie, orgId } = await
|
|
853
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
835
854
|
const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
836
855
|
const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
|
|
837
856
|
if (customerId) TestStripe.track(customerId)
|
|
@@ -844,8 +863,9 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
844
863
|
invoice_settings: { default_payment_method: attached.paymentMethod },
|
|
845
864
|
})
|
|
846
865
|
await StripeCustomers.setStatus(db, orgId, 'active')
|
|
866
|
+
await EnabledBillingSources.remove(db, { orgId, source: 'stripe' })
|
|
847
867
|
|
|
848
|
-
//
|
|
868
|
+
// Disabling setup does not block management of the existing Stripe source.
|
|
849
869
|
const list = await app.request(`/v1/orgs/${orgId}/billing/payment-methods`, {
|
|
850
870
|
headers: { cookie },
|
|
851
871
|
})
|
|
@@ -900,7 +920,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
900
920
|
// prettier-ignore
|
|
901
921
|
const db = TestApp.database()
|
|
902
922
|
const app = createApp({ billing: sandboxBilling(), db })
|
|
903
|
-
const { cookie, orgId } = await
|
|
923
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
904
924
|
const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
905
925
|
const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
|
|
906
926
|
if (customerId) TestStripe.track(customerId)
|
|
@@ -930,11 +950,12 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
930
950
|
|
|
931
951
|
const db = TestApp.database()
|
|
932
952
|
const app = createApp({ billing: sandboxBilling(), db })
|
|
933
|
-
const { cookie, orgId } = await
|
|
953
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
934
954
|
const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
935
955
|
const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
|
|
936
956
|
if (customerId) TestStripe.track(customerId)
|
|
937
957
|
expect(checkout.status).toBe(200)
|
|
958
|
+
await EnabledBillingSources.remove(db, { orgId, source: 'stripe' })
|
|
938
959
|
|
|
939
960
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/manage`, post(cookie))
|
|
940
961
|
expect(response.status).toBe(200)
|
|
@@ -963,7 +984,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (live delivery)', () => {
|
|
|
963
984
|
})
|
|
964
985
|
|
|
965
986
|
test('attach → active, detach → none, delete → canceled', { timeout: 300_000 }, async () => {
|
|
966
|
-
const { cookie, orgId } = await
|
|
987
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
967
988
|
const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
968
989
|
const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
|
|
969
990
|
if (customerId) TestStripe.track(customerId)
|
|
@@ -6,6 +6,7 @@ import * as ApiKeys from '../../../ApiKeys.js'
|
|
|
6
6
|
import * as Auth from '../../../internal/Auth.js'
|
|
7
7
|
import * as BillingSettings from '../../../db/tables/billingSettings.js'
|
|
8
8
|
import * as Db from '../../../db/Db.js'
|
|
9
|
+
import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js'
|
|
9
10
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
10
11
|
import * as Response from '../../../internal/Response.js'
|
|
11
12
|
import * as Schema from '../../../internal/Schema.js'
|
|
@@ -113,6 +114,12 @@ export namespace schema {
|
|
|
113
114
|
/** An organization's billing state. */
|
|
114
115
|
export const Billing = Schema.describe(
|
|
115
116
|
z.object({
|
|
117
|
+
enabledSources: z
|
|
118
|
+
.array(z.enum(EnabledBillingSources.sources))
|
|
119
|
+
.check(
|
|
120
|
+
z.describe('Billing sources this organization may set up.'),
|
|
121
|
+
z.meta({ examples: [['stripe']] }),
|
|
122
|
+
),
|
|
116
123
|
spend: z
|
|
117
124
|
.optional(
|
|
118
125
|
z.object({
|
|
@@ -234,7 +241,7 @@ export namespace schema {
|
|
|
234
241
|
}
|
|
235
242
|
|
|
236
243
|
/**
|
|
237
|
-
* Mounts billing routes. Reads require membership or scoped API key access; organizations without
|
|
244
|
+
* Mounts billing routes. Reads require membership or scoped API key access; organizations without provider state return `none`.
|
|
238
245
|
*/
|
|
239
246
|
export function billing() {
|
|
240
247
|
return new Hono<Environment>()
|
|
@@ -255,7 +262,6 @@ export function billing() {
|
|
|
255
262
|
responses: OpenApi.responses({
|
|
256
263
|
errors: {
|
|
257
264
|
404: 'No accessible organization was found for this id.',
|
|
258
|
-
501: 'Billing is not configured on this deployment.',
|
|
259
265
|
},
|
|
260
266
|
success: { description: "The organization's billing state.", schema: schema.Billing },
|
|
261
267
|
}),
|
|
@@ -276,10 +282,7 @@ export function billing() {
|
|
|
276
282
|
message: 'Check the query parameters and try again.',
|
|
277
283
|
})
|
|
278
284
|
|
|
279
|
-
const billing = c.get('billing')
|
|
280
|
-
if (!billing) return billingUnconfigured(c)
|
|
281
285
|
const environment = c.req.valid('query').environment
|
|
282
|
-
if (!stripeFor(billing, environment)) return billingUnconfigured(c)
|
|
283
286
|
|
|
284
287
|
try {
|
|
285
288
|
return c.json(
|
|
@@ -313,7 +316,6 @@ export function billing() {
|
|
|
313
316
|
errors: {
|
|
314
317
|
403: 'Requires the owner role.',
|
|
315
318
|
404: 'No accessible organization was found for this id.',
|
|
316
|
-
501: 'Billing is not configured on this deployment.',
|
|
317
319
|
},
|
|
318
320
|
success: {
|
|
319
321
|
description: "The organization's updated billing state.",
|
|
@@ -342,10 +344,7 @@ export function billing() {
|
|
|
342
344
|
message: 'Check the request body and try again.',
|
|
343
345
|
})
|
|
344
346
|
|
|
345
|
-
const billing = c.get('billing')
|
|
346
|
-
if (!billing) return billingUnconfigured(c)
|
|
347
347
|
const environment = c.req.valid('query').environment
|
|
348
|
-
if (!stripeFor(billing, environment)) return billingUnconfigured(c)
|
|
349
348
|
|
|
350
349
|
const body = c.req.valid('json')
|
|
351
350
|
const org = Auth.org(c)
|
|
@@ -380,7 +379,7 @@ export function billing() {
|
|
|
380
379
|
)
|
|
381
380
|
.post(
|
|
382
381
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/checkout',
|
|
383
|
-
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true
|
|
382
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
384
383
|
Auth.ensureOrg({ role: 'owner' }),
|
|
385
384
|
OpenApi.validate('param', schema.Params, {
|
|
386
385
|
code: 'param_invalid',
|
|
@@ -395,8 +394,8 @@ export function billing() {
|
|
|
395
394
|
responses: OpenApi.responses({
|
|
396
395
|
errors: {
|
|
397
396
|
403: {
|
|
398
|
-
codes: ['api_key_forbidden', '
|
|
399
|
-
description: 'Requires the owner role and
|
|
397
|
+
codes: ['api_key_forbidden', 'billing_source_disabled', 'forbidden'],
|
|
398
|
+
description: 'Requires the owner role and Stripe billing enabled.',
|
|
400
399
|
},
|
|
401
400
|
404: 'No accessible organization was found for this id.',
|
|
402
401
|
501: 'Billing is not configured on this deployment.',
|
|
@@ -413,7 +412,6 @@ export function billing() {
|
|
|
413
412
|
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
414
413
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
415
414
|
if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c)
|
|
416
|
-
if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
|
|
417
415
|
if (OpenApi.narrowValidation)
|
|
418
416
|
return OpenApi.validationError(c, {
|
|
419
417
|
code: 'param_invalid',
|
|
@@ -425,16 +423,19 @@ export function billing() {
|
|
|
425
423
|
message: 'Check the query parameters and try again.',
|
|
426
424
|
})
|
|
427
425
|
|
|
428
|
-
const billing = c.get('billing')
|
|
429
|
-
if (!billing) return billingUnconfigured(c)
|
|
430
|
-
const environment = c.req.valid('query').environment
|
|
431
|
-
const source = stripeFor(billing, environment)
|
|
432
|
-
if (!source) return billingUnconfigured(c)
|
|
433
|
-
|
|
434
|
-
const { client } = source
|
|
435
426
|
const org = Auth.org(c)
|
|
436
427
|
const db = Db.get(c.get('db'))
|
|
437
428
|
try {
|
|
429
|
+
if (!(await EnabledBillingSources.isEnabled(db, { orgId: org.id, source: 'stripe' })))
|
|
430
|
+
return billingSourceDisabled(c)
|
|
431
|
+
|
|
432
|
+
const billing = c.get('billing')
|
|
433
|
+
if (!billing) return billingUnconfigured(c)
|
|
434
|
+
const environment = c.req.valid('query').environment
|
|
435
|
+
const source = stripeFor(billing, environment)
|
|
436
|
+
if (!source) return billingUnconfigured(c)
|
|
437
|
+
|
|
438
|
+
const { client } = source
|
|
438
439
|
const existing = await StripeCustomers.get(db, org.id, environment)
|
|
439
440
|
const customerId = existing
|
|
440
441
|
? existing.stripeCustomerId
|
|
@@ -885,7 +886,8 @@ async function billingBody(
|
|
|
885
886
|
environment: ResourceEnvironment,
|
|
886
887
|
) {
|
|
887
888
|
const db = Db.get(c.get('db'))
|
|
888
|
-
const [record, settings] = await Promise.all([
|
|
889
|
+
const [enabledSources, record, settings] = await Promise.all([
|
|
890
|
+
EnabledBillingSources.listByOrg(db, orgId),
|
|
889
891
|
StripeCustomers.get(db, orgId, environment),
|
|
890
892
|
BillingSettings.get(db, orgId, environment),
|
|
891
893
|
])
|
|
@@ -900,6 +902,7 @@ async function billingBody(
|
|
|
900
902
|
since: core_Billing.periodStart(period),
|
|
901
903
|
})
|
|
902
904
|
return {
|
|
905
|
+
enabledSources: enabledSources.map((record) => record.source),
|
|
903
906
|
spend: { amount: core_Billing.fromBaseUnits(spend), currency, period },
|
|
904
907
|
...(settings?.spendLimit != null
|
|
905
908
|
? { spendLimit: { amount: settings.spendLimit, currency, period } }
|
|
@@ -959,6 +962,14 @@ function billingNotFound(c: Context<Environment>) {
|
|
|
959
962
|
})
|
|
960
963
|
}
|
|
961
964
|
|
|
965
|
+
function billingSourceDisabled(c: Context<Environment>) {
|
|
966
|
+
return Response.error(c, {
|
|
967
|
+
code: 'billing_source_disabled',
|
|
968
|
+
message: 'Stripe billing is not enabled for this organization',
|
|
969
|
+
status: 403,
|
|
970
|
+
})
|
|
971
|
+
}
|
|
972
|
+
|
|
962
973
|
function billingUnconfigured(c: Context<Environment>) {
|
|
963
974
|
return Response.error(c, {
|
|
964
975
|
code: 'billing_unconfigured',
|
|
@@ -43,9 +43,17 @@ describe('faucet', () => {
|
|
|
43
43
|
expect(funded.status).toBe(200)
|
|
44
44
|
const result = await TestApp.json(funded, Faucet.schema.FundResponse)
|
|
45
45
|
expect(result.tokens.length).toBeGreaterThan(0)
|
|
46
|
-
expect(
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
expect(
|
|
47
|
+
result.tokens.every(
|
|
48
|
+
(entry) =>
|
|
49
|
+
entry.token.symbol.length > 0 &&
|
|
50
|
+
BigInt(entry.amount.baseUnits) > 0n &&
|
|
51
|
+
entry.amount.currency === entry.token.currency &&
|
|
52
|
+
entry.amount.decimals === entry.token.decimals &&
|
|
53
|
+
/^\d+(\.\d+)?$/.test(entry.amount.formatted),
|
|
54
|
+
),
|
|
55
|
+
).toBe(true)
|
|
56
|
+
expect(result.transactionHashes).toEqual(result.tokens.map((entry) => entry.transactionHash))
|
|
49
57
|
|
|
50
58
|
const repeated = await app.request(`/v1/orgs/${org.id}/faucet`, {
|
|
51
59
|
body: JSON.stringify({ address: recipient }),
|
|
@@ -6,6 +6,7 @@ import * as OpenApi from '../../../internal/OpenApi.js'
|
|
|
6
6
|
import * as RateLimit from '../../../internal/RateLimit.js'
|
|
7
7
|
import * as Response from '../../../internal/Response.js'
|
|
8
8
|
import * as Schema from '../../../internal/Schema.js'
|
|
9
|
+
import * as Value from '../../../internal/Value.js'
|
|
9
10
|
import * as Viem from '../../../internal/Viem.js'
|
|
10
11
|
import type { Environment } from '../App.js'
|
|
11
12
|
|
|
@@ -14,6 +15,37 @@ const userRateLimit = { perMinute: 3 } satisfies RateLimit.Limit
|
|
|
14
15
|
|
|
15
16
|
/** OpenAPI schemas owned by the sandbox faucet. */
|
|
16
17
|
export namespace schema {
|
|
18
|
+
const Token = Schema.describe(
|
|
19
|
+
z.object({
|
|
20
|
+
address: Schema.TokenAddress.check(z.describe('The test token contract that minted funds.')),
|
|
21
|
+
currency: z
|
|
22
|
+
.string()
|
|
23
|
+
.check(z.describe('The token’s currency label.'), z.meta({ examples: ['USD'] })),
|
|
24
|
+
decimals: z
|
|
25
|
+
.number()
|
|
26
|
+
.check(
|
|
27
|
+
z.int(),
|
|
28
|
+
z.nonnegative(),
|
|
29
|
+
z.describe('The decimal places used by the token.'),
|
|
30
|
+
z.meta({ examples: [6] }),
|
|
31
|
+
),
|
|
32
|
+
name: z
|
|
33
|
+
.string()
|
|
34
|
+
.check(z.describe('The token’s human-readable name.'), z.meta({ examples: ['Test USD'] })),
|
|
35
|
+
symbol: z.string().check(z.describe('The token’s symbol.'), z.meta({ examples: ['TUSD'] })),
|
|
36
|
+
}),
|
|
37
|
+
'A test token minted by the faucet.',
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const FundedToken = Schema.describe(
|
|
41
|
+
z.object({
|
|
42
|
+
amount: Schema.TokenAmount.check(z.describe('Amount minted by the faucet.')),
|
|
43
|
+
token: Token,
|
|
44
|
+
transactionHash: Schema.Hash.check(z.describe('The transaction that minted this token.')),
|
|
45
|
+
}),
|
|
46
|
+
'One token amount minted by the faucet.',
|
|
47
|
+
)
|
|
48
|
+
|
|
17
49
|
/** Faucet request body. */
|
|
18
50
|
export const FundBody = Schema.describe(
|
|
19
51
|
z.object({ address: Schema.Address }),
|
|
@@ -24,29 +56,7 @@ export namespace schema {
|
|
|
24
56
|
export const FundResponse = Schema.describe(
|
|
25
57
|
z.object({
|
|
26
58
|
tokens: z
|
|
27
|
-
.array(
|
|
28
|
-
z.object({
|
|
29
|
-
address: Schema.TokenAddress.check(
|
|
30
|
-
z.describe('The test token contract that minted funds.'),
|
|
31
|
-
),
|
|
32
|
-
amount: Schema.DecimalString.check(
|
|
33
|
-
z.describe('The minted amount in the token’s smallest units.'),
|
|
34
|
-
),
|
|
35
|
-
currency: z.string().check(z.describe('The token’s currency label.')),
|
|
36
|
-
decimals: z
|
|
37
|
-
.number()
|
|
38
|
-
.check(
|
|
39
|
-
z.int(),
|
|
40
|
-
z.nonnegative(),
|
|
41
|
-
z.describe('The number of decimal places the token uses.'),
|
|
42
|
-
),
|
|
43
|
-
name: z.string().check(z.describe('The token’s human-readable name.')),
|
|
44
|
-
symbol: z.string().check(z.describe('The token’s symbol.')),
|
|
45
|
-
transactionHash: Schema.Hash.check(
|
|
46
|
-
z.describe('The transaction that minted this token.'),
|
|
47
|
-
),
|
|
48
|
-
}),
|
|
49
|
-
)
|
|
59
|
+
.array(FundedToken)
|
|
50
60
|
.check(z.describe('The configured test tokens minted to the account.')),
|
|
51
61
|
transactionHashes: z
|
|
52
62
|
.array(Schema.Hash)
|
|
@@ -124,17 +134,23 @@ export function faucet() {
|
|
|
124
134
|
const event = client.token.mint.extractEvent(receipt.logs)
|
|
125
135
|
const metadata = await client.token.getMetadata({ token: event.address })
|
|
126
136
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
amount: Value.tokenAmount({
|
|
138
|
+
baseUnits: event.args.amount,
|
|
139
|
+
currency: metadata.currency,
|
|
140
|
+
decimals: metadata.decimals,
|
|
141
|
+
}),
|
|
142
|
+
token: {
|
|
143
|
+
address: event.address,
|
|
144
|
+
currency: metadata.currency,
|
|
145
|
+
decimals: metadata.decimals,
|
|
146
|
+
name: metadata.name,
|
|
147
|
+
symbol: metadata.symbol,
|
|
148
|
+
},
|
|
133
149
|
transactionHash: receipt.transactionHash,
|
|
134
150
|
}
|
|
135
151
|
}),
|
|
136
152
|
)
|
|
137
|
-
const transactionHashes = tokens.map((
|
|
153
|
+
const transactionHashes = tokens.map((entry) => entry.transactionHash)
|
|
138
154
|
return c.json(Response.validated(schema.FundResponse, { tokens, transactionHashes }), 200)
|
|
139
155
|
} catch (cause) {
|
|
140
156
|
return Response.upstream(c, cause)
|