@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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Dispute semantics. A dispute is Stripe's to open, when a charge's cardholder disputes it: the test cards
|
|
2
|
+
// that are disputed open one as their charge succeeds (./after-payment.ts). It is then listed, retrieved,
|
|
3
|
+
// updated with evidence and closed. Closing concedes it (status → lost), the machine's move in
|
|
4
|
+
// ../manifest.ts. Evidence is submitted with the update unless `submit` is false
|
|
5
|
+
// (docs.stripe.com/api/disputes/update#update_dispute-submit), and the bank then decides it: in test mode, evidence whose
|
|
6
|
+
// uncategorized_text is `winning_evidence` wins it and `losing_evidence` loses it (docs.stripe.com/testing#evidence);
|
|
7
|
+
// a won dispute returns the disputed amount. Where the documentation stops and the twin decides: the bank decides at
|
|
8
|
+
// once, and any other evidence leaves the dispute under review. List is the derived core's.
|
|
9
|
+
import type { Semantics, SemanticsContext } from '@volter/world-core';
|
|
10
|
+
import { asBool } from '../stripe-twin.ts';
|
|
11
|
+
import { settleDisputeWon } from './ledger.ts';
|
|
12
|
+
import { at, fail, type Row } from './shared.ts';
|
|
13
|
+
|
|
14
|
+
const disputeMissing = (ctx: SemanticsContext, id: string): Response => fail(ctx, `No such dispute: '${id}'`, 404, 'resource_missing');
|
|
15
|
+
|
|
16
|
+
const retrieve: Semantics = async (ctx) => {
|
|
17
|
+
const d = ctx.get('dispute', at(ctx, 'dispute'));
|
|
18
|
+
return d ? ctx.reply(d) : disputeMissing(ctx, at(ctx, 'dispute'));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const close: Semantics = async (ctx) => {
|
|
22
|
+
const id = at(ctx, 'dispute');
|
|
23
|
+
const d = ctx.get('dispute', id);
|
|
24
|
+
if (!d) return disputeMissing(ctx, id);
|
|
25
|
+
const refused = ctx.legal('dispute', 'status', 'PostDisputesDisputeClose', d.status, undefined, id);
|
|
26
|
+
if (refused) return ctx.refuse(refused);
|
|
27
|
+
return ctx.reply(await ctx.write('dispute', id, { status: 'lost' }, 'dispute.close'));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Evidence staged without submitting it (submit=false), or on a dispute already under review or decided. */
|
|
31
|
+
async function keepEvidence(ctx: SemanticsContext, id: string, fields: Row): Promise<Response> {
|
|
32
|
+
return ctx.reply(await ctx.write('dispute', id, fields, 'dispute.update'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const update: Semantics = async (ctx) => {
|
|
36
|
+
const id = at(ctx, 'dispute');
|
|
37
|
+
const d = ctx.get('dispute', id);
|
|
38
|
+
if (!d) return disputeMissing(ctx, id);
|
|
39
|
+
const params = ctx.params;
|
|
40
|
+
const given = params.evidence && typeof params.evidence === 'object' ? (params.evidence as Row) : {};
|
|
41
|
+
const evidence = { ...((d.evidence as Row | undefined) ?? {}), ...given };
|
|
42
|
+
const fields: Row = { evidence, ...(params.metadata && typeof params.metadata === 'object' ? { metadata: { ...((d.metadata as Row | undefined) ?? {}), ...(params.metadata as Row) } } : {}) };
|
|
43
|
+
const submit = params.submit === undefined ? true : asBool(params.submit);
|
|
44
|
+
const review = d.status === 'warning_needs_response' ? 'warning_under_review' : 'under_review';
|
|
45
|
+
if (!submit || ctx.legal('dispute', 'status', 'PostDisputesDispute', d.status, review, id)) return keepEvidence(ctx, id, fields);
|
|
46
|
+
const details = (d.evidence_details as Row | undefined) ?? {};
|
|
47
|
+
let body = await ctx.write('dispute', id, { ...fields, status: review, evidence_details: { ...details, has_evidence: true, submission_count: (Number(details.submission_count) || 0) + 1 } }, 'charge.dispute.updated');
|
|
48
|
+
const text = String(evidence.uncategorized_text ?? '');
|
|
49
|
+
const decided = review === 'under_review' ? (text.includes('winning_evidence') ? 'won' : text.includes('losing_evidence') ? 'lost' : undefined) : undefined;
|
|
50
|
+
if (decided && !ctx.legal('dispute', 'status', 'PostDisputesDispute', review, decided, id, 'vendor')) {
|
|
51
|
+
const bt = decided === 'won' ? await settleDisputeWon(ctx, id, Number(d.amount) || 0, String(d.currency ?? 'usd')) : undefined;
|
|
52
|
+
const kept = ((ctx.get('dispute', id)?.balance_transactions as unknown[] | undefined) ?? []);
|
|
53
|
+
body = await ctx.write('dispute', id, { status: decided, ...(bt ? { balance_transactions: [...kept, ctx.get('balance_transaction', bt) ?? bt] } : {}) }, 'charge.dispute.closed');
|
|
54
|
+
}
|
|
55
|
+
return ctx.reply(body);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const disputes: Record<string, Semantics> = {
|
|
59
|
+
GetDisputesDispute: retrieve,
|
|
60
|
+
PostDisputesDispute: update,
|
|
61
|
+
PostDisputesDisputeClose: close,
|
|
62
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Entitlements semantics: features (unique by lookup_key) that products grant, and the features a
|
|
2
|
+
// customer is entitled to now, derived from its live subscriptions' products. Feature retrieve is
|
|
3
|
+
// the derived core's.
|
|
4
|
+
import type { Semantics, SemanticsContext } from '@volter/world-core';
|
|
5
|
+
import { asBool, subscriptionItemPairs } from '../stripe-twin.ts';
|
|
6
|
+
import { at, created, fail, list, newest, path, where, type Row, kept } from './shared.ts';
|
|
7
|
+
|
|
8
|
+
const FEAT = 'entitlements.feature';
|
|
9
|
+
|
|
10
|
+
/** A feature whose lookup_key an unarchived feature already has: "A unique key you provide as your own system identifier"
|
|
11
|
+
* (docs.stripe.com/api/entitlements/feature/create). */
|
|
12
|
+
function lookupKeyTaken(ctx: SemanticsContext, lookupKey: string): Response {
|
|
13
|
+
return fail(ctx, `A feature with lookup_key '${lookupKey}' already exists.`, 400, 'resource_already_exists');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const create: Semantics = async (ctx) => {
|
|
17
|
+
const params = ctx.params;
|
|
18
|
+
if (typeof params.name !== 'string' || !params.name) return fail(ctx, 'Missing required param: name.', 400, 'parameter_missing');
|
|
19
|
+
const lookupKey = typeof params.lookup_key === 'string' ? params.lookup_key : '';
|
|
20
|
+
if (!lookupKey) return fail(ctx, 'Missing required param: lookup_key.', 400, 'parameter_missing');
|
|
21
|
+
if (ctx.rowsRaw(FEAT, { withDeleted: true }).some((f) => f.lookup_key === lookupKey && !f._archived)) return lookupKeyTaken(ctx, lookupKey);
|
|
22
|
+
return ctx.reply(
|
|
23
|
+
await created(ctx, FEAT, {}, {
|
|
24
|
+
name: params.name, lookup_key: lookupKey, active: true, livemode: false,
|
|
25
|
+
metadata: params.metadata && typeof params.metadata === 'object' ? params.metadata : {},
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const listFeatures: Semantics = async (ctx) =>
|
|
31
|
+
list(ctx, FEAT, where(ctx, newest(ctx, FEAT), {
|
|
32
|
+
lookup_key: (f, v) => f.lookup_key === v,
|
|
33
|
+
archived: (f, v) => asBool(kept(ctx, FEAT, f, '_archived')) === asBool(v),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// name, active and metadata change
|
|
37
|
+
const update: Semantics = async (ctx) => {
|
|
38
|
+
const id = at(ctx, 'id');
|
|
39
|
+
if (!ctx.get(FEAT, id)) return ctx.notFound(FEAT, id);
|
|
40
|
+
const patch: Row = {};
|
|
41
|
+
if (ctx.params.name !== undefined) patch.name = ctx.params.name;
|
|
42
|
+
if (ctx.params.active !== undefined) {
|
|
43
|
+
patch.active = asBool(ctx.params.active);
|
|
44
|
+
patch._archived = undefined;
|
|
45
|
+
}
|
|
46
|
+
if (ctx.params.metadata !== undefined) patch.metadata = ctx.params.metadata;
|
|
47
|
+
return ctx.reply(await ctx.write(FEAT, id, patch, 'entitlements_feature.update'));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// a customer is entitled to every feature granted to a product its live subscriptions bill, as the product stood when
|
|
51
|
+
// the subscription's current period began: "When a customer purchases or subscribes to a product with an attached
|
|
52
|
+
// feature, Stripe creates and manages an Active Entitlement for that customer and feature"; "Existing subscriptions will
|
|
53
|
+
// create active entitlements for any product feature changes at the start of the next billing period"
|
|
54
|
+
// (docs.stripe.com/billing/entitlements). Where the documentation stops and the twin decides: a feature removed from a
|
|
55
|
+
// product is kept, the same way, until the next period begins.
|
|
56
|
+
const activeEntitlements: Semantics = async (ctx) => {
|
|
57
|
+
const customer = typeof ctx.params.customer === 'string' ? ctx.params.customer : '';
|
|
58
|
+
if (!customer) return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
|
|
59
|
+
if (!ctx.row('customer', customer, { withDeleted: true })) return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
|
|
60
|
+
// each product a live subscription bills, with the start of the period it is billed for
|
|
61
|
+
const productIds = new Map<string, number>();
|
|
62
|
+
for (const sub of newest(ctx, 'subscription')) {
|
|
63
|
+
// a subscription entitles while it runs: not before its first payment (incomplete), nor once it has ended
|
|
64
|
+
// (docs.stripe.com/billing/entitlements; past_due keeps access during retries — that edge is the twin's reading)
|
|
65
|
+
if (sub.customer !== customer || !['active', 'trialing', 'past_due'].includes(String(sub.status))) continue;
|
|
66
|
+
const periodStart = Number(sub.current_period_start ?? sub.start_date ?? sub.created ?? 0);
|
|
67
|
+
for (const it of subscriptionItemPairs(sub)) {
|
|
68
|
+
const price = ctx.get('price', it.price);
|
|
69
|
+
if (price && typeof price.product === 'string') productIds.set(price.product, Math.max(productIds.get(price.product) ?? -Infinity, periodStart));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const seen = new Set<string>();
|
|
73
|
+
const data: Row[] = [];
|
|
74
|
+
for (const pf of ctx.rowsRaw('product_feature', { withDeleted: true })) {
|
|
75
|
+
const periodStart = productIds.get(String(pf._product));
|
|
76
|
+
if (periodStart === undefined) continue;
|
|
77
|
+
// attached by the period's start, and not removed before it
|
|
78
|
+
if (Number(pf._created ?? 0) > periodStart) continue;
|
|
79
|
+
if (pf.deleted && !(Number(pf._removed_at) > periodStart)) continue;
|
|
80
|
+
const featId = String(pf._feature ?? (pf.entitlement_feature as Row | undefined)?.id ?? pf.entitlement_feature);
|
|
81
|
+
if (seen.has(featId)) continue;
|
|
82
|
+
seen.add(featId);
|
|
83
|
+
const feat = ctx.get(FEAT, featId);
|
|
84
|
+
if (feat) data.push({ id: `ent_twin_${customer}_${featId}`, object: 'entitlements.active_entitlement', feature: featId, lookup_key: feat.lookup_key, livemode: false });
|
|
85
|
+
}
|
|
86
|
+
return ctx.reply({ object: 'list', url: path(ctx), has_more: false, data });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const entitlements: Record<string, Semantics> = {
|
|
90
|
+
PostEntitlementsFeatures: create,
|
|
91
|
+
GetEntitlementsFeatures: listFeatures,
|
|
92
|
+
PostEntitlementsFeaturesId: update,
|
|
93
|
+
GetEntitlementsActiveEntitlements: activeEntitlements,
|
|
94
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Ephemeral key semantics: the short-lived key the mobile SDKs use, scoped to exactly one object.
|
|
2
|
+
// It needs the Stripe-Version header; its secret is answered only by the create; deleting it
|
|
3
|
+
// expires it early.
|
|
4
|
+
import type { Semantics } from '@volter/world-core';
|
|
5
|
+
import { at, fail } from './shared.ts';
|
|
6
|
+
|
|
7
|
+
const create: Semantics = async (ctx) => {
|
|
8
|
+
const version = ctx.call.request.headers.get('stripe-version');
|
|
9
|
+
if (!version) return fail(ctx, 'You must pass the `Stripe-Version` header (e.g. via the apiVersion option) when creating an ephemeral key.', 400, 'parameter_missing');
|
|
10
|
+
const params = ctx.params;
|
|
11
|
+
const scopes: Array<[string, string]> = [];
|
|
12
|
+
if (typeof params.customer === 'string' && params.customer) scopes.push(['customer', params.customer]);
|
|
13
|
+
if (typeof params.issuing_card === 'string' && params.issuing_card) scopes.push(['issuing.card', params.issuing_card]);
|
|
14
|
+
if (typeof params.verification_session === 'string' && params.verification_session) scopes.push(['identity.verification_session', params.verification_session]);
|
|
15
|
+
if (scopes.length !== 1) return fail(ctx, 'You must pass exactly one of `customer`, `issuing_card`, or `verification_session`.', 400, 'parameter_missing');
|
|
16
|
+
const [scope, scopedId] = scopes[0]!;
|
|
17
|
+
if (scope === 'customer' && !ctx.get('customer', scopedId)) return fail(ctx, `No such customer: '${scopedId}'`, 400, 'resource_missing');
|
|
18
|
+
const id = ctx.mint('ephemeral_key');
|
|
19
|
+
const now = Number(ctx.now());
|
|
20
|
+
const associated_objects = [{ type: scope, id: scopedId }];
|
|
21
|
+
await ctx.write('ephemeral_key', id, { object: 'ephemeral_key', created: now, expires: now + 3600, livemode: false, associated_objects }, 'ephemeral_key.create');
|
|
22
|
+
return ctx.reply({ id, object: 'ephemeral_key', created: now, expires: now + 3600, livemode: false, secret: `ek_test_twin_${id}`, associated_objects });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const expire: Semantics = async (ctx) => {
|
|
26
|
+
const id = at(ctx, 'key');
|
|
27
|
+
if (!ctx.get('ephemeral_key', id)) return fail(ctx, `No such ephemeral key: '${id}'`, 404, 'resource_missing');
|
|
28
|
+
return ctx.reply(await ctx.write('ephemeral_key', id, { expires: ctx.now() }, 'ephemeral_key.delete'));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const ephemeralKeys: Record<string, Semantics> = {
|
|
32
|
+
PostEphemeralKeys: create,
|
|
33
|
+
DeleteEphemeralKeysKey: expire,
|
|
34
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// File, file link, Identity and billing-portal semantics. A file is the uploaded document's object
|
|
2
|
+
// (the twin keeps no bytes); a file link is answered, not kept. An Identity verification session is
|
|
3
|
+
// created here and verified through the twin's own helper outside Stripe's surface; its reports
|
|
4
|
+
// are the derived core's to list and read, as are file list and retrieve, session retrieve and
|
|
5
|
+
// portal-configuration retrieve.
|
|
6
|
+
import type { Semantics, SemanticsContext } from '@volter/world-core';
|
|
7
|
+
import { asBool } from '../stripe-twin.ts';
|
|
8
|
+
import { actingAccount } from './ledger.ts';
|
|
9
|
+
import { at, created, fail, filePurposeRefused, list, mergeUpdate, newest, where, type Row } from './shared.ts';
|
|
10
|
+
|
|
11
|
+
/** The type Stripe answers for an uploaded file: "The returned file type (for example, csv, pdf, jpg, or png)"
|
|
12
|
+
* (docs.stripe.com/api/files/object). Where the documentation stops and the twin decides: read from the name's
|
|
13
|
+
* extension (jpeg as jpg), and null for a name without one. */
|
|
14
|
+
function fileType(name: string): string | null {
|
|
15
|
+
const ext = /\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase();
|
|
16
|
+
return ext ? (ext === 'jpeg' ? 'jpg' : ext) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// multipart on Stripe; the file is described by the part uploaded: its name, its size in bytes and its type
|
|
20
|
+
const createFile: Semantics = async (ctx) => {
|
|
21
|
+
const purpose = typeof ctx.params.purpose === 'string' ? ctx.params.purpose : '';
|
|
22
|
+
if (!purpose) return fail(ctx, 'Missing required param: purpose.', 400, 'parameter_missing');
|
|
23
|
+
const part = ctx.params.file && typeof ctx.params.file === 'object' ? (ctx.params.file as { name?: unknown; type?: unknown; size?: unknown }) : undefined;
|
|
24
|
+
const filename = typeof ctx.params.file === 'string' ? ctx.params.file : typeof part?.name === 'string' ? part.name : 'twin_upload';
|
|
25
|
+
const made = await created(ctx, 'file', { purpose }, {
|
|
26
|
+
filename, size: typeof part?.size === 'number' ? part.size : 0,
|
|
27
|
+
type: fileType(filename), title: null, expires_at: null, url: null,
|
|
28
|
+
links: { object: 'list', data: [], has_more: false, total_count: 0, url: '' },
|
|
29
|
+
});
|
|
30
|
+
await syncLinks(ctx, String(made.id));
|
|
31
|
+
return ctx.reply(ctx.get('file', String(made.id))!);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** A file's links, kept in step with the links made to it, at the address Stripe lists them
|
|
35
|
+
* (/v1/file_links?file=…, the File object's example). */
|
|
36
|
+
async function syncLinks(ctx: SemanticsContext, fileId: string): Promise<void> {
|
|
37
|
+
const data = newest(ctx, 'file_link').filter((l) => l.file === fileId);
|
|
38
|
+
await ctx.write('file', fileId, { links: { object: 'list', data, has_more: false, url: `/v1/file_links?file=${fileId}` } }, 'file.links_synced');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The purposes a file link may be made to: "The file's `purpose` must be one of the following: …" (the served
|
|
42
|
+
* spec, POST /v1/file_links `file`). */
|
|
43
|
+
const LINKABLE = ['business_icon', 'business_logo', 'customer_signature', 'dispute_evidence', 'finance_report_run', 'financial_account_statement', 'identity_document_downloadable', 'issuing_regulatory_reporting', 'pci_document', 'selfie', 'sigma_scheduled_query', 'tax_document_user_upload', 'terminal_android_apk', 'terminal_reader_splashscreen'] as const;
|
|
44
|
+
|
|
45
|
+
// `file` is required; a link is kept, has its own public url, and is listed on its file
|
|
46
|
+
const createFileLink: Semantics = async (ctx) => {
|
|
47
|
+
if (ctx.params.file === undefined || ctx.params.file === '') return fail(ctx, 'Missing required param: file.', 400, 'parameter_missing');
|
|
48
|
+
const fileId = String(ctx.params.file);
|
|
49
|
+
const refused = filePurposeRefused(ctx, 'file', LINKABLE);
|
|
50
|
+
if (refused) return refused;
|
|
51
|
+
const id = ctx.mint('file_link');
|
|
52
|
+
const link = await created(ctx, 'file_link', { ...ctx.params, id, file: fileId }, {
|
|
53
|
+
url: `https://files.twin.local/links/${id}`, expired: false, livemode: false, metadata: {},
|
|
54
|
+
expires_at: ctx.params.expires_at !== undefined ? Number(ctx.params.expires_at) : null,
|
|
55
|
+
});
|
|
56
|
+
await syncLinks(ctx, fileId);
|
|
57
|
+
return ctx.reply(link);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// the person verifies at the session's url, Stripe Identity's hosted page (../screens/identity.tsx)
|
|
61
|
+
const createVerificationSession: Semantics = async (ctx) => {
|
|
62
|
+
const id = ctx.mint('identity.verification_session');
|
|
63
|
+
return ctx.reply(await created(ctx, 'identity.verification_session', { ...ctx.params, id }, {
|
|
64
|
+
status: 'requires_input', livemode: false, client_secret: `${id}_secret_twin`, url: `https://verify.stripe.com/start/${id}`,
|
|
65
|
+
last_error: null, last_verification_report: null, verified_outputs: null, return_url: ctx.params.return_url ?? null,
|
|
66
|
+
}));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ── the customer portal ──
|
|
70
|
+
|
|
71
|
+
// the configuration defaults to the account's default one
|
|
72
|
+
const createPortalSession: Semantics = async (ctx) => {
|
|
73
|
+
const customer = typeof ctx.params.customer === 'string' ? ctx.params.customer : '';
|
|
74
|
+
if (!customer) return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
|
|
75
|
+
if (!ctx.row('customer', customer, { withDeleted: true })) return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
|
|
76
|
+
const configuration = typeof ctx.params.configuration === 'string' && ctx.params.configuration
|
|
77
|
+
? ctx.params.configuration
|
|
78
|
+
: (ctx.rowsRaw('billing_portal.configuration', { withDeleted: true }).find((c) => c.is_default)?.id ?? 'bpc_twin_default');
|
|
79
|
+
const id = ctx.mint('billing_portal.session');
|
|
80
|
+
return ctx.reply(
|
|
81
|
+
await created(ctx, 'billing_portal.session', {
|
|
82
|
+
id, customer, configuration, return_url: ctx.params.return_url ?? null, url: `https://billing.stripe.com/p/session/${id}`,
|
|
83
|
+
locale: ctx.params.locale ?? null, on_behalf_of: ctx.params.on_behalf_of ?? null, livemode: false,
|
|
84
|
+
// the account the session was made on (Stripe-Account), whose name the portal shows (screens/portal.tsx)
|
|
85
|
+
...(actingAccount(ctx) ? { _brand_account: actingAccount(ctx) } : {}),
|
|
86
|
+
}, {}),
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const PBC = 'billing_portal.configuration';
|
|
91
|
+
|
|
92
|
+
/** A portal configuration's features, each filled where the request leaves it: every feature is answered (the served
|
|
93
|
+
* spec requires all five), as the create page's example answers a request that sets only customer_update and
|
|
94
|
+
* invoice_history: payment_method_update disabled, subscription_cancel disabled at_period_end with proration_behavior
|
|
95
|
+
* none and its five default reasons, subscription_update disabled with no allowed updates and proration_behavior none
|
|
96
|
+
* (docs.stripe.com/api/customer_portal/configurations/create); trial_update_behavior "Defaults to a value of
|
|
97
|
+
* `end_trial` if you don't set it during creation" (the served spec). Where the documentation stops and the twin
|
|
98
|
+
* decides: a feature the request leaves out is disabled, and schedule_at_period_end has no conditions. */
|
|
99
|
+
const PORTAL_FEATURES: Row = {
|
|
100
|
+
customer_update: { allowed_updates: [], enabled: false },
|
|
101
|
+
invoice_history: { enabled: false },
|
|
102
|
+
payment_method_update: { enabled: false, payment_method_configuration: null },
|
|
103
|
+
subscription_cancel: { cancellation_reason: { enabled: false, options: ['too_expensive', 'missing_features', 'switched_service', 'unused', 'other'] }, enabled: false, mode: 'at_period_end', proration_behavior: 'none' },
|
|
104
|
+
subscription_update: { default_allowed_updates: [], enabled: false, proration_behavior: 'none', schedule_at_period_end: { conditions: [] }, trial_update_behavior: 'end_trial' },
|
|
105
|
+
};
|
|
106
|
+
const filled = (base: unknown, given: unknown): unknown => {
|
|
107
|
+
if (!given || typeof given !== 'object' || Array.isArray(given) || !base || typeof base !== 'object' || Array.isArray(base)) return given === undefined ? base : given;
|
|
108
|
+
const out: Row = { ...(base as Row) };
|
|
109
|
+
for (const [k, v] of Object.entries(given as Row)) out[k] = filled((base as Row)[k], v);
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const createPortalConfiguration: Semantics = async (ctx) =>
|
|
114
|
+
ctx.reply(
|
|
115
|
+
await created(ctx, PBC, { ...ctx.params, business_profile: filled({ headline: null, privacy_policy_url: null, terms_of_service_url: null }, ctx.params.business_profile), features: filled(PORTAL_FEATURES, ctx.params.features) }, {
|
|
116
|
+
active: true, is_default: false, livemode: false,
|
|
117
|
+
login_page: { enabled: false, url: null },
|
|
118
|
+
metadata: {}, default_return_url: ctx.params.default_return_url ?? null,
|
|
119
|
+
updated: ctx.now(),
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const listPortalConfigurations: Semantics = async (ctx) =>
|
|
124
|
+
list(ctx, PBC, where(ctx, newest(ctx, PBC), {
|
|
125
|
+
active: (c, v) => asBool(c.active) === asBool(v),
|
|
126
|
+
is_default: (c, v) => asBool(c.is_default) === asBool(v),
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
const updatePortalConfiguration: Semantics = async (ctx) => {
|
|
130
|
+
const id = at(ctx, 'configuration');
|
|
131
|
+
if (!ctx.get(PBC, id)) return ctx.notFound(PBC, id);
|
|
132
|
+
return ctx.reply(await ctx.write(PBC, id, mergeUpdate(ctx, PBC, id, { ...ctx.params, updated: ctx.now() }), 'billing_portal.configuration.update'));
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const files: Record<string, Semantics> = {
|
|
136
|
+
PostFiles: createFile,
|
|
137
|
+
PostFileLinks: createFileLink,
|
|
138
|
+
PostIdentityVerificationSessions: createVerificationSession,
|
|
139
|
+
PostBillingPortalSessions: createPortalSession,
|
|
140
|
+
PostBillingPortalConfigurations: createPortalConfiguration,
|
|
141
|
+
GetBillingPortalConfigurations: listPortalConfigurations,
|
|
142
|
+
PostBillingPortalConfigurationsConfiguration: updatePortalConfiguration,
|
|
143
|
+
};
|