@volter/twin-stripe 0.1.2 → 2.0.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/README.md +96 -27
- package/client/dashboard-api.ts +286 -0
- package/client/stripe-mirror.css +272 -159
- package/client/stripe-mirror.tsx +1384 -541
- package/dist/client/dashboard-api.d.ts +107 -0
- package/dist/client/dashboard-api.js +238 -0
- package/dist/client/dashboard-api.ts +286 -0
- package/dist/client/stripe-mirror.bundle.js +236 -0
- package/dist/client/stripe-mirror.css +275 -0
- package/dist/client/stripe-mirror.d.ts +134 -0
- package/dist/client/stripe-mirror.js +823 -0
- package/dist/client/stripe-mirror.tsx +1534 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +39 -0
- package/dist/src/generated/events.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +75 -0
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +1070 -0
- package/dist/src/screens/checkout.d.ts +31 -0
- package/dist/src/screens/checkout.js +255 -0
- package/dist/src/screens/connect-oauth.d.ts +27 -0
- package/dist/src/screens/connect-oauth.js +414 -0
- package/dist/src/screens/connect-settings.d.ts +22 -0
- package/dist/src/screens/connect-settings.js +103 -0
- package/dist/src/screens/consent-skin.d.ts +4 -0
- package/dist/src/screens/consent-skin.js +18 -0
- package/dist/src/screens/financial-connections.d.ts +5 -0
- package/dist/src/screens/financial-connections.js +90 -0
- package/dist/src/screens/identity.d.ts +5 -0
- package/dist/src/screens/identity.js +86 -0
- package/dist/src/screens/industries.d.ts +1 -0
- package/dist/src/screens/industries.js +267 -0
- package/dist/src/screens/onboarding.d.ts +13 -0
- package/dist/src/screens/onboarding.js +225 -0
- package/dist/src/screens/portal.d.ts +5 -0
- package/dist/src/screens/portal.js +216 -0
- package/dist/src/screens/public-details.d.ts +5 -0
- package/dist/src/screens/public-details.js +90 -0
- package/dist/src/semantics/after-payment.d.ts +22 -0
- package/dist/src/semantics/after-payment.js +99 -0
- package/dist/src/semantics/apps-secrets.d.ts +2 -0
- package/dist/src/semantics/apps-secrets.js +54 -0
- package/dist/src/semantics/balance.d.ts +11 -0
- package/dist/src/semantics/balance.js +195 -0
- package/dist/src/semantics/billing.d.ts +2 -0
- package/dist/src/semantics/billing.js +220 -0
- package/dist/src/semantics/charges.d.ts +28 -0
- package/dist/src/semantics/charges.js +209 -0
- package/dist/src/semantics/checkout.d.ts +15 -0
- package/dist/src/semantics/checkout.js +316 -0
- package/dist/src/semantics/connect.d.ts +5 -0
- package/dist/src/semantics/connect.js +493 -0
- package/dist/src/semantics/coupons.d.ts +6 -0
- package/dist/src/semantics/coupons.js +92 -0
- package/dist/src/semantics/credit-notes.d.ts +2 -0
- package/dist/src/semantics/credit-notes.js +172 -0
- package/dist/src/semantics/customers.d.ts +6 -0
- package/dist/src/semantics/customers.js +429 -0
- package/dist/src/semantics/disputes.d.ts +2 -0
- package/dist/src/semantics/disputes.js +51 -0
- package/dist/src/semantics/entitlements.d.ts +2 -0
- package/dist/src/semantics/entitlements.js +95 -0
- package/dist/src/semantics/ephemeral-keys.d.ts +2 -0
- package/dist/src/semantics/ephemeral-keys.js +34 -0
- package/dist/src/semantics/files.d.ts +2 -0
- package/dist/src/semantics/files.js +125 -0
- package/dist/src/semantics/invoices.d.ts +18 -0
- package/dist/src/semantics/invoices.js +545 -0
- package/dist/src/semantics/issuing.d.ts +13 -0
- package/dist/src/semantics/issuing.js +575 -0
- package/dist/src/semantics/ledger.d.ts +59 -0
- package/dist/src/semantics/ledger.js +200 -0
- package/dist/src/semantics/payment-intents.d.ts +18 -0
- package/dist/src/semantics/payment-intents.js +404 -0
- package/dist/src/semantics/payment-links.d.ts +2 -0
- package/dist/src/semantics/payment-links.js +133 -0
- package/dist/src/semantics/payment-methods.d.ts +20 -0
- package/dist/src/semantics/payment-methods.js +140 -0
- package/dist/src/semantics/plans.d.ts +5 -0
- package/dist/src/semantics/plans.js +121 -0
- package/dist/src/semantics/platform.d.ts +9 -0
- package/dist/src/semantics/platform.js +206 -0
- package/dist/src/semantics/products.d.ts +2 -0
- package/dist/src/semantics/products.js +140 -0
- package/dist/src/semantics/radar.d.ts +2 -0
- package/dist/src/semantics/radar.js +83 -0
- package/dist/src/semantics/refunds.d.ts +9 -0
- package/dist/src/semantics/refunds.js +195 -0
- package/dist/src/semantics/renewals.d.ts +47 -0
- package/dist/src/semantics/renewals.js +251 -0
- package/dist/src/semantics/setup-intents.d.ts +2 -0
- package/dist/src/semantics/setup-intents.js +84 -0
- package/dist/src/semantics/shared.d.ts +82 -0
- package/dist/src/semantics/shared.js +203 -0
- package/dist/src/semantics/subscription-schedules.d.ts +2 -0
- package/dist/src/semantics/subscription-schedules.js +119 -0
- package/dist/src/semantics/subscriptions.d.ts +11 -0
- package/dist/src/semantics/subscriptions.js +605 -0
- package/dist/src/semantics/tax.d.ts +2 -0
- package/dist/src/semantics/tax.js +197 -0
- package/dist/src/semantics/terminal.d.ts +5 -0
- package/dist/src/semantics/terminal.js +182 -0
- package/dist/src/semantics/test-cards.d.ts +4 -0
- package/dist/src/semantics/test-cards.js +7 -0
- package/dist/src/semantics/test-clocks.d.ts +6 -0
- package/dist/src/semantics/test-clocks.js +73 -0
- package/dist/src/semantics/tokens.d.ts +4 -0
- package/dist/src/semantics/tokens.js +44 -0
- package/dist/src/semantics/transfers.d.ts +2 -0
- package/dist/src/semantics/transfers.js +154 -0
- package/dist/src/semantics/treasury.d.ts +2 -0
- package/dist/src/semantics/treasury.js +377 -0
- package/dist/src/semantics/webhook-endpoints.d.ts +3 -0
- package/dist/src/semantics/webhook-endpoints.js +85 -0
- package/dist/src/stripe-budget.d.ts +55 -0
- package/dist/src/stripe-budget.js +155 -0
- package/dist/src/stripe-capabilities.d.ts +3 -0
- package/dist/src/stripe-capabilities.js +5695 -0
- package/dist/src/stripe-conformance.d.ts +43 -0
- package/dist/src/stripe-conformance.js +105 -0
- package/dist/src/stripe-connector.d.ts +161 -0
- package/dist/src/stripe-connector.js +414 -0
- package/dist/src/stripe-emit.d.ts +2 -0
- package/dist/src/stripe-emit.js +145 -0
- package/dist/src/stripe-events.d.ts +93 -0
- package/dist/src/stripe-events.js +392 -0
- package/dist/src/stripe-js.d.ts +4 -0
- package/dist/src/stripe-js.js +70 -0
- package/dist/src/stripe-mirror-ui.d.ts +15 -0
- package/dist/src/stripe-mirror-ui.js +87 -0
- package/dist/src/stripe-params.d.ts +3 -0
- package/dist/src/stripe-params.js +43 -0
- package/dist/src/stripe-perform-harness.d.ts +9 -0
- package/dist/src/stripe-perform-harness.js +26 -0
- package/dist/src/stripe-server.d.ts +33 -0
- package/dist/src/stripe-server.js +393 -0
- package/dist/src/stripe-shared.d.ts +109 -0
- package/dist/src/stripe-shared.js +276 -0
- package/dist/src/stripe-twin.d.ts +155 -0
- package/dist/src/stripe-twin.js +1232 -0
- package/dist/src/stripe-ui-conformance.d.ts +5 -0
- package/dist/src/stripe-ui-conformance.js +79 -0
- package/dist/src/stripe-ui-structure.d.ts +3 -0
- package/dist/src/stripe-ui-structure.js +168 -0
- package/dist/src/stripe-version.d.ts +12 -0
- package/dist/src/stripe-version.js +287 -0
- package/dist/test-fixtures/stripe-known-deviations.json +110 -0
- package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +14 -0
- package/dist/test-fixtures/stripe-openapi-operations.json +4717 -0
- package/dist/test-fixtures/stripe-schemas.SOURCE.md +35 -0
- package/dist/test-fixtures/stripe-schemas.json +3813 -0
- package/package.json +18 -10
- package/src/cli.ts +7 -7
- package/src/generated/events.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/index.ts +34 -10
- package/src/manifest.ts +1102 -0
- package/src/screens/checkout.tsx +267 -0
- package/src/screens/connect-oauth.tsx +400 -0
- package/src/screens/connect-settings.tsx +121 -0
- package/src/screens/consent-skin.ts +20 -0
- package/src/screens/financial-connections.tsx +101 -0
- package/src/screens/identity.tsx +96 -0
- package/src/screens/industries.ts +267 -0
- package/src/screens/onboarding.tsx +243 -0
- package/src/screens/portal.tsx +220 -0
- package/src/screens/public-details.tsx +105 -0
- package/src/semantics/after-payment.ts +118 -0
- package/src/semantics/apps-secrets.ts +58 -0
- package/src/semantics/balance.ts +209 -0
- package/src/semantics/billing.ts +216 -0
- package/src/semantics/charges.ts +220 -0
- package/src/semantics/checkout.ts +310 -0
- package/src/semantics/connect.ts +487 -0
- package/src/semantics/coupons.ts +97 -0
- package/src/semantics/credit-notes.ts +168 -0
- package/src/semantics/customers.ts +432 -0
- package/src/semantics/disputes.ts +62 -0
- package/src/semantics/entitlements.ts +94 -0
- package/src/semantics/ephemeral-keys.ts +34 -0
- package/src/semantics/files.ts +143 -0
- package/src/semantics/invoices.ts +545 -0
- package/src/semantics/issuing.ts +590 -0
- package/src/semantics/ledger.ts +253 -0
- package/src/semantics/payment-intents.ts +420 -0
- package/src/semantics/payment-links.ts +148 -0
- package/src/semantics/payment-methods.ts +145 -0
- package/src/semantics/plans.ts +131 -0
- package/src/semantics/platform.ts +220 -0
- package/src/semantics/products.ts +154 -0
- package/src/semantics/radar.ts +85 -0
- package/src/semantics/refunds.ts +218 -0
- package/src/semantics/renewals.ts +274 -0
- package/src/semantics/setup-intents.ts +87 -0
- package/src/semantics/shared.ts +226 -0
- package/src/semantics/subscription-schedules.ts +129 -0
- package/src/semantics/subscriptions.ts +610 -0
- package/src/semantics/tax.ts +220 -0
- package/src/semantics/terminal.ts +195 -0
- package/src/semantics/test-cards.ts +7 -0
- package/src/semantics/test-clocks.ts +77 -0
- package/src/semantics/tokens.ts +52 -0
- package/src/semantics/transfers.ts +174 -0
- package/src/semantics/treasury.ts +383 -0
- package/src/semantics/webhook-endpoints.ts +87 -0
- package/src/stripe-budget.ts +4 -4
- package/src/stripe-capabilities.ts +2258 -380
- package/src/stripe-conformance.ts +19 -7
- package/src/stripe-connector.ts +68 -40
- package/src/stripe-emit.ts +15 -8
- package/src/stripe-events.ts +102 -40
- package/src/stripe-js.ts +70 -0
- package/src/stripe-mirror-ui.ts +28 -298
- package/src/stripe-params.ts +44 -0
- package/src/stripe-perform-harness.ts +29 -0
- package/src/stripe-server.ts +318 -38
- package/src/stripe-shared.ts +297 -0
- package/src/stripe-twin.ts +434 -5325
- package/src/stripe-ui-conformance.ts +70 -107
- package/src/stripe-ui-structure.ts +124 -348
- package/src/stripe-version.ts +281 -0
- package/test-fixtures/stripe-known-deviations.json +8 -8
- package/test-fixtures/stripe-openapi-operations.json +1188 -2855
- package/test-fixtures/stripe-schemas.json +85 -12
- package/src/stripe-form.ts +0 -35
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @volter/twin-stripe
|
|
2
2
|
|
|
3
3
|
The **Stripe twin** — a local, spec-correct replica of the Stripe REST API on the
|
|
4
|
-
shared [`@volter/
|
|
4
|
+
shared [`@volter/world-core`](../../world-core) kernel. The real `stripe` SDK works against it
|
|
5
5
|
unmodified; it's the QA stack's authoritative local Stripe (it replaced the old
|
|
6
6
|
hand-made in-process mock).
|
|
7
7
|
|
|
@@ -12,7 +12,18 @@ hand-made in-process mock).
|
|
|
12
12
|
payment_intents, setup_intents, payment_methods, products, prices, invoices,
|
|
13
13
|
invoiceitems, subscriptions, refunds, coupons, promotion_codes, identity
|
|
14
14
|
verification sessions, ephemeral_keys, checkout sessions, billing/customer-portal
|
|
15
|
-
sessions + configurations.
|
|
15
|
+
sessions + configurations, and the legacy **Plans** API (`semantics/plans.ts`: a plan is the recurring
|
|
16
|
+
price it is; plan writes send `plan.created`/`plan.updated`/`plan.deleted`; a deleted plan's price stays
|
|
17
|
+
readable, inactive, for its existing subscribers).
|
|
18
|
+
- **Hosted pages** (`src/screens/`): Stripe's own pages an application sends a person to, served at their
|
|
19
|
+
vendor urls — Checkout's payment page (`checkout.stripe.com/c/pay/{id}`: the customer pays, the session
|
|
20
|
+
completes and `checkout.session.completed` fires), the Customer Portal (`billing.stripe.com/p/session/{id}`),
|
|
21
|
+
Connect hosted onboarding (`connect.stripe.com/setup/…`), Identity (`verify.stripe.com/start/{id}`) and the
|
|
22
|
+
Financial Connections bank-linking flow Stripe.js opens.
|
|
23
|
+
- **Stripe.js** (`stripe-js.ts`): `js.stripe.com` serves the client library at `/v3`, `/v3/`, `/v3/stripe.js` and
|
|
24
|
+
`/<release train>/stripe.js` — the paths the `@stripe/stripe-js` loader loads or accepts — with
|
|
25
|
+
`redirectToCheckout({ sessionId })` sending the page to the hosted Checkout page; any other member throws,
|
|
26
|
+
naming itself (`stripe.js.loader`).
|
|
16
27
|
- **Writes** are local transactions (action log); reads are the projection (R3/R5/R18).
|
|
17
28
|
- **Card declines** (`stripe-twin.ts`): Stripe's documented test cards get vendor-faithful
|
|
18
29
|
outcomes on charge create / PaymentIntent confirm — `4242…4242` (and `pm_card_visa`/`tok_visa`)
|
|
@@ -20,10 +31,61 @@ hand-made in-process mock).
|
|
|
20
31
|
`pm_card_*`/`tok_*` tokens) return the real `card_error` envelope (HTTP 402) and leave a
|
|
21
32
|
confirmed PaymentIntent at `requires_payment_method`. Unknown cards succeed. See
|
|
22
33
|
`_card.declines-test-set-only` in `stripe-known-deviations.json`.
|
|
34
|
+
- **Manual capture** (`semantics/payment-intents.ts`, `semantics/charges.ts`): confirming a `capture_method=manual`
|
|
35
|
+
intent authorizes an uncaptured charge (`captured: false`, nothing on the balance); capture takes that charge
|
|
36
|
+
(`charge.captured`; a partial capture releases the rest with no Refund, Stripe's basil change), and an incremental
|
|
37
|
+
authorization grows its amount. An uncaptured charge is never refunded out of the balance: cancelling a
|
|
38
|
+
`requires_capture` intent releases the authorization (no Refund; `amount_captured` 0, `refunded` false) and its
|
|
39
|
+
charge's capture is then refused with `payment_intent_unexpected_state`, a refund of a PaymentIntent's uncaptured
|
|
40
|
+
charge is refused (cancel the intent, as Stripe's refunds guide says), and a `capture=false` charge's refund releases
|
|
41
|
+
it, after which its capture is refused with `charge_already_refunded`. An uncaptured charge is refused as a transfer's
|
|
42
|
+
`source_transaction` (the twin's rule; the docs are silent).
|
|
23
43
|
- **Idempotency keys**: the `Idempotency-Key` header is honored on POST — a replay with the
|
|
24
44
|
same key returns the stored response without re-applying the write (`_idempotency.stored-per-root`).
|
|
25
45
|
- **Events** (`stripe-events.ts`): fires Stripe `event` objects on write
|
|
26
|
-
(`payment_intent.succeeded`, `customer.subscription.created`, `invoice.paid`, …).
|
|
46
|
+
(`payment_intent.succeeded`, `customer.subscription.created`, `invoice.paid`, …). A charge fires the event
|
|
47
|
+
Stripe sends for it — `charge.succeeded` (made, or authorized under manual capture), `charge.failed` (a declined
|
|
48
|
+
attempt), `charge.captured` (an authorization captured) — never an invented `charge.created`.
|
|
49
|
+
- **Connect webhooks**: an event of a connected account (a write made with its `Stripe-Account` header, its own
|
|
50
|
+
`account.updated`, or a row kept on its books such as the automatic payout time makes for it) carries the
|
|
51
|
+
top-level `account` and reaches only endpoints created with `connect=true`; the platform's reach only the others
|
|
52
|
+
(`stripe.connect.webhook_scope`, `stripe.connect.event_scope`). `GET /v1/events` and `/v1/events/{id}` answer the
|
|
53
|
+
acting account's events: with the header that account's, without it the platform's. An event's id is minted once
|
|
54
|
+
per World (`evt_twin_<n>`) and is the delivered webhook's id and the stored event's alike (`stripe.events.ids`).
|
|
55
|
+
`world-stripe emit` re-fires a connected account's object with its `account`; it does not yet route it to Connect
|
|
56
|
+
endpoints only (the kernel's emit seam has no endpoint scope).
|
|
57
|
+
- **Connect OAuth (Standard accounts)** (`src/screens/connect-oauth.tsx`, `src/screens/connect-settings.tsx`;
|
|
58
|
+
docs.stripe.com/connect/oauth-reference). The platform's **client_id is `ca_twin_self`** in every World (a test
|
|
59
|
+
client_id; the platform account is `acct_twin_self`), shown on the Dashboard's Connect OAuth settings page,
|
|
60
|
+
`dashboard.stripe.com/settings/connect/onboarding-options/oauth` (also `/settings/connect` and the `/test/…` link),
|
|
61
|
+
in `<code data-testid="connect-client-id">`. OAuth starts **off**: a runner POSTs that page as a browser does,
|
|
62
|
+
`oauth_enabled=on&redirect_uris=<one per line>` (303 to `?saved=1`), and the saved URIs are listed in
|
|
63
|
+
`data-testid="connect-redirect-uri"` items. `GET connect.stripe.com/oauth/authorize` checks client_id (unknown:
|
|
64
|
+
`invalid_client`), OAuth on, `redirect_uri` (must exactly match a registered one; absent: the first; http and
|
|
65
|
+
localhost allowed, as Stripe's test client_id allows), `response_type=code` and `scope` (`read_write` | `read_only`,
|
|
66
|
+
default `read_only`), each refusal a 400 JSON `{error, error_description, state}` as the reference says (no
|
|
67
|
+
redirect). The page offers the test-mode **Skip this form**, which creates a new Standard account (controller type
|
|
68
|
+
`account`, prefilled from valid `stripe_user[...]` values, charges and payouts enabled) and redirects with
|
|
69
|
+
`scope`, `code` and `state`, and **Deny access**, which redirects with
|
|
70
|
+
`error=access_denied&error_description=The%20user%20denied%20your%20request&state=…`. `POST
|
|
71
|
+
connect.stripe.com/oauth/token` (the secret key as Bearer, Basic or `client_secret`) exchanges a code once, within 5
|
|
72
|
+
minutes, into `{access_token, livemode, refresh_token, scope, stripe_publishable_key, stripe_user_id, token_type}`;
|
|
73
|
+
a reused code is `invalid_grant` and revokes the connection; `refresh_token` grants an equal or lesser scope.
|
|
74
|
+
`POST connect.stripe.com/oauth/deauthorize` answers `{stripe_user_id}`, after which the Stripe-Account header (and
|
|
75
|
+
`/v1/accounts/{id}`) for that account is refused 403 `account_invalid`. `account.application.authorized` /
|
|
76
|
+
`.deauthorized` reach Connect endpoints with the account; once revoked, `GET /v1/accounts` no longer lists it and its own events (payouts, balance) stop reaching the platform. The token answer's
|
|
77
|
+
deprecated `access_token` and `stripe_publishable_key` act as the connected account (as the Stripe-Account header
|
|
78
|
+
does, as Bearer or Basic; `GET /v1/account` then answers it), a token replaced by a refresh or revoked is a 401 invalid API key, and it is never the platform's key at `/oauth/token` or `/oauth/deauthorize`. The
|
|
79
|
+
application's own setting of its client_id (Cal.com's `client_id` app key, `STRIPE_CLIENT_ID`) is the runner's to
|
|
80
|
+
set to `ca_twin_self`; the World does not provision it. Where the docs stop, the twin decides (the file header lists
|
|
81
|
+
each): connecting an existing Stripe account, the full account application form, and holding a `read_only`
|
|
82
|
+
connection to reads are not modelled (todos in `stripe-capabilities.ts`).
|
|
83
|
+
- **Signing secrets in a World**: Stripe mints an endpoint's `secret`; in a World the app's env is the World's, so an
|
|
84
|
+
endpoint is given the World's value, which the app already verifies with: `STRIPE_WEBHOOK_SECRET` (or
|
|
85
|
+
`STRIPE_WEBHOOK_SIGNING_SECRET`, `STRIPE_ENDPOINT_SECRET`, `STRIPE_WEBHOOK_SECRET_KEY`,
|
|
86
|
+
`NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET`), and for a `connect=true` endpoint `STRIPE_CONNECT_WEBHOOK_SECRET` (or
|
|
87
|
+
`STRIPE_WEBHOOK_SECRET_CONNECT`, `STRIPE_CONNECT_WEBHOOK_SIGNING_SECRET`) first. With none set the twin mints
|
|
88
|
+
`whsec_twin_<id>`. Where Stripe stops and the twin decides: Stripe lets no one choose a secret.
|
|
27
89
|
- **Conformance** (`stripe-conformance.ts`): field name + type checked vs Stripe's
|
|
28
90
|
real OpenAPI (standing gate).
|
|
29
91
|
- **UI mirror** (`stripe-mirror-ui.ts`): a Stripe-dashboard-style React app over the
|
|
@@ -42,10 +104,21 @@ Point the real `stripe` SDK at it with `{ host, port, protocol: 'http' }`.
|
|
|
42
104
|
|
|
43
105
|
## Interaction surfaces
|
|
44
106
|
|
|
45
|
-
1. **SDK/API** — *zero edits (preferred):* `STRIPE_TWIN_URL=http://127.0.0.1:PORT node --require @volter/
|
|
46
|
-
2. **API + CLI** — `world
|
|
107
|
+
1. **SDK/API** — *zero edits (preferred):* `STRIPE_TWIN_URL=http://127.0.0.1:PORT node --require @volter/world-core/inject your-app` redirects the real `stripe` SDK from `api.stripe.com` to the twin (`cookbook/zero-edit-inject`). For browser routing, follow [run a full stack](../../../docs/guides/run-a-full-stack.md). *Or* override directly: `new Stripe(key, { host: '127.0.0.1', port: PORT, protocol: 'http' })`.
|
|
108
|
+
2. **API + CLI** — run the twin in a World and inspect it with `volter world log` and `diff`. Use the [deployment guide](../../../docs/guides/deploy-from-a-shared-world.md) for changeset review and deployment.
|
|
47
109
|
3. **Read-only** — `world-stripe serve --read-only`: unlimited local reads, no rate limits; writes refuse like Stripe (4xx).
|
|
48
110
|
4. **UI mirror** — `world-stripe mirror` renders a Stripe-dashboard-style view of the twin's state.
|
|
111
|
+
5. **Time's events** — `POST /_twin/drain` sends what World time produced since the last drain: `balance.available`
|
|
112
|
+
to each account whose settled funds added to its balance (never for negative transactions alone), `payout.paid` for
|
|
113
|
+
each payout that arrived, a connected account's with its `account` to its Connect endpoints
|
|
114
|
+
(`stripe.events.time_drain`). A read sees time's moves without it; a runner's drainer calls it so the events
|
|
115
|
+
arrive, as the qstash and vercel twins' drain doors do.
|
|
116
|
+
Test mode keeps live timing except where Stripe documents otherwise: a card charge's funds are pending two days,
|
|
117
|
+
except the cards that bypass the pending balance (4000000000000077, 4000003720000278, `pm_card_bypassPending*`,
|
|
118
|
+
`tok_bypassPending*`, and a PaymentMethod saved from one) and US bank account debits ("Test transactions settle
|
|
119
|
+
instantly"), whose funds, and a `source_transaction` transfer's from them, are available at once; every credit
|
|
120
|
+
available at once is sent as `balance.available` at the next drain; a test payout is paid at its `arrival_date`
|
|
121
|
+
(`stripe.balance.test_mode_bypass_pending`; sources in `src/semantics/ledger.ts`).
|
|
49
122
|
|
|
50
123
|
(See Getting Started → "Twin interaction surfaces".)
|
|
51
124
|
|
|
@@ -53,9 +126,8 @@ Stable on the twin rubric: fidelity, read/write/fork, sync, observability, event
|
|
|
53
126
|
|
|
54
127
|
## Coverage
|
|
55
128
|
|
|
56
|
-
Goal: **honest, explicitly tracked coverage of Stripe's core feature surface.**
|
|
57
|
-
|
|
58
|
-
or carved out is a gap to close.
|
|
129
|
+
Goal: **honest, explicitly tracked coverage of Stripe's core feature surface.** Every capability
|
|
130
|
+
is either done or a tracked todo; anything not done is a gap to close.
|
|
59
131
|
|
|
60
132
|
**Done** — core resources: **customers**, **charges**, **payment_intents** (+ confirm),
|
|
61
133
|
**setup_intents** (+ confirm), **payment_methods** (+ detach), **subscriptions** (create/
|
|
@@ -68,7 +140,10 @@ line_items), **Customer Portal** (`billing_portal` sessions + configurations cre
|
|
|
68
140
|
list/update), **Connect** (connected **accounts** create/retrieve/list/update/delete +
|
|
69
141
|
`login_links`, and **transfers** platform→connected-account create/retrieve/list filtered by
|
|
70
142
|
destination; a new account starts un-onboarded with charges/payouts disabled + a `requirements`
|
|
71
|
-
hash
|
|
143
|
+
hash; a transfer's `source_transaction` waives the balance check only while its charge is unsettled,
|
|
144
|
+
takes the charge's `transfer_group` or writes `group_<payment intent>` onto both, and counts reversals
|
|
145
|
+
back as room; connected-account balances, payouts (manual and automatic) and payout reversal, top-ups,
|
|
146
|
+
account sessions, persons, external accounts, application fees, hosted onboarding and Connect OAuth for Standard accounts), plus **ephemeral_keys**, **identity verification_sessions**, **file_links**
|
|
72
147
|
(synthesized) and **coupons**/**promotion_codes** (retrieve/list, seed-only). Cursor pagination
|
|
73
148
|
(`limit`/`starting_after`/`ending_before` + `has_more`); per-resource list filters; `expand[]`
|
|
74
149
|
on the modeled paths; vendor-faithful **test-card declines** (`4242…` succeeds; documented
|
|
@@ -107,21 +182,15 @@ helpers) are tracked todos in `stripe-capabilities.ts`. **Terminal**
|
|
|
107
182
|
(locations/readers/connection tokens + process_payment_intent) is likewise modeled — the old
|
|
108
183
|
"Planned" listing for both families was stale.
|
|
109
184
|
|
|
110
|
-
**
|
|
111
|
-
|
|
112
|
-
credit grants
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
Stripe's, not an API object — the twin models the **Session/API object + redirect `url`**
|
|
120
|
-
instead (now **Done**; the page pixels stay out of scope). See
|
|
121
|
-
`_checkout.hosted-page-pixels` in `stripe-known-deviations.json`.
|
|
122
|
-
- Real **money settlement / bank movement** (proposed — pending owner approval): actually moving
|
|
123
|
-
funds is real-world infra, not the API — the payout/balance_transaction/balance *objects* and
|
|
124
|
-
their lifecycle are modeled.
|
|
185
|
+
Also modelled: **Treasury** financial accounts and money movement, **Climate** orders,
|
|
186
|
+
**Financial Connections** sessions/accounts/transactions, **Entitlements**, **Billing** meters,
|
|
187
|
+
credit grants and usage alerts, **Reporting** report runs, and the legacy **Sources** API.
|
|
188
|
+
|
|
189
|
+
**Planned** (known-missing, will do) — **Sigma scheduled query runs** (`GET /v1/sigma/scheduled_query_runs`
|
|
190
|
+
list + retrieve, with the run object's status/result-file lifecycle), Treasury financial addresses and
|
|
191
|
+
reversals, Financial Connections refresh, the Issuing gaps above, connector pulls of payment methods and
|
|
192
|
+
setup intents, and additional list endpoints/filters as needed. (See `stripe-capabilities.ts` for the full
|
|
193
|
+
honest todo list — every entry is a tracked gap.)
|
|
125
194
|
|
|
126
195
|
## Rate budget — the fail-closed backstop on live calls
|
|
127
196
|
|
|
@@ -136,9 +205,9 @@ validated by *method identity*, so a subclass or a `Proxy` that replaces `checkB
|
|
|
136
205
|
|
|
137
206
|
The declared numbers: **120 weighted units / 60s** = 2 requests/second — 2% of Stripe's documented 100/s live mode and 8% of the 25/s a sandbox key or any single endpoint gets. `POST`/`DELETE` cost 2 (not a published ratio — a judgement call about blast radius: a write creates a charge, refund or receipt e-mail and cannot be taken back) and `POST /v1/payouts` costs 5 (documented at 15 creates/second). Stripe's own window is a **second** while this one is a minute, so an intra-second burst reaches Stripe's limiter first; the 429 cooldown is the backstop for that shape.
|
|
138
207
|
|
|
139
|
-
The mechanism is **shared and vendor-agnostic** — it lives in the kernel (`@volter/
|
|
140
|
-
`
|
|
208
|
+
The mechanism is **shared and vendor-agnostic** — it lives in the kernel (`@volter/world-core` →
|
|
209
|
+
`packages/world-core/src/rateBudget.ts`); what lives here in [`src/stripe-budget.ts`](src/stripe-budget.ts) is this vendor's
|
|
141
210
|
**declaration** (window, ceiling, per-endpoint weights, and a `reason` citing the limits above) plus
|
|
142
211
|
the vendor-bound `StripeBudget`. The rule is ratified as
|
|
143
|
-
[
|
|
212
|
+
[../../../docs/contributing/architecture.md](../../../docs/contributing/architecture.md) **D8**, and the kernel module's header documents what the
|
|
144
213
|
guard does *not* guarantee — read that before trusting it.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// The Stripe Dashboard's reads and writes, through Stripe's own API on this origin (docs/contributing/
|
|
2
|
+
// architecture.md, "Screens": a workspace reads and writes only through the pack's operations). The calls
|
|
3
|
+
// the Dashboard makes for the pages it shows: the balance, payments (PaymentIntents and the Charges they
|
|
4
|
+
// made), refunds, customers and their saved payment methods, subscriptions, invoices, products and prices,
|
|
5
|
+
// and events for a detail page's timeline. The writes a person makes here: refund a payment
|
|
6
|
+
// (POST /v1/refunds), cancel a subscription (DELETE /v1/subscriptions/{id}, or schedule it with
|
|
7
|
+
// cancel_at_period_end), create a customer or a product. Bodies are form-encoded as Stripe's API takes them;
|
|
8
|
+
// an `error` envelope, never the HTTP status alone, is what refuses.
|
|
9
|
+
|
|
10
|
+
type Json = any;
|
|
11
|
+
export type StripeObject = Record<string, any>;
|
|
12
|
+
|
|
13
|
+
export function wireBaseFrom(hasBaseElement: boolean, baseURI: string): string {
|
|
14
|
+
return hasBaseElement ? new URL('.', baseURI).pathname.replace(/\/$/, '') : '';
|
|
15
|
+
}
|
|
16
|
+
// Served at the origin root the base is ''; served under a path prefix (an embedder's earlier <base>) every
|
|
17
|
+
// call resolves inside that prefix.
|
|
18
|
+
export const API_BASE = typeof document === 'undefined' ? '' : wireBaseFrom(document.querySelector('base[href]') !== null, document.baseURI);
|
|
19
|
+
|
|
20
|
+
export class StripeApiError extends Error {
|
|
21
|
+
constructor(message: string, readonly code?: string) { super(message); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function call(method: string, path: string, body?: Record<string, string | number | boolean | undefined>): Promise<Json> {
|
|
25
|
+
const init: RequestInit = { method, headers: { accept: 'application/json' } };
|
|
26
|
+
if (body) {
|
|
27
|
+
const form = Object.entries(body).filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&');
|
|
28
|
+
init.body = form;
|
|
29
|
+
(init.headers as Record<string, string>)['content-type'] = 'application/x-www-form-urlencoded';
|
|
30
|
+
}
|
|
31
|
+
const res = await fetch(`${API_BASE}${path}`, init);
|
|
32
|
+
const json = await res.json().catch(() => ({ error: { message: `${res.status} ${res.statusText}` } }));
|
|
33
|
+
if (json && json.error) throw new StripeApiError(String(json.error.message ?? 'Request failed'), json.error.code);
|
|
34
|
+
return json;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const retrieve = (path: string): Promise<StripeObject> => call('GET', path);
|
|
38
|
+
/** A list endpoint's `data`, newest first as Stripe returns it (one page of up to 100). */
|
|
39
|
+
export async function list(path: string): Promise<StripeObject[]> {
|
|
40
|
+
const sep = path.includes('?') ? '&' : '?';
|
|
41
|
+
const body = await call('GET', `${path}${sep}limit=100`);
|
|
42
|
+
return Array.isArray(body?.data) ? body.data : [];
|
|
43
|
+
}
|
|
44
|
+
const optional = <T>(p: Promise<T>, fallback: T): Promise<T> => p.catch(() => fallback);
|
|
45
|
+
|
|
46
|
+
// ── what a payment row is ────────────────────────────────────────────────────
|
|
47
|
+
/** One row of the Payments list: a PaymentIntent (or a Charge made without one) with what the Dashboard
|
|
48
|
+
* shows beside it — its charge, refunds, card and customer. */
|
|
49
|
+
export type Payment = {
|
|
50
|
+
id: string;
|
|
51
|
+
amount: number;
|
|
52
|
+
currency: string;
|
|
53
|
+
status: PaymentStatus;
|
|
54
|
+
description?: string;
|
|
55
|
+
created: number;
|
|
56
|
+
customer?: StripeObject;
|
|
57
|
+
charge?: StripeObject;
|
|
58
|
+
intent?: StripeObject;
|
|
59
|
+
paymentMethod?: StripeObject;
|
|
60
|
+
amountRefunded: number;
|
|
61
|
+
refundedAt?: number;
|
|
62
|
+
declineReason?: string;
|
|
63
|
+
declineMessage?: string;
|
|
64
|
+
};
|
|
65
|
+
export type PaymentStatus = 'succeeded' | 'refunded' | 'partially_refunded' | 'failed' | 'incomplete' | 'uncaptured' | 'canceled' | 'disputed' | 'processing';
|
|
66
|
+
|
|
67
|
+
export function paymentStatusOf(intent: StripeObject | undefined, charge: StripeObject | undefined): PaymentStatus {
|
|
68
|
+
if (charge?.disputed) return 'disputed';
|
|
69
|
+
if (charge?.refunded) return 'refunded';
|
|
70
|
+
if (charge && Number(charge.amount_refunded) > 0) return 'partially_refunded';
|
|
71
|
+
if (charge?.status === 'failed') return 'failed';
|
|
72
|
+
if (intent) {
|
|
73
|
+
if (intent.status === 'succeeded') return 'succeeded';
|
|
74
|
+
if (intent.status === 'canceled') return 'canceled';
|
|
75
|
+
if (intent.status === 'requires_capture') return 'uncaptured';
|
|
76
|
+
if (intent.status === 'processing') return 'processing';
|
|
77
|
+
if (intent.last_payment_error) return 'failed';
|
|
78
|
+
return 'incomplete';
|
|
79
|
+
}
|
|
80
|
+
return charge?.status === 'succeeded' ? 'succeeded' : charge?.status === 'pending' ? 'processing' : 'failed';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const DECLINE_REASONS: Record<string, string> = {
|
|
84
|
+
generic_decline: 'Generic decline', insufficient_funds: 'Insufficient funds', lost_card: 'Lost card', stolen_card: 'Stolen card',
|
|
85
|
+
expired_card: 'Expired card', incorrect_cvc: 'Incorrect CVC', processing_error: 'Processing error', card_declined: 'Card declined',
|
|
86
|
+
};
|
|
87
|
+
export const declineReasonText = (code: string | undefined): string | undefined => (code ? DECLINE_REASONS[code] ?? code.replace(/_/g, ' ') : undefined);
|
|
88
|
+
|
|
89
|
+
type Directory = { customers: Map<string, StripeObject>; paymentMethods: Map<string, StripeObject> };
|
|
90
|
+
|
|
91
|
+
async function paymentMethodsById(ids: Iterable<string>): Promise<Map<string, StripeObject>> {
|
|
92
|
+
const unique = [...new Set([...ids].filter((id) => typeof id === 'string' && id.startsWith('pm_')))];
|
|
93
|
+
const found = await Promise.all(unique.map((id) => optional(retrieve(`/v1/payment_methods/${id}`), undefined as StripeObject | undefined)));
|
|
94
|
+
return new Map(found.filter((pm): pm is StripeObject => Boolean(pm?.id)).map((pm) => [pm.id, pm]));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function paymentOf(intent: StripeObject | undefined, charge: StripeObject | undefined, refunds: StripeObject[], dir: Directory): Payment {
|
|
98
|
+
const base = intent ?? charge!;
|
|
99
|
+
const customerId = typeof base.customer === 'string' ? base.customer : base.customer?.id;
|
|
100
|
+
const pmId = typeof base.payment_method === 'string' ? base.payment_method : charge?.payment_method;
|
|
101
|
+
const error = intent?.last_payment_error;
|
|
102
|
+
const refundsOf = refunds.filter((r) => (intent && r.payment_intent === intent.id) || (charge && r.charge === charge.id));
|
|
103
|
+
return {
|
|
104
|
+
id: String(base.id),
|
|
105
|
+
amount: Number(base.amount) || 0,
|
|
106
|
+
currency: String(base.currency ?? 'usd'),
|
|
107
|
+
status: paymentStatusOf(intent, charge),
|
|
108
|
+
description: base.description ?? charge?.description ?? undefined,
|
|
109
|
+
created: Number(base.created) || 0,
|
|
110
|
+
customer: customerId ? dir.customers.get(customerId) ?? { id: customerId } : undefined,
|
|
111
|
+
charge, intent,
|
|
112
|
+
paymentMethod: pmId ? dir.paymentMethods.get(pmId) : undefined,
|
|
113
|
+
amountRefunded: Number(charge?.amount_refunded) || refundsOf.reduce((n, r) => n + (r.status === 'failed' ? 0 : Number(r.amount) || 0), 0),
|
|
114
|
+
refundedAt: refundsOf.length ? Math.max(...refundsOf.map((r) => Number(r.created) || 0)) : undefined,
|
|
115
|
+
declineReason: declineReasonText(error?.decline_code ?? error?.code ?? charge?.failure_code),
|
|
116
|
+
declineMessage: error?.message ?? charge?.failure_message ?? undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The Payments list: every PaymentIntent, and every Charge made without one. */
|
|
121
|
+
export async function loadPayments(): Promise<Payment[]> {
|
|
122
|
+
const [intents, charges, refunds, customers] = await Promise.all([
|
|
123
|
+
list('/v1/payment_intents'), list('/v1/charges'), optional(list('/v1/refunds'), []), list('/v1/customers'),
|
|
124
|
+
]);
|
|
125
|
+
const chargeById = new Map(charges.map((c) => [c.id, c]));
|
|
126
|
+
const pmIds = [...intents.map((i) => i.payment_method), ...charges.map((c) => c.payment_method)];
|
|
127
|
+
const dir: Directory = { customers: new Map(customers.map((c) => [c.id, c])), paymentMethods: await paymentMethodsById(pmIds) };
|
|
128
|
+
const rows = intents.map((pi) => {
|
|
129
|
+
const charge = (typeof pi.latest_charge === 'string' ? chargeById.get(pi.latest_charge) : pi.latest_charge) ?? charges.find((c) => c.payment_intent === pi.id);
|
|
130
|
+
return paymentOf(pi, charge, refunds, dir);
|
|
131
|
+
});
|
|
132
|
+
for (const ch of charges) if (!ch.payment_intent) rows.push(paymentOf(undefined, ch, refunds, dir));
|
|
133
|
+
return rows.sort((a, b) => b.created - a.created);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type PaymentDetail = Payment & { refunds: StripeObject[]; events: StripeObject[]; invoice?: StripeObject };
|
|
137
|
+
|
|
138
|
+
/** One payment's page: the intent (or charge), its charge, refunds, card, customer and the events about it. */
|
|
139
|
+
export async function loadPayment(id: string): Promise<PaymentDetail> {
|
|
140
|
+
let intent: StripeObject | undefined;
|
|
141
|
+
let charge: StripeObject | undefined;
|
|
142
|
+
if (id.startsWith('ch_') || id.startsWith('py_')) {
|
|
143
|
+
charge = await retrieve(`/v1/charges/${id}`);
|
|
144
|
+
if (typeof charge.payment_intent === 'string') intent = await optional(retrieve(`/v1/payment_intents/${charge.payment_intent}`), undefined);
|
|
145
|
+
} else {
|
|
146
|
+
intent = await retrieve(`/v1/payment_intents/${id}`);
|
|
147
|
+
const chargeId = typeof intent.latest_charge === 'string' ? intent.latest_charge : intent.latest_charge?.id;
|
|
148
|
+
if (chargeId) charge = await optional(retrieve(`/v1/charges/${chargeId}`), undefined);
|
|
149
|
+
}
|
|
150
|
+
const base = intent ?? charge!;
|
|
151
|
+
const customerId = typeof base.customer === 'string' ? base.customer : undefined;
|
|
152
|
+
const pmId = typeof base.payment_method === 'string' ? base.payment_method : charge?.payment_method;
|
|
153
|
+
const refundPath = intent ? `/v1/refunds?payment_intent=${intent.id}` : `/v1/refunds?charge=${charge!.id}`;
|
|
154
|
+
const [customer, pms, refunds, events, invoice] = await Promise.all([
|
|
155
|
+
customerId ? optional(retrieve(`/v1/customers/${customerId}`), { id: customerId }) : Promise.resolve(undefined),
|
|
156
|
+
paymentMethodsById(pmId ? [pmId] : []),
|
|
157
|
+
optional(list(refundPath), []),
|
|
158
|
+
optional(list('/v1/events'), []),
|
|
159
|
+
typeof base.invoice === 'string' ? optional(retrieve(`/v1/invoices/${base.invoice}`), undefined) : Promise.resolve(undefined),
|
|
160
|
+
]);
|
|
161
|
+
const dir: Directory = { customers: new Map(customer ? [[customer.id, customer]] : []), paymentMethods: pms };
|
|
162
|
+
const related = new Set<string>([base.id, charge?.id, intent?.id, ...refunds.map((r) => r.id)].filter(Boolean) as string[]);
|
|
163
|
+
return {
|
|
164
|
+
...paymentOf(intent, charge, refunds, dir),
|
|
165
|
+
refunds,
|
|
166
|
+
events: events.filter((e) => related.has(e?.data?.object?.id)).sort((a, b) => b.created - a.created),
|
|
167
|
+
invoice,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── customers ────────────────────────────────────────────────────────────────
|
|
172
|
+
export type CustomerRow = StripeObject & { defaultCard?: StripeObject; totalSpend: number; payments: number; refunds: number; currency: string };
|
|
173
|
+
|
|
174
|
+
export async function loadCustomers(): Promise<CustomerRow[]> {
|
|
175
|
+
const [customers, charges, intents] = await Promise.all([list('/v1/customers'), list('/v1/charges'), list('/v1/payment_intents')]);
|
|
176
|
+
const pmIds = customers.map((c) => c.invoice_settings?.default_payment_method).filter((x) => typeof x === 'string');
|
|
177
|
+
const pms = await paymentMethodsById(pmIds);
|
|
178
|
+
return customers.map((c) => {
|
|
179
|
+
const mine = charges.filter((ch) => ch.customer === c.id && ch.status === 'succeeded');
|
|
180
|
+
const pmId = c.invoice_settings?.default_payment_method;
|
|
181
|
+
return {
|
|
182
|
+
...c,
|
|
183
|
+
defaultCard: typeof pmId === 'string' ? pms.get(pmId) : undefined,
|
|
184
|
+
totalSpend: mine.reduce((n, ch) => n + (Number(ch.amount_captured ?? ch.amount) || 0) - (Number(ch.amount_refunded) || 0), 0),
|
|
185
|
+
payments: intents.filter((i) => i.customer === c.id && i.status === 'succeeded').length || mine.length,
|
|
186
|
+
refunds: mine.reduce((n, ch) => n + (Number(ch.amount_refunded) || 0), 0),
|
|
187
|
+
currency: String(mine[0]?.currency ?? c.currency ?? 'usd'),
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type CustomerDetail = {
|
|
193
|
+
customer: StripeObject;
|
|
194
|
+
paymentMethods: StripeObject[];
|
|
195
|
+
subscriptions: StripeObject[];
|
|
196
|
+
invoices: StripeObject[];
|
|
197
|
+
payments: Payment[];
|
|
198
|
+
products: Map<string, StripeObject>;
|
|
199
|
+
events: StripeObject[];
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export async function loadCustomer(id: string): Promise<CustomerDetail> {
|
|
203
|
+
const customer = await retrieve(`/v1/customers/${id}`);
|
|
204
|
+
const [paymentMethods, subscriptions, invoices, payments, products, events] = await Promise.all([
|
|
205
|
+
optional(list(`/v1/customers/${id}/payment_methods`), []),
|
|
206
|
+
optional(list(`/v1/subscriptions?customer=${id}&status=all`), []),
|
|
207
|
+
optional(list(`/v1/invoices?customer=${id}`), []),
|
|
208
|
+
loadPayments().then((all) => all.filter((p) => p.customer?.id === id)),
|
|
209
|
+
optional(list('/v1/products'), []),
|
|
210
|
+
optional(list('/v1/events'), []),
|
|
211
|
+
]);
|
|
212
|
+
const mine = new Set([id, ...subscriptions.map((s) => s.id), ...invoices.map((i) => i.id), ...payments.map((p) => p.id)]);
|
|
213
|
+
return {
|
|
214
|
+
customer, paymentMethods, subscriptions, invoices, payments,
|
|
215
|
+
products: new Map(products.map((p) => [p.id, p])),
|
|
216
|
+
events: events.filter((e) => mine.has(e?.data?.object?.id)).sort((a, b) => b.created - a.created),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── billing ──────────────────────────────────────────────────────────────────
|
|
221
|
+
export type Billing = { subscriptions: StripeObject[]; customers: Map<string, StripeObject>; products: Map<string, StripeObject> };
|
|
222
|
+
|
|
223
|
+
export async function loadSubscriptions(): Promise<Billing> {
|
|
224
|
+
const [subscriptions, customers, products] = await Promise.all([list('/v1/subscriptions?status=all'), list('/v1/customers'), list('/v1/products')]);
|
|
225
|
+
return { subscriptions, customers: new Map(customers.map((c) => [c.id, c])), products: new Map(products.map((p) => [p.id, p])) };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type SubscriptionDetail = { subscription: StripeObject; customer?: StripeObject; invoices: StripeObject[]; products: Map<string, StripeObject>; events: StripeObject[]; paymentMethod?: StripeObject };
|
|
229
|
+
|
|
230
|
+
export async function loadSubscription(id: string): Promise<SubscriptionDetail> {
|
|
231
|
+
const subscription = await retrieve(`/v1/subscriptions/${id}`);
|
|
232
|
+
const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer?.id;
|
|
233
|
+
const [customer, invoices, products, events] = await Promise.all([
|
|
234
|
+
customerId ? optional(retrieve(`/v1/customers/${customerId}`), undefined) : Promise.resolve(undefined),
|
|
235
|
+
optional(list(`/v1/invoices?subscription=${id}`), []),
|
|
236
|
+
optional(list('/v1/products'), []),
|
|
237
|
+
optional(list('/v1/events'), []),
|
|
238
|
+
]);
|
|
239
|
+
const pmId = subscription.default_payment_method ?? customer?.invoice_settings?.default_payment_method;
|
|
240
|
+
const pms = await paymentMethodsById(typeof pmId === 'string' ? [pmId] : []);
|
|
241
|
+
return {
|
|
242
|
+
subscription, customer, invoices, products: new Map(products.map((p) => [p.id, p])),
|
|
243
|
+
events: events.filter((e) => e?.data?.object?.id === id).sort((a, b) => b.created - a.created),
|
|
244
|
+
paymentMethod: typeof pmId === 'string' ? pms.get(pmId) : undefined,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function loadInvoices(): Promise<{ invoices: StripeObject[]; customers: Map<string, StripeObject> }> {
|
|
249
|
+
const [invoices, customers] = await Promise.all([list('/v1/invoices'), list('/v1/customers')]);
|
|
250
|
+
return { invoices, customers: new Map(customers.map((c) => [c.id, c])) };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function loadProducts(): Promise<{ products: StripeObject[]; prices: StripeObject[] }> {
|
|
254
|
+
const [products, prices] = await Promise.all([list('/v1/products'), list('/v1/prices')]);
|
|
255
|
+
return { products, prices };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export type HomeData = { balance: StripeObject; payments: Payment[]; customers: StripeObject[]; subscriptions: StripeObject[]; invoices: StripeObject[] };
|
|
259
|
+
export async function loadHome(): Promise<HomeData> {
|
|
260
|
+
const [balance, payments, customers, subscriptions, invoices] = await Promise.all([
|
|
261
|
+
retrieve('/v1/balance'), loadPayments(), list('/v1/customers'), optional(list('/v1/subscriptions?status=all'), []), optional(list('/v1/invoices'), []),
|
|
262
|
+
]);
|
|
263
|
+
return { balance, payments, customers, subscriptions, invoices };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function loadBalances(): Promise<{ balance: StripeObject; transactions: StripeObject[]; payouts: StripeObject[] }> {
|
|
267
|
+
const [balance, transactions, payouts] = await Promise.all([retrieve('/v1/balance'), optional(list('/v1/balance_transactions'), []), optional(list('/v1/payouts'), [])]);
|
|
268
|
+
return { balance, transactions, payouts };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function loadDevelopers(): Promise<{ events: StripeObject[]; webhooks: StripeObject[]; account?: StripeObject }> {
|
|
272
|
+
const [events, webhooks, account] = await Promise.all([optional(list('/v1/events'), []), optional(list('/v1/webhook_endpoints'), []), optional(retrieve('/v1/account'), undefined)]);
|
|
273
|
+
return { events, webhooks, account };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── writes ───────────────────────────────────────────────────────────────────
|
|
277
|
+
/** Refund a payment in full (no amount) or in part. */
|
|
278
|
+
export function createRefund(payment: { intent?: StripeObject; charge?: StripeObject }, amount: number | undefined, reason: string | undefined): Promise<StripeObject> {
|
|
279
|
+
const target = payment.intent ? { payment_intent: String(payment.intent.id) } : { charge: String(payment.charge?.id) };
|
|
280
|
+
return call('POST', '/v1/refunds', { ...target, amount, reason });
|
|
281
|
+
}
|
|
282
|
+
/** Cancel now (DELETE) or at the end of the current period (cancel_at_period_end). */
|
|
283
|
+
export function cancelSubscription(id: string, when: 'now' | 'period_end'): Promise<StripeObject> {
|
|
284
|
+
return when === 'now' ? call('DELETE', `/v1/subscriptions/${id}`) : call('POST', `/v1/subscriptions/${id}`, { cancel_at_period_end: true });
|
|
285
|
+
}
|
|
286
|
+
export const createObject = (collection: string, fields: Record<string, string>): Promise<StripeObject> => call('POST', `/v1/${collection}`, fields);
|