@volter/twin-stripe 0.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -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 +73 -0
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +1065 -0
- package/dist/src/screens/checkout.d.ts +31 -0
- package/dist/src/screens/checkout.js +241 -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 +214 -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 +93 -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 +201 -0
- package/dist/src/semantics/checkout.d.ts +15 -0
- package/dist/src/semantics/checkout.js +303 -0
- package/dist/src/semantics/connect.d.ts +5 -0
- package/dist/src/semantics/connect.js +476 -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 +541 -0
- package/dist/src/semantics/issuing.d.ts +13 -0
- package/dist/src/semantics/issuing.js +570 -0
- package/dist/src/semantics/ledger.d.ts +54 -0
- package/dist/src/semantics/ledger.js +181 -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 +138 -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 +78 -0
- package/dist/src/semantics/shared.js +192 -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-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 +5052 -0
- package/dist/src/stripe-conformance.d.ts +41 -0
- package/dist/src/stripe-conformance.js +96 -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 +388 -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 +326 -0
- package/dist/src/stripe-shared.d.ts +106 -0
- package/dist/src/stripe-shared.js +273 -0
- package/dist/src/stripe-twin.d.ts +155 -0
- package/dist/src/stripe-twin.js +1226 -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 +10 -0
- package/dist/src/stripe-version.js +285 -0
- package/dist/test-fixtures/stripe-known-deviations.json +105 -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 +3740 -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 +31 -9
- package/src/manifest.ts +1097 -0
- package/src/screens/checkout.tsx +252 -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 +218 -0
- package/src/screens/public-details.tsx +105 -0
- package/src/semantics/after-payment.ts +113 -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 +211 -0
- package/src/semantics/checkout.ts +297 -0
- package/src/semantics/connect.ts +471 -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 +541 -0
- package/src/semantics/issuing.ts +585 -0
- package/src/semantics/ledger.ts +216 -0
- package/src/semantics/payment-intents.ts +420 -0
- package/src/semantics/payment-links.ts +148 -0
- package/src/semantics/payment-methods.ts +143 -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 +215 -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-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 +1456 -222
- package/src/stripe-conformance.ts +6 -5
- package/src/stripe-connector.ts +68 -40
- package/src/stripe-emit.ts +14 -7
- package/src/stripe-events.ts +94 -36
- 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 +263 -38
- package/src/stripe-shared.ts +294 -0
- package/src/stripe-twin.ts +429 -5325
- package/src/stripe-ui-conformance.ts +70 -107
- package/src/stripe-ui-structure.ts +124 -348
- package/src/stripe-version.ts +278 -0
- package/test-fixtures/stripe-known-deviations.json +2 -7
- package/test-fixtures/stripe-openapi-operations.json +1188 -2855
- package/src/stripe-form.ts +0 -35
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { asBool } from "../stripe-twin.js";
|
|
2
|
+
import { settleDisputeWon } from "./ledger.js";
|
|
3
|
+
import { at, fail } from "./shared.js";
|
|
4
|
+
const disputeMissing = (ctx, id) => fail(ctx, `No such dispute: '${id}'`, 404, 'resource_missing');
|
|
5
|
+
const retrieve = async (ctx) => {
|
|
6
|
+
const d = ctx.get('dispute', at(ctx, 'dispute'));
|
|
7
|
+
return d ? ctx.reply(d) : disputeMissing(ctx, at(ctx, 'dispute'));
|
|
8
|
+
};
|
|
9
|
+
const close = async (ctx) => {
|
|
10
|
+
const id = at(ctx, 'dispute');
|
|
11
|
+
const d = ctx.get('dispute', id);
|
|
12
|
+
if (!d)
|
|
13
|
+
return disputeMissing(ctx, id);
|
|
14
|
+
const refused = ctx.legal('dispute', 'status', 'PostDisputesDisputeClose', d.status, undefined, id);
|
|
15
|
+
if (refused)
|
|
16
|
+
return ctx.refuse(refused);
|
|
17
|
+
return ctx.reply(await ctx.write('dispute', id, { status: 'lost' }, 'dispute.close'));
|
|
18
|
+
};
|
|
19
|
+
/** Evidence staged without submitting it (submit=false), or on a dispute already under review or decided. */
|
|
20
|
+
async function keepEvidence(ctx, id, fields) {
|
|
21
|
+
return ctx.reply(await ctx.write('dispute', id, fields, 'dispute.update'));
|
|
22
|
+
}
|
|
23
|
+
const update = async (ctx) => {
|
|
24
|
+
const id = at(ctx, 'dispute');
|
|
25
|
+
const d = ctx.get('dispute', id);
|
|
26
|
+
if (!d)
|
|
27
|
+
return disputeMissing(ctx, id);
|
|
28
|
+
const params = ctx.params;
|
|
29
|
+
const given = params.evidence && typeof params.evidence === 'object' ? params.evidence : {};
|
|
30
|
+
const evidence = { ...(d.evidence ?? {}), ...given };
|
|
31
|
+
const fields = { evidence, ...(params.metadata && typeof params.metadata === 'object' ? { metadata: { ...(d.metadata ?? {}), ...params.metadata } } : {}) };
|
|
32
|
+
const submit = params.submit === undefined ? true : asBool(params.submit);
|
|
33
|
+
const review = d.status === 'warning_needs_response' ? 'warning_under_review' : 'under_review';
|
|
34
|
+
if (!submit || ctx.legal('dispute', 'status', 'PostDisputesDispute', d.status, review, id))
|
|
35
|
+
return keepEvidence(ctx, id, fields);
|
|
36
|
+
const details = d.evidence_details ?? {};
|
|
37
|
+
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');
|
|
38
|
+
const text = String(evidence.uncategorized_text ?? '');
|
|
39
|
+
const decided = review === 'under_review' ? (text.includes('winning_evidence') ? 'won' : text.includes('losing_evidence') ? 'lost' : undefined) : undefined;
|
|
40
|
+
if (decided && !ctx.legal('dispute', 'status', 'PostDisputesDispute', review, decided, id, 'vendor')) {
|
|
41
|
+
const bt = decided === 'won' ? await settleDisputeWon(ctx, id, Number(d.amount) || 0, String(d.currency ?? 'usd')) : undefined;
|
|
42
|
+
const kept = (ctx.get('dispute', id)?.balance_transactions ?? []);
|
|
43
|
+
body = await ctx.write('dispute', id, { status: decided, ...(bt ? { balance_transactions: [...kept, ctx.get('balance_transaction', bt) ?? bt] } : {}) }, 'charge.dispute.closed');
|
|
44
|
+
}
|
|
45
|
+
return ctx.reply(body);
|
|
46
|
+
};
|
|
47
|
+
export const disputes = {
|
|
48
|
+
GetDisputesDispute: retrieve,
|
|
49
|
+
PostDisputesDispute: update,
|
|
50
|
+
PostDisputesDisputeClose: close,
|
|
51
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { asBool, subscriptionItemPairs } from "../stripe-twin.js";
|
|
2
|
+
import { at, created, fail, list, newest, path, where, kept } from "./shared.js";
|
|
3
|
+
const FEAT = 'entitlements.feature';
|
|
4
|
+
/** A feature whose lookup_key an unarchived feature already has: "A unique key you provide as your own system identifier"
|
|
5
|
+
* (docs.stripe.com/api/entitlements/feature/create). */
|
|
6
|
+
function lookupKeyTaken(ctx, lookupKey) {
|
|
7
|
+
return fail(ctx, `A feature with lookup_key '${lookupKey}' already exists.`, 400, 'resource_already_exists');
|
|
8
|
+
}
|
|
9
|
+
const create = async (ctx) => {
|
|
10
|
+
const params = ctx.params;
|
|
11
|
+
if (typeof params.name !== 'string' || !params.name)
|
|
12
|
+
return fail(ctx, 'Missing required param: name.', 400, 'parameter_missing');
|
|
13
|
+
const lookupKey = typeof params.lookup_key === 'string' ? params.lookup_key : '';
|
|
14
|
+
if (!lookupKey)
|
|
15
|
+
return fail(ctx, 'Missing required param: lookup_key.', 400, 'parameter_missing');
|
|
16
|
+
if (ctx.rowsRaw(FEAT, { withDeleted: true }).some((f) => f.lookup_key === lookupKey && !f._archived))
|
|
17
|
+
return lookupKeyTaken(ctx, lookupKey);
|
|
18
|
+
return ctx.reply(await created(ctx, FEAT, {}, {
|
|
19
|
+
name: params.name, lookup_key: lookupKey, active: true, livemode: false,
|
|
20
|
+
metadata: params.metadata && typeof params.metadata === 'object' ? params.metadata : {},
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
const listFeatures = async (ctx) => list(ctx, FEAT, where(ctx, newest(ctx, FEAT), {
|
|
24
|
+
lookup_key: (f, v) => f.lookup_key === v,
|
|
25
|
+
archived: (f, v) => asBool(kept(ctx, FEAT, f, '_archived')) === asBool(v),
|
|
26
|
+
}));
|
|
27
|
+
// name, active and metadata change
|
|
28
|
+
const update = async (ctx) => {
|
|
29
|
+
const id = at(ctx, 'id');
|
|
30
|
+
if (!ctx.get(FEAT, id))
|
|
31
|
+
return ctx.notFound(FEAT, id);
|
|
32
|
+
const patch = {};
|
|
33
|
+
if (ctx.params.name !== undefined)
|
|
34
|
+
patch.name = ctx.params.name;
|
|
35
|
+
if (ctx.params.active !== undefined) {
|
|
36
|
+
patch.active = asBool(ctx.params.active);
|
|
37
|
+
patch._archived = undefined;
|
|
38
|
+
}
|
|
39
|
+
if (ctx.params.metadata !== undefined)
|
|
40
|
+
patch.metadata = ctx.params.metadata;
|
|
41
|
+
return ctx.reply(await ctx.write(FEAT, id, patch, 'entitlements_feature.update'));
|
|
42
|
+
};
|
|
43
|
+
// a customer is entitled to every feature granted to a product its live subscriptions bill, as the product stood when
|
|
44
|
+
// the subscription's current period began: "When a customer purchases or subscribes to a product with an attached
|
|
45
|
+
// feature, Stripe creates and manages an Active Entitlement for that customer and feature"; "Existing subscriptions will
|
|
46
|
+
// create active entitlements for any product feature changes at the start of the next billing period"
|
|
47
|
+
// (docs.stripe.com/billing/entitlements). Where the documentation stops and the twin decides: a feature removed from a
|
|
48
|
+
// product is kept, the same way, until the next period begins.
|
|
49
|
+
const activeEntitlements = async (ctx) => {
|
|
50
|
+
const customer = typeof ctx.params.customer === 'string' ? ctx.params.customer : '';
|
|
51
|
+
if (!customer)
|
|
52
|
+
return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
|
|
53
|
+
if (!ctx.row('customer', customer, { withDeleted: true }))
|
|
54
|
+
return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
|
|
55
|
+
// each product a live subscription bills, with the start of the period it is billed for
|
|
56
|
+
const productIds = new Map();
|
|
57
|
+
for (const sub of newest(ctx, 'subscription')) {
|
|
58
|
+
// a subscription entitles while it runs: not before its first payment (incomplete), nor once it has ended
|
|
59
|
+
// (docs.stripe.com/billing/entitlements; past_due keeps access during retries — that edge is the twin's reading)
|
|
60
|
+
if (sub.customer !== customer || !['active', 'trialing', 'past_due'].includes(String(sub.status)))
|
|
61
|
+
continue;
|
|
62
|
+
const periodStart = Number(sub.current_period_start ?? sub.start_date ?? sub.created ?? 0);
|
|
63
|
+
for (const it of subscriptionItemPairs(sub)) {
|
|
64
|
+
const price = ctx.get('price', it.price);
|
|
65
|
+
if (price && typeof price.product === 'string')
|
|
66
|
+
productIds.set(price.product, Math.max(productIds.get(price.product) ?? -Infinity, periodStart));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const data = [];
|
|
71
|
+
for (const pf of ctx.rowsRaw('product_feature', { withDeleted: true })) {
|
|
72
|
+
const periodStart = productIds.get(String(pf._product));
|
|
73
|
+
if (periodStart === undefined)
|
|
74
|
+
continue;
|
|
75
|
+
// attached by the period's start, and not removed before it
|
|
76
|
+
if (Number(pf._created ?? 0) > periodStart)
|
|
77
|
+
continue;
|
|
78
|
+
if (pf.deleted && !(Number(pf._removed_at) > periodStart))
|
|
79
|
+
continue;
|
|
80
|
+
const featId = String(pf._feature ?? pf.entitlement_feature?.id ?? pf.entitlement_feature);
|
|
81
|
+
if (seen.has(featId))
|
|
82
|
+
continue;
|
|
83
|
+
seen.add(featId);
|
|
84
|
+
const feat = ctx.get(FEAT, featId);
|
|
85
|
+
if (feat)
|
|
86
|
+
data.push({ id: `ent_twin_${customer}_${featId}`, object: 'entitlements.active_entitlement', feature: featId, lookup_key: feat.lookup_key, livemode: false });
|
|
87
|
+
}
|
|
88
|
+
return ctx.reply({ object: 'list', url: path(ctx), has_more: false, data });
|
|
89
|
+
};
|
|
90
|
+
export const entitlements = {
|
|
91
|
+
PostEntitlementsFeatures: create,
|
|
92
|
+
GetEntitlementsFeatures: listFeatures,
|
|
93
|
+
PostEntitlementsFeaturesId: update,
|
|
94
|
+
GetEntitlementsActiveEntitlements: activeEntitlements,
|
|
95
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { at, fail } from "./shared.js";
|
|
2
|
+
const create = async (ctx) => {
|
|
3
|
+
const version = ctx.call.request.headers.get('stripe-version');
|
|
4
|
+
if (!version)
|
|
5
|
+
return fail(ctx, 'You must pass the `Stripe-Version` header (e.g. via the apiVersion option) when creating an ephemeral key.', 400, 'parameter_missing');
|
|
6
|
+
const params = ctx.params;
|
|
7
|
+
const scopes = [];
|
|
8
|
+
if (typeof params.customer === 'string' && params.customer)
|
|
9
|
+
scopes.push(['customer', params.customer]);
|
|
10
|
+
if (typeof params.issuing_card === 'string' && params.issuing_card)
|
|
11
|
+
scopes.push(['issuing.card', params.issuing_card]);
|
|
12
|
+
if (typeof params.verification_session === 'string' && params.verification_session)
|
|
13
|
+
scopes.push(['identity.verification_session', params.verification_session]);
|
|
14
|
+
if (scopes.length !== 1)
|
|
15
|
+
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))
|
|
18
|
+
return fail(ctx, `No such customer: '${scopedId}'`, 400, 'resource_missing');
|
|
19
|
+
const id = ctx.mint('ephemeral_key');
|
|
20
|
+
const now = Number(ctx.now());
|
|
21
|
+
const associated_objects = [{ type: scope, id: scopedId }];
|
|
22
|
+
await ctx.write('ephemeral_key', id, { object: 'ephemeral_key', created: now, expires: now + 3600, livemode: false, associated_objects }, 'ephemeral_key.create');
|
|
23
|
+
return ctx.reply({ id, object: 'ephemeral_key', created: now, expires: now + 3600, livemode: false, secret: `ek_test_twin_${id}`, associated_objects });
|
|
24
|
+
};
|
|
25
|
+
const expire = async (ctx) => {
|
|
26
|
+
const id = at(ctx, 'key');
|
|
27
|
+
if (!ctx.get('ephemeral_key', id))
|
|
28
|
+
return fail(ctx, `No such ephemeral key: '${id}'`, 404, 'resource_missing');
|
|
29
|
+
return ctx.reply(await ctx.write('ephemeral_key', id, { expires: ctx.now() }, 'ephemeral_key.delete'));
|
|
30
|
+
};
|
|
31
|
+
export const ephemeralKeys = {
|
|
32
|
+
PostEphemeralKeys: create,
|
|
33
|
+
DeleteEphemeralKeysKey: expire,
|
|
34
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { asBool } from "../stripe-twin.js";
|
|
2
|
+
import { actingAccount } from "./ledger.js";
|
|
3
|
+
import { at, created, fail, filePurposeRefused, list, mergeUpdate, newest, where } from "./shared.js";
|
|
4
|
+
/** The type Stripe answers for an uploaded file: "The returned file type (for example, csv, pdf, jpg, or png)"
|
|
5
|
+
* (docs.stripe.com/api/files/object). Where the documentation stops and the twin decides: read from the name's
|
|
6
|
+
* extension (jpeg as jpg), and null for a name without one. */
|
|
7
|
+
function fileType(name) {
|
|
8
|
+
const ext = /\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase();
|
|
9
|
+
return ext ? (ext === 'jpeg' ? 'jpg' : ext) : null;
|
|
10
|
+
}
|
|
11
|
+
// multipart on Stripe; the file is described by the part uploaded: its name, its size in bytes and its type
|
|
12
|
+
const createFile = async (ctx) => {
|
|
13
|
+
const purpose = typeof ctx.params.purpose === 'string' ? ctx.params.purpose : '';
|
|
14
|
+
if (!purpose)
|
|
15
|
+
return fail(ctx, 'Missing required param: purpose.', 400, 'parameter_missing');
|
|
16
|
+
const part = ctx.params.file && typeof ctx.params.file === 'object' ? ctx.params.file : undefined;
|
|
17
|
+
const filename = typeof ctx.params.file === 'string' ? ctx.params.file : typeof part?.name === 'string' ? part.name : 'twin_upload';
|
|
18
|
+
const made = await created(ctx, 'file', { purpose }, {
|
|
19
|
+
filename, size: typeof part?.size === 'number' ? part.size : 0,
|
|
20
|
+
type: fileType(filename), title: null, expires_at: null, url: null,
|
|
21
|
+
links: { object: 'list', data: [], has_more: false, total_count: 0, url: '' },
|
|
22
|
+
});
|
|
23
|
+
await syncLinks(ctx, String(made.id));
|
|
24
|
+
return ctx.reply(ctx.get('file', String(made.id)));
|
|
25
|
+
};
|
|
26
|
+
/** A file's links, kept in step with the links made to it, at the address Stripe lists them
|
|
27
|
+
* (/v1/file_links?file=…, the File object's example). */
|
|
28
|
+
async function syncLinks(ctx, fileId) {
|
|
29
|
+
const data = newest(ctx, 'file_link').filter((l) => l.file === fileId);
|
|
30
|
+
await ctx.write('file', fileId, { links: { object: 'list', data, has_more: false, url: `/v1/file_links?file=${fileId}` } }, 'file.links_synced');
|
|
31
|
+
}
|
|
32
|
+
/** The purposes a file link may be made to: "The file's `purpose` must be one of the following: …" (the served
|
|
33
|
+
* spec, POST /v1/file_links `file`). */
|
|
34
|
+
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'];
|
|
35
|
+
// `file` is required; a link is kept, has its own public url, and is listed on its file
|
|
36
|
+
const createFileLink = async (ctx) => {
|
|
37
|
+
if (ctx.params.file === undefined || ctx.params.file === '')
|
|
38
|
+
return fail(ctx, 'Missing required param: file.', 400, 'parameter_missing');
|
|
39
|
+
const fileId = String(ctx.params.file);
|
|
40
|
+
const refused = filePurposeRefused(ctx, 'file', LINKABLE);
|
|
41
|
+
if (refused)
|
|
42
|
+
return refused;
|
|
43
|
+
const id = ctx.mint('file_link');
|
|
44
|
+
const link = await created(ctx, 'file_link', { ...ctx.params, id, file: fileId }, {
|
|
45
|
+
url: `https://files.twin.local/links/${id}`, expired: false, livemode: false, metadata: {},
|
|
46
|
+
expires_at: ctx.params.expires_at !== undefined ? Number(ctx.params.expires_at) : null,
|
|
47
|
+
});
|
|
48
|
+
await syncLinks(ctx, fileId);
|
|
49
|
+
return ctx.reply(link);
|
|
50
|
+
};
|
|
51
|
+
// the person verifies at the session's url, Stripe Identity's hosted page (../screens/identity.tsx)
|
|
52
|
+
const createVerificationSession = async (ctx) => {
|
|
53
|
+
const id = ctx.mint('identity.verification_session');
|
|
54
|
+
return ctx.reply(await created(ctx, 'identity.verification_session', { ...ctx.params, id }, {
|
|
55
|
+
status: 'requires_input', livemode: false, client_secret: `${id}_secret_twin`, url: `https://verify.stripe.com/start/${id}`,
|
|
56
|
+
last_error: null, last_verification_report: null, verified_outputs: null, return_url: ctx.params.return_url ?? null,
|
|
57
|
+
}));
|
|
58
|
+
};
|
|
59
|
+
// ── the customer portal ──
|
|
60
|
+
// the configuration defaults to the account's default one
|
|
61
|
+
const createPortalSession = async (ctx) => {
|
|
62
|
+
const customer = typeof ctx.params.customer === 'string' ? ctx.params.customer : '';
|
|
63
|
+
if (!customer)
|
|
64
|
+
return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
|
|
65
|
+
if (!ctx.row('customer', customer, { withDeleted: true }))
|
|
66
|
+
return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
|
|
67
|
+
const configuration = typeof ctx.params.configuration === 'string' && ctx.params.configuration
|
|
68
|
+
? ctx.params.configuration
|
|
69
|
+
: (ctx.rowsRaw('billing_portal.configuration', { withDeleted: true }).find((c) => c.is_default)?.id ?? 'bpc_twin_default');
|
|
70
|
+
const id = ctx.mint('billing_portal.session');
|
|
71
|
+
return ctx.reply(await created(ctx, 'billing_portal.session', {
|
|
72
|
+
id, customer, configuration, return_url: ctx.params.return_url ?? null, url: `https://billing.stripe.com/p/session/${id}`,
|
|
73
|
+
locale: ctx.params.locale ?? null, on_behalf_of: ctx.params.on_behalf_of ?? null, livemode: false,
|
|
74
|
+
// the account the session was made on (Stripe-Account), whose name the portal shows (screens/portal.tsx)
|
|
75
|
+
...(actingAccount(ctx) ? { _brand_account: actingAccount(ctx) } : {}),
|
|
76
|
+
}, {}));
|
|
77
|
+
};
|
|
78
|
+
const PBC = 'billing_portal.configuration';
|
|
79
|
+
/** A portal configuration's features, each filled where the request leaves it: every feature is answered (the served
|
|
80
|
+
* spec requires all five), as the create page's example answers a request that sets only customer_update and
|
|
81
|
+
* invoice_history: payment_method_update disabled, subscription_cancel disabled at_period_end with proration_behavior
|
|
82
|
+
* none and its five default reasons, subscription_update disabled with no allowed updates and proration_behavior none
|
|
83
|
+
* (docs.stripe.com/api/customer_portal/configurations/create); trial_update_behavior "Defaults to a value of
|
|
84
|
+
* `end_trial` if you don't set it during creation" (the served spec). Where the documentation stops and the twin
|
|
85
|
+
* decides: a feature the request leaves out is disabled, and schedule_at_period_end has no conditions. */
|
|
86
|
+
const PORTAL_FEATURES = {
|
|
87
|
+
customer_update: { allowed_updates: [], enabled: false },
|
|
88
|
+
invoice_history: { enabled: false },
|
|
89
|
+
payment_method_update: { enabled: false, payment_method_configuration: null },
|
|
90
|
+
subscription_cancel: { cancellation_reason: { enabled: false, options: ['too_expensive', 'missing_features', 'switched_service', 'unused', 'other'] }, enabled: false, mode: 'at_period_end', proration_behavior: 'none' },
|
|
91
|
+
subscription_update: { default_allowed_updates: [], enabled: false, proration_behavior: 'none', schedule_at_period_end: { conditions: [] }, trial_update_behavior: 'end_trial' },
|
|
92
|
+
};
|
|
93
|
+
const filled = (base, given) => {
|
|
94
|
+
if (!given || typeof given !== 'object' || Array.isArray(given) || !base || typeof base !== 'object' || Array.isArray(base))
|
|
95
|
+
return given === undefined ? base : given;
|
|
96
|
+
const out = { ...base };
|
|
97
|
+
for (const [k, v] of Object.entries(given))
|
|
98
|
+
out[k] = filled(base[k], v);
|
|
99
|
+
return out;
|
|
100
|
+
};
|
|
101
|
+
const createPortalConfiguration = async (ctx) => ctx.reply(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) }, {
|
|
102
|
+
active: true, is_default: false, livemode: false,
|
|
103
|
+
login_page: { enabled: false, url: null },
|
|
104
|
+
metadata: {}, default_return_url: ctx.params.default_return_url ?? null,
|
|
105
|
+
updated: ctx.now(),
|
|
106
|
+
}));
|
|
107
|
+
const listPortalConfigurations = async (ctx) => list(ctx, PBC, where(ctx, newest(ctx, PBC), {
|
|
108
|
+
active: (c, v) => asBool(c.active) === asBool(v),
|
|
109
|
+
is_default: (c, v) => asBool(c.is_default) === asBool(v),
|
|
110
|
+
}));
|
|
111
|
+
const updatePortalConfiguration = async (ctx) => {
|
|
112
|
+
const id = at(ctx, 'configuration');
|
|
113
|
+
if (!ctx.get(PBC, id))
|
|
114
|
+
return ctx.notFound(PBC, id);
|
|
115
|
+
return ctx.reply(await ctx.write(PBC, id, mergeUpdate(ctx, PBC, id, { ...ctx.params, updated: ctx.now() }), 'billing_portal.configuration.update'));
|
|
116
|
+
};
|
|
117
|
+
export const files = {
|
|
118
|
+
PostFiles: createFile,
|
|
119
|
+
PostFileLinks: createFileLink,
|
|
120
|
+
PostIdentityVerificationSessions: createVerificationSession,
|
|
121
|
+
PostBillingPortalSessions: createPortalSession,
|
|
122
|
+
PostBillingPortalConfigurations: createPortalConfiguration,
|
|
123
|
+
GetBillingPortalConfigurations: listPortalConfigurations,
|
|
124
|
+
PostBillingPortalConfigurationsConfiguration: updatePortalConfiguration,
|
|
125
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Semantics, SemanticsContext } from '@volter/world-core';
|
|
2
|
+
import { type Row } from './shared.js';
|
|
3
|
+
/** A draft invoice another object makes (an accepted quote): its customer's, with an invoice item per line the
|
|
4
|
+
* object gives, and the object named as its parent. Answers the invoice's id. */
|
|
5
|
+
export declare function draftInvoiceFor(ctx: SemanticsContext, customer: string, currency: string, parent: Row, lines: Array<{
|
|
6
|
+
price: unknown;
|
|
7
|
+
quantity: number;
|
|
8
|
+
amount: number;
|
|
9
|
+
description: unknown;
|
|
10
|
+
}>): Promise<string>;
|
|
11
|
+
/** What finalizing assigns: the invoice's `number`, the customer's invoice_prefix and a sequence
|
|
12
|
+
* (https://docs.stripe.com/api/invoices/object#invoice_object-number), and for send_invoice a
|
|
13
|
+
* `due_date` days_until_due after finalization (#invoice_object-due_date). */
|
|
14
|
+
/** A customer's invoice_prefix, "The prefix for the customer used to generate unique invoice numbers"
|
|
15
|
+
* (docs.stripe.com/api/customers/object): the twin's, from the customer's id. */
|
|
16
|
+
export declare const invoicePrefix: (customerId: string) => string;
|
|
17
|
+
export declare function finalizedFields(ctx: SemanticsContext, inv: Row, now: number): Row;
|
|
18
|
+
export declare const invoices: Record<string, Semantics>;
|