@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,140 @@
|
|
|
1
|
+
import { asBool } from "../stripe-twin.js";
|
|
2
|
+
import { at, created, fail, kept, list, newest, where } from "./shared.js";
|
|
3
|
+
const productMissing = (ctx, id) => fail(ctx, `No such product: '${id}'`, 404, 'resource_missing');
|
|
4
|
+
const createProduct = async (ctx) => ctx.reply(await created(ctx, 'product', ctx.params, { active: true, livemode: false, images: [], marketing_features: [], metadata: {}, updated: ctx.now() }));
|
|
5
|
+
const listProducts = async (ctx) => list(ctx, 'product', where(ctx, newest(ctx, 'product'), {
|
|
6
|
+
active: (p, v) => asBool(p.active) === asBool(v),
|
|
7
|
+
ids: (p, v) => (Array.isArray(v) ? v.map(String).includes(String(p.id)) : String(p.id) === String(v)),
|
|
8
|
+
}));
|
|
9
|
+
// a product with prices cannot be deleted; Stripe says to deactivate it instead
|
|
10
|
+
/** A product deleted while it has prices: "Deleting a product is only possible if it has no prices associated with it"
|
|
11
|
+
* (docs.stripe.com/api/products/delete). */
|
|
12
|
+
function pricedProductRefused(ctx) {
|
|
13
|
+
return fail(ctx, 'You cannot delete the product because it has one or more prices. Deactivate the product instead.', 400, 'resource_already_exists');
|
|
14
|
+
}
|
|
15
|
+
const deleteProduct = async (ctx) => {
|
|
16
|
+
const id = at(ctx, 'id');
|
|
17
|
+
if (!ctx.get('product', id))
|
|
18
|
+
return productMissing(ctx, id);
|
|
19
|
+
if (ctx.rows('price').some((p) => p.product === id))
|
|
20
|
+
return pricedProductRefused(ctx);
|
|
21
|
+
await ctx.write('product', id, { deleted: true }, 'product.delete');
|
|
22
|
+
return ctx.reply({ id, object: 'product', deleted: true });
|
|
23
|
+
};
|
|
24
|
+
// ── features: an entitlements feature granted to a product, at most once ──
|
|
25
|
+
/** A feature attached to a product that already has it. */
|
|
26
|
+
function alreadyAttached(ctx) {
|
|
27
|
+
return fail(ctx, 'This feature is already attached to the product.', 400, 'resource_already_exists');
|
|
28
|
+
}
|
|
29
|
+
const attachFeature = async (ctx) => {
|
|
30
|
+
const product = at(ctx, 'product');
|
|
31
|
+
if (!ctx.get('product', product))
|
|
32
|
+
return productMissing(ctx, product);
|
|
33
|
+
const feature = typeof ctx.params.entitlement_feature === 'string' ? ctx.params.entitlement_feature : '';
|
|
34
|
+
if (!feature)
|
|
35
|
+
return fail(ctx, 'Missing required param: entitlement_feature.', 400, 'parameter_missing');
|
|
36
|
+
const feat = ctx.get('entitlements_feature', feature);
|
|
37
|
+
if (!feat)
|
|
38
|
+
return fail(ctx, `No such feature: '${feature}'`, 400, 'resource_missing');
|
|
39
|
+
if (ctx.rowsRaw('product_feature').some((pf) => pf._product === product && pf._feature === feature))
|
|
40
|
+
return alreadyAttached(ctx);
|
|
41
|
+
// Stripe answers the feature itself as the product feature's entitlement_feature (docs.stripe.com/api/product-feature/object)
|
|
42
|
+
return ctx.reply(await created(ctx, 'product_feature', { _product: product, _feature: feature }, {
|
|
43
|
+
livemode: false,
|
|
44
|
+
entitlement_feature: { id: feature, object: 'entitlements.feature', name: feat.name, lookup_key: feat.lookup_key, active: feat.active, metadata: feat.metadata ?? {}, livemode: false },
|
|
45
|
+
}));
|
|
46
|
+
};
|
|
47
|
+
const features = async (ctx) => {
|
|
48
|
+
const product = at(ctx, 'product');
|
|
49
|
+
if (!ctx.get('product', product))
|
|
50
|
+
return productMissing(ctx, product);
|
|
51
|
+
return list(ctx, 'product_feature', newest(ctx, 'product_feature').filter((pf) => kept(ctx, 'product_feature', pf, '_product') === product));
|
|
52
|
+
};
|
|
53
|
+
const productFeature = (ctx) => {
|
|
54
|
+
const pf = ctx.get('product_feature', at(ctx, 'id'));
|
|
55
|
+
// the product a feature is attached to is its address, not a field Stripe answers
|
|
56
|
+
return pf && kept(ctx, 'product_feature', pf, '_product') === at(ctx, 'product') ? pf : undefined;
|
|
57
|
+
};
|
|
58
|
+
const featureMissing = (ctx) => fail(ctx, `No such product feature: '${at(ctx, 'id')}'`, 404, 'resource_missing');
|
|
59
|
+
const feature = async (ctx) => {
|
|
60
|
+
const pf = productFeature(ctx);
|
|
61
|
+
return pf ? ctx.reply(pf) : featureMissing(ctx);
|
|
62
|
+
};
|
|
63
|
+
const detachFeature = async (ctx) => {
|
|
64
|
+
if (!productFeature(ctx))
|
|
65
|
+
return featureMissing(ctx);
|
|
66
|
+
const id = at(ctx, 'id');
|
|
67
|
+
// when it came off the product: a subscription keeps it until its next period (semantics/entitlements.ts)
|
|
68
|
+
await ctx.write('product_feature', id, { deleted: true, _removed_at: Number(ctx.now()) }, 'product_feature.delete');
|
|
69
|
+
return ctx.reply({ id, object: 'product_feature', deleted: true });
|
|
70
|
+
};
|
|
71
|
+
// ── prices ──
|
|
72
|
+
/** A tiered price: tiers_mode and tiers, each tier with up_to and a unit or flat amount; no single unit_amount, and
|
|
73
|
+
* the last tier's up_to null (∞) (docs.stripe.com/products-prices/pricing-models#tiered-pricing). */
|
|
74
|
+
async function tieredPrice(ctx, params) {
|
|
75
|
+
const mode = typeof params.tiers_mode === 'string' ? params.tiers_mode : '';
|
|
76
|
+
if (mode !== 'graduated' && mode !== 'volume')
|
|
77
|
+
return fail(ctx, 'Missing required param: tiers_mode (graduated or volume).', 400, 'parameter_missing');
|
|
78
|
+
const rawTiers = Array.isArray(params.tiers) ? params.tiers : [];
|
|
79
|
+
if (rawTiers.length === 0)
|
|
80
|
+
return fail(ctx, 'Missing required param: tiers.', 400, 'parameter_missing');
|
|
81
|
+
const tiers = rawTiers.map((t) => {
|
|
82
|
+
const upToRaw = t.up_to;
|
|
83
|
+
return {
|
|
84
|
+
up_to: upToRaw === 'inf' || upToRaw === undefined || upToRaw === null ? null : Math.trunc(Number(upToRaw)),
|
|
85
|
+
unit_amount: t.unit_amount !== undefined ? Math.trunc(Number(t.unit_amount)) : null,
|
|
86
|
+
unit_amount_decimal: t.unit_amount !== undefined ? String(Math.trunc(Number(t.unit_amount))) : null,
|
|
87
|
+
flat_amount: t.flat_amount !== undefined ? Math.trunc(Number(t.flat_amount)) : null,
|
|
88
|
+
flat_amount_decimal: t.flat_amount !== undefined ? String(Math.trunc(Number(t.flat_amount))) : null,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
if (tiers.some((t) => t.unit_amount === null && t.flat_amount === null))
|
|
92
|
+
return fail(ctx, 'Each tier must specify a unit_amount or flat_amount.', 400, 'parameter_invalid_empty');
|
|
93
|
+
const { tiers: _t, ...rest } = params;
|
|
94
|
+
return ctx.reply(await created(ctx, 'price', { ...rest, billing_scheme: 'tiered', tiers_mode: mode, tiers, unit_amount: null }, { active: true, livemode: false, type: rest.recurring ? 'recurring' : 'one_time', metadata: {} }));
|
|
95
|
+
}
|
|
96
|
+
const createPrice = async (ctx) => {
|
|
97
|
+
const params = ctx.params;
|
|
98
|
+
// the product must exist
|
|
99
|
+
const product = typeof params.product === 'string' ? params.product : '';
|
|
100
|
+
if (!product || !ctx.get('product', product))
|
|
101
|
+
return fail(ctx, `No such product: '${product}'`, 400, 'resource_missing');
|
|
102
|
+
// a tiered price needs tiers_mode and tiers, each tier with up_to and a unit or flat amount; it
|
|
103
|
+
// has no single unit_amount, and its last tier's up_to is null (∞)
|
|
104
|
+
const scheme = typeof params.billing_scheme === 'string' ? params.billing_scheme : 'per_unit';
|
|
105
|
+
if (scheme === 'tiered')
|
|
106
|
+
return tieredPrice(ctx, params);
|
|
107
|
+
// a price that recurs is billed by subscriptions; any other is paid once. Its recurring block answers what the create
|
|
108
|
+
// left out as Stripe does: `usage_type` "Defaults to `licensed`", and the create example's answer
|
|
109
|
+
// (docs.stripe.com/api/prices/create) carries `"interval_count": 1, "trial_period_days": null`; a meter only a metered
|
|
110
|
+
// price names
|
|
111
|
+
if (params.recurring && typeof params.recurring === 'object') {
|
|
112
|
+
const r = params.recurring;
|
|
113
|
+
params.recurring = { ...r, interval_count: r.interval_count !== undefined ? Math.trunc(Number(r.interval_count)) : 1, usage_type: r.usage_type ?? 'licensed', trial_period_days: r.trial_period_days !== undefined ? Math.trunc(Number(r.trial_period_days)) : null, meter: r.meter ?? null };
|
|
114
|
+
}
|
|
115
|
+
return ctx.reply(await created(ctx, 'price', params, { active: true, livemode: false, billing_scheme: 'per_unit', type: params.recurring ? 'recurring' : 'one_time', metadata: {} }));
|
|
116
|
+
};
|
|
117
|
+
// `type` is not stored (it collides with the tree's discriminator), so `recurring` stands for it
|
|
118
|
+
const listPrices = async (ctx) => list(ctx, 'price', where(ctx, newest(ctx, 'price'), {
|
|
119
|
+
active: (p, v) => asBool(p.active) === asBool(v),
|
|
120
|
+
product: (p, v) => p.product === v,
|
|
121
|
+
currency: (p, v) => p.currency === v,
|
|
122
|
+
type: (p, v) => (v === 'recurring' ? !!p.recurring : !p.recurring),
|
|
123
|
+
lookup_keys: (p, v) => (Array.isArray(v) ? v.map(String) : [String(v)]).includes(String(p.lookup_key)),
|
|
124
|
+
}));
|
|
125
|
+
const retrievePrice = async (ctx) => {
|
|
126
|
+
const p = ctx.get('price', at(ctx, 'price'));
|
|
127
|
+
return p ? ctx.reply(p) : fail(ctx, `No such price: '${at(ctx, 'price')}'`, 404, 'resource_missing');
|
|
128
|
+
};
|
|
129
|
+
export const products = {
|
|
130
|
+
PostProducts: createProduct,
|
|
131
|
+
GetProducts: listProducts,
|
|
132
|
+
DeleteProductsId: deleteProduct,
|
|
133
|
+
PostProductsProductFeatures: attachFeature,
|
|
134
|
+
GetProductsProductFeatures: features,
|
|
135
|
+
GetProductsProductFeaturesId: feature,
|
|
136
|
+
DeleteProductsProductFeaturesId: detachFeature,
|
|
137
|
+
PostPrices: createPrice,
|
|
138
|
+
GetPrices: listPrices,
|
|
139
|
+
GetPricesPrice: retrievePrice,
|
|
140
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { at, created, fail, list, newest } from "./shared.js";
|
|
2
|
+
const approve = async (ctx) => {
|
|
3
|
+
const id = at(ctx, 'review');
|
|
4
|
+
const r = ctx.get('review', id);
|
|
5
|
+
if (!r)
|
|
6
|
+
return fail(ctx, `No such review: '${id}'`, 404, 'resource_missing');
|
|
7
|
+
const refused = ctx.legal('review', 'open', 'PostReviewsReviewApprove', r.open === true, undefined, id);
|
|
8
|
+
if (refused)
|
|
9
|
+
return ctx.refuse(refused);
|
|
10
|
+
return ctx.reply(await ctx.write('review', id, { open: false, closed_reason: 'approved', reason: 'approved' }, 'review.closed'));
|
|
11
|
+
};
|
|
12
|
+
const VL = 'radar.value_list';
|
|
13
|
+
const VLI = 'radar.value_list_item';
|
|
14
|
+
const listMissing = (ctx, id, status = 404) => fail(ctx, `No such radar value list: '${id}'`, status, 'resource_missing');
|
|
15
|
+
const createList = async (ctx) => {
|
|
16
|
+
const params = ctx.params;
|
|
17
|
+
if (params.alias === undefined || params.alias === '')
|
|
18
|
+
return fail(ctx, 'Missing required param: alias.', 400, 'parameter_missing');
|
|
19
|
+
if (params.name === undefined || params.name === '')
|
|
20
|
+
return fail(ctx, 'Missing required param: name.', 400, 'parameter_missing');
|
|
21
|
+
const made = await created(ctx, VL, params, {
|
|
22
|
+
item_type: typeof params.item_type === 'string' ? params.item_type : 'string', livemode: false, metadata: {},
|
|
23
|
+
created_by: 'twin', list_items: { object: 'list', data: [], has_more: false, url: '' },
|
|
24
|
+
});
|
|
25
|
+
await syncItems(ctx, String(made.id));
|
|
26
|
+
return ctx.reply(ctx.get(VL, String(made.id)));
|
|
27
|
+
};
|
|
28
|
+
/** A value list's list_items, "List of items contained within this value list" (the served spec), kept in step with
|
|
29
|
+
* its items, at the address the fixture names (/v1/radar/value_list_items?value_list=…). */
|
|
30
|
+
async function syncItems(ctx, listId) {
|
|
31
|
+
const data = newest(ctx, VLI).filter((i) => i.value_list === listId);
|
|
32
|
+
await ctx.write(VL, listId, { list_items: { object: 'list', data, has_more: false, url: `/v1/radar/value_list_items?value_list=${listId}` } }, 'radar.value_list.items_synced');
|
|
33
|
+
}
|
|
34
|
+
const updateList = async (ctx) => {
|
|
35
|
+
const id = at(ctx, 'value_list');
|
|
36
|
+
if (!ctx.row(VL, id, { withDeleted: true }))
|
|
37
|
+
return listMissing(ctx, id);
|
|
38
|
+
return ctx.reply(await ctx.write(VL, id, ctx.params, 'radar.value_list.updated'));
|
|
39
|
+
};
|
|
40
|
+
const deleteList = async (ctx) => {
|
|
41
|
+
const id = at(ctx, 'value_list');
|
|
42
|
+
if (!ctx.row(VL, id, { withDeleted: true }))
|
|
43
|
+
return listMissing(ctx, id);
|
|
44
|
+
await ctx.write(VL, id, { deleted: true }, 'radar.value_list.deleted');
|
|
45
|
+
return ctx.reply({ id, object: 'radar.value_list', deleted: true });
|
|
46
|
+
};
|
|
47
|
+
const addItem = async (ctx) => {
|
|
48
|
+
const listId = typeof ctx.params.value_list === 'string' ? ctx.params.value_list : '';
|
|
49
|
+
if (!listId)
|
|
50
|
+
return fail(ctx, 'Missing required param: value_list.', 400, 'parameter_missing');
|
|
51
|
+
if (!ctx.row(VL, listId, { withDeleted: true }))
|
|
52
|
+
return listMissing(ctx, listId, 400);
|
|
53
|
+
if (ctx.params.value === undefined || ctx.params.value === '')
|
|
54
|
+
return fail(ctx, 'Missing required param: value.', 400, 'parameter_missing');
|
|
55
|
+
const item = await created(ctx, VLI, { value_list: listId, value: ctx.params.value }, { livemode: false, created_by: 'twin' });
|
|
56
|
+
await syncItems(ctx, listId);
|
|
57
|
+
return ctx.reply(item);
|
|
58
|
+
};
|
|
59
|
+
const items = async (ctx) => {
|
|
60
|
+
const listId = typeof ctx.params.value_list === 'string' ? ctx.params.value_list : '';
|
|
61
|
+
if (!listId)
|
|
62
|
+
return fail(ctx, 'Missing required param: value_list.', 400, 'parameter_missing');
|
|
63
|
+
return list(ctx, VLI, newest(ctx, VLI).filter((i) => i.value_list === listId));
|
|
64
|
+
};
|
|
65
|
+
const removeItem = async (ctx) => {
|
|
66
|
+
const id = at(ctx, 'item');
|
|
67
|
+
const was = ctx.row(VLI, id, { withDeleted: true });
|
|
68
|
+
if (!was)
|
|
69
|
+
return fail(ctx, `No such radar value list item: '${id}'`, 404, 'resource_missing');
|
|
70
|
+
await ctx.write(VLI, id, { deleted: true }, 'radar.value_list_item.deleted');
|
|
71
|
+
if (typeof was.value_list === 'string' && ctx.get(VL, was.value_list))
|
|
72
|
+
await syncItems(ctx, was.value_list);
|
|
73
|
+
return ctx.reply({ id, object: 'radar.value_list_item', deleted: true });
|
|
74
|
+
};
|
|
75
|
+
export const radar = {
|
|
76
|
+
PostReviewsReviewApprove: approve,
|
|
77
|
+
PostRadarValueLists: createList,
|
|
78
|
+
PostRadarValueListsValueList: updateList,
|
|
79
|
+
DeleteRadarValueListsValueList: deleteList,
|
|
80
|
+
PostRadarValueListItems: addItem,
|
|
81
|
+
GetRadarValueListItems: items,
|
|
82
|
+
DeleteRadarValueListItemsItem: removeItem,
|
|
83
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Semantics, SemanticsContext } from '@volter/world-core';
|
|
2
|
+
import { type Row } from './shared.js';
|
|
3
|
+
export declare function refundCharge(ctx: SemanticsContext, charge: Row, params: Row): Promise<Response>;
|
|
4
|
+
/** Time's settling of held refunds, caught up to the World's clock: each refund held for want of balance, oldest first,
|
|
5
|
+
* is made at the first moment the account's available balance covers it (the funds that come due then), its debit
|
|
6
|
+
* written then, as `refund.updated`. Where the documentation stops and the twin decides: a held refund is made the
|
|
7
|
+
* moment the balance covers it. */
|
|
8
|
+
export declare function settleHeldRefunds(ctx: SemanticsContext): Promise<void>;
|
|
9
|
+
export declare const refunds: Record<string, Semantics>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { releaseAuthorization } from "./charges.js";
|
|
2
|
+
import { actingAccount, availableAt, fundsDueBetween, settleRefund, settleTransferReversal } from "./ledger.js";
|
|
3
|
+
import { asBool } from "../stripe-twin.js";
|
|
4
|
+
import { at, at_, created, fail, newest, refundDestination } from "./shared.js";
|
|
5
|
+
// the reasons a caller may give; `expired_uncaptured_charge` is Stripe's own
|
|
6
|
+
const REFUND_REASONS = new Set(['duplicate', 'fraudulent', 'requested_by_customer']);
|
|
7
|
+
/**
|
|
8
|
+
* Create a refund and land it on its charge. An omitted amount is the whole remaining balance. The
|
|
9
|
+
* charge's write records `charge.refunded`, the event Stripe sends carrying the charge's new totals;
|
|
10
|
+
* `refunded` is true only at the full amount.
|
|
11
|
+
*/
|
|
12
|
+
/** A reason Stripe does not take (docs.stripe.com/api/refunds/create#create_refund-reason). */
|
|
13
|
+
function badReason(ctx) {
|
|
14
|
+
return fail(ctx, `Invalid refund reason: must be one of ${[...REFUND_REASONS].join(', ')}.`, 400, 'parameter_invalid_string_enum');
|
|
15
|
+
}
|
|
16
|
+
/** A bank-transfer payment refunded with origin=customer_balance goes back to the cash balance
|
|
17
|
+
* (docs.stripe.com/payments/customer-balance/refunding). */
|
|
18
|
+
async function backToCashBalance(ctx, customer, currency, amount, refundId) {
|
|
19
|
+
const before = ctx.rows('customer_cash_balance_transaction').filter((t) => t.customer === customer && String(t.currency) === currency).reduce((n, t) => n + (Number(t.net_amount) || 0), 0);
|
|
20
|
+
await created(ctx, 'customer_cash_balance_transaction', { customer }, { currency, type: 'refunded_from_payment', net_amount: amount, ending_balance: before + amount, refunded_from_payment: { refund: refundId }, livemode: false });
|
|
21
|
+
}
|
|
22
|
+
/** A capture=false charge's refund, which releases its whole authorization (see the header). */
|
|
23
|
+
async function refundUncaptured(ctx, charge, amount, remaining, params) {
|
|
24
|
+
if (amount < remaining)
|
|
25
|
+
return fail(ctx, `Charge ${String(charge.id)} is uncaptured: refunding it releases the whole authorization (${remaining}).`, 400, 'amount_too_small');
|
|
26
|
+
const { reason, metadata } = params;
|
|
27
|
+
return ctx.reply(await releaseAuthorization(ctx, charge, ctx.call.operation.id, { ...(reason !== undefined ? { reason } : {}), ...(metadata !== undefined ? { metadata } : {}) }));
|
|
28
|
+
}
|
|
29
|
+
export async function refundCharge(ctx, charge, params) {
|
|
30
|
+
const chargeId = String(charge.id);
|
|
31
|
+
const total = Number(charge.amount) || 0;
|
|
32
|
+
const already = Number(charge.amount_refunded) || 0;
|
|
33
|
+
const remaining = Math.max(0, total - already);
|
|
34
|
+
const amount = params.amount === undefined ? remaining : Math.trunc(Number(params.amount));
|
|
35
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
36
|
+
return fail(ctx, 'Invalid integer: amount must be a positive integer.', 400, 'parameter_invalid_integer');
|
|
37
|
+
if (amount > remaining)
|
|
38
|
+
return fail(ctx, `Refund amount (${amount}) is greater than unrefunded amount on charge (${remaining}).`, 400, 'amount_too_large');
|
|
39
|
+
if (params.reason !== undefined && !REFUND_REASONS.has(String(params.reason)))
|
|
40
|
+
return badReason(ctx);
|
|
41
|
+
// an authorization is released, never refunded out of the balance: a PaymentIntent's by cancelling the intent, a
|
|
42
|
+
// capture=false charge's by its refund (refundUncaptured)
|
|
43
|
+
if (charge.captured === false && typeof charge.payment_intent === 'string')
|
|
44
|
+
return fail(ctx, `Charge ${chargeId} is uncaptured and can't be refunded directly; cancel its PaymentIntent (${charge.payment_intent}) to release the authorization.`, 400, 'payment_intent_unexpected_state');
|
|
45
|
+
if (charge.captured === false)
|
|
46
|
+
return refundUncaptured(ctx, charge, amount, remaining, params);
|
|
47
|
+
const refunded = already + amount;
|
|
48
|
+
// the refund that takes the rest refunds the charge: the charge machine's move under this operation
|
|
49
|
+
if (refunded >= total && charge.refunded !== true) {
|
|
50
|
+
const refused = ctx.legal('charge', 'refunded', ctx.call.operation.id, 'false', 'true', chargeId);
|
|
51
|
+
if (refused)
|
|
52
|
+
return ctx.refuse(refused);
|
|
53
|
+
}
|
|
54
|
+
// a refund has no `livemode` field; it debits the balance at once
|
|
55
|
+
const refundId = ctx.mint('refund');
|
|
56
|
+
const currency = String(charge.currency ?? 'usd');
|
|
57
|
+
const pi = typeof charge.payment_intent === 'string' ? ctx.get('payment_intent', charge.payment_intent) : undefined;
|
|
58
|
+
const byTransfer = (pi?.payment_method_types ?? []).includes('customer_balance');
|
|
59
|
+
const toCashBalance = byTransfer && params.origin === 'customer_balance';
|
|
60
|
+
const awaitsBank = byTransfer && !toCashBalance;
|
|
61
|
+
const now = Number(ctx.now());
|
|
62
|
+
const customer = typeof charge.customer === 'string' ? charge.customer : typeof pi?.customer === 'string' ? pi.customer : undefined;
|
|
63
|
+
const emailTo = typeof params.instructions_email === 'string' ? params.instructions_email : (customer ? ctx.get('customer', customer)?.email : undefined) ?? null;
|
|
64
|
+
// "Refunds use your available Stripe balance (not including pending amounts). If your available balance doesn't
|
|
65
|
+
// cover the amount of the refund, Stripe holds the refund as pending for card transactions ... until your Stripe
|
|
66
|
+
// balance becomes sufficient" (docs.stripe.com/refunds): the refund waits, moving no money, until the balance covers
|
|
67
|
+
// it (settleHeldRefunds). Where the documentation stops and the twin decides: a refund of another payment method
|
|
68
|
+
// short of balance is not modelled (Stripe fails it); the twin makes it as a card's.
|
|
69
|
+
const account = actingAccount(ctx);
|
|
70
|
+
const isCard = charge.payment_method_details?.type === 'card';
|
|
71
|
+
const held = !awaitsBank && !toCashBalance && isCard && availableAt(ctx, account, currency, now) < amount;
|
|
72
|
+
if (held)
|
|
73
|
+
ctx.legal('refund', 'status', ctx.call.operation.id, 'succeeded', 'pending', refundId);
|
|
74
|
+
const bt = awaitsBank || held ? null : await settleRefund(ctx, refundId, amount, currency);
|
|
75
|
+
if (toCashBalance && customer)
|
|
76
|
+
await backToCashBalance(ctx, customer, currency, amount, refundId);
|
|
77
|
+
const { reverse_transfer: reverseTransfer, refund_application_fee: refundFee, instructions_email: _email, origin: _origin, ...stored } = params;
|
|
78
|
+
const share = (whole) => Math.round((whole * amount) / (total || 1));
|
|
79
|
+
let transferReversal = null;
|
|
80
|
+
const transfer = typeof charge.transfer === 'string' ? ctx.get('transfer', charge.transfer) : undefined;
|
|
81
|
+
if (reverseTransfer !== undefined && asBool(reverseTransfer) && transfer) {
|
|
82
|
+
const back = Math.min(share(Number(transfer.amount) || 0), (Number(transfer.amount) || 0) - (Number(transfer.amount_reversed) || 0));
|
|
83
|
+
const trr = await created(ctx, 'transfer_reversal', { amount: back, currency, transfer: transfer.id }, { balance_transaction: null, destination_payment_refund: null, source_refund: refundId, metadata: {} });
|
|
84
|
+
const trrBt = await settleTransferReversal(ctx, String(trr.id), back, currency, String(transfer.destination));
|
|
85
|
+
const reversal = await ctx.write('transfer_reversal', String(trr.id), { balance_transaction: trrBt }, 'transfer_reversal.updated');
|
|
86
|
+
const reversed = (Number(transfer.amount_reversed) || 0) + back;
|
|
87
|
+
const list = transfer.reversals ?? { object: 'list', data: [], has_more: false, total_count: 0 };
|
|
88
|
+
const data = [...((list.data ?? [])), reversal];
|
|
89
|
+
await ctx.write('transfer', String(transfer.id), { amount_reversed: reversed, reversed: reversed >= (Number(transfer.amount) || 0), reversals: { ...list, data, total_count: data.length, url: `/v1/transfers/${String(transfer.id)}/reversals` } }, 'transfer.reversed');
|
|
90
|
+
transferReversal = String(trr.id);
|
|
91
|
+
}
|
|
92
|
+
const fee = typeof charge.application_fee === 'string' ? ctx.get('application_fee', charge.application_fee) : undefined;
|
|
93
|
+
if (refundFee !== undefined && asBool(refundFee) && fee) {
|
|
94
|
+
const feeTotal = Number(fee.amount) || 0;
|
|
95
|
+
const back = Math.min(share(feeTotal), feeTotal - (Number(fee.amount_refunded) || 0));
|
|
96
|
+
const done = (Number(fee.amount_refunded) || 0) + back;
|
|
97
|
+
if (!(done >= feeTotal && fee.refunded !== true && ctx.legal('application_fee', 'refunded', ctx.call.operation.id, 'false', 'true', String(fee.id)))) {
|
|
98
|
+
const fr = await created(ctx, 'fee_refund', { amount: back, currency, fee: fee.id }, { balance_transaction: null, metadata: {} });
|
|
99
|
+
const list = fee.refunds ?? { object: 'list', data: [], has_more: false, total_count: 0 };
|
|
100
|
+
const data = [...((list.data ?? [])), fr];
|
|
101
|
+
await ctx.write('application_fee', String(fee.id), { amount_refunded: done, refunded: done >= feeTotal, refunds: { ...list, data, total_count: data.length, url: `/v1/application_fees/${String(fee.id)}/refunds` } }, 'application_fee.refunded');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const body = await created(ctx, 'refund', {
|
|
105
|
+
...stored, id: refundId, charge: chargeId, amount, currency, transfer_reversal: transferReversal,
|
|
106
|
+
...(typeof charge.payment_intent === 'string' ? { payment_intent: charge.payment_intent } : {}),
|
|
107
|
+
}, {
|
|
108
|
+
status: awaitsBank ? 'requires_action' : held ? 'pending' : 'succeeded', metadata: {}, reason: null, receipt_number: null,
|
|
109
|
+
...(held ? { pending_reason: 'insufficient_funds', _account: account ?? null } : {}),
|
|
110
|
+
balance_transaction: bt, source_transfer_reversal: null,
|
|
111
|
+
// a bank transfer's refund goes to the customer's bank (a us_bank_transfer) or back to its cash balance
|
|
112
|
+
// (customer_cash_balance): "If this is a `us_bank_transfer` refund, this hash contains the transaction specific details",
|
|
113
|
+
// "If this is a `customer_cash_balance` refund ..." (docs.stripe.com/api/refunds/object, destination_details)
|
|
114
|
+
...(byTransfer
|
|
115
|
+
? { destination_details: toCashBalance ? { type: 'customer_cash_balance', customer_cash_balance: {} } : { type: 'us_bank_transfer', us_bank_transfer: { reference: null, reference_status: null } } }
|
|
116
|
+
: refundDestination(charge, 'refund')),
|
|
117
|
+
...(awaitsBank ? { next_action: { type: 'display_details', display_details: { email_sent: { email_sent_at: now, email_sent_to: emailTo }, expires_at: now + 30 * 86_400 } } } : {}),
|
|
118
|
+
});
|
|
119
|
+
await ctx.write('charge', chargeId, { amount_refunded: refunded, refunded: refunded >= total }, 'charge.refunded');
|
|
120
|
+
return ctx.reply(body);
|
|
121
|
+
}
|
|
122
|
+
/** Time's settling of held refunds, caught up to the World's clock: each refund held for want of balance, oldest first,
|
|
123
|
+
* is made at the first moment the account's available balance covers it (the funds that come due then), its debit
|
|
124
|
+
* written then, as `refund.updated`. Where the documentation stops and the twin decides: a held refund is made the
|
|
125
|
+
* moment the balance covers it. */
|
|
126
|
+
export async function settleHeldRefunds(ctx) {
|
|
127
|
+
const now = Number(ctx.now());
|
|
128
|
+
const heldRefunds = ctx.rowsRaw('refund').filter((r) => r.status === 'pending' && r.pending_reason === 'insufficient_funds').sort((a, b) => Number(a.created) - Number(b.created));
|
|
129
|
+
for (const r of heldRefunds) {
|
|
130
|
+
const account = typeof r._account === 'string' ? r._account : undefined;
|
|
131
|
+
const currency = String(r.currency ?? 'usd');
|
|
132
|
+
const amount = Number(r.amount) || 0;
|
|
133
|
+
const when = [Number(r.created), ...fundsDueBetween(ctx, account, currency, Number(r.created), now)].find((t) => availableAt(ctx, account, currency, t) >= amount);
|
|
134
|
+
if (when === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
const c = await at_(ctx)(when);
|
|
137
|
+
c.legal('refund', 'status', ctx.call.operation.id, 'pending', 'succeeded', String(r.id), 'vendor');
|
|
138
|
+
const bt = await settleRefund(c, String(r.id), amount, currency, account ?? null);
|
|
139
|
+
await c.write('refund', String(r.id), { status: 'succeeded', pending_reason: null, balance_transaction: bt }, 'refund.updated');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** The charge a refund aims at: named directly, or reached through its PaymentIntent. */
|
|
143
|
+
function target(ctx) {
|
|
144
|
+
const chargeId = typeof ctx.params.charge === 'string' ? ctx.params.charge : '';
|
|
145
|
+
const piId = typeof ctx.params.payment_intent === 'string' ? ctx.params.payment_intent : '';
|
|
146
|
+
if (!chargeId && !piId)
|
|
147
|
+
return fail(ctx, 'One of `charge` or `payment_intent` is required.', 400, 'parameter_missing');
|
|
148
|
+
if (chargeId)
|
|
149
|
+
return ctx.get('charge', chargeId) ?? fail(ctx, `No such charge: '${chargeId}'`, 404, 'resource_missing');
|
|
150
|
+
const pi = ctx.get('payment_intent', piId);
|
|
151
|
+
if (!pi)
|
|
152
|
+
return fail(ctx, `No such payment_intent: '${piId}'`, 404, 'resource_missing');
|
|
153
|
+
const latest = typeof pi.latest_charge === 'string' ? pi.latest_charge : undefined;
|
|
154
|
+
const ch = (latest ? ctx.get('charge', latest) : undefined) ?? newest(ctx, 'charge').find((c) => c.payment_intent === piId);
|
|
155
|
+
return ch ?? fail(ctx, `This PaymentIntent does not have a successful charge to refund. Its status is ${String(pi.status)}.`, 400, 'payment_intent_unexpected_state');
|
|
156
|
+
}
|
|
157
|
+
const create = async (ctx) => {
|
|
158
|
+
const ch = target(ctx);
|
|
159
|
+
return ch instanceof Response ? ch : refundCharge(ctx, ch, ctx.params);
|
|
160
|
+
};
|
|
161
|
+
const refundMissing = (ctx, id) => fail(ctx, `No such refund: '${id}'`, 404, 'resource_missing');
|
|
162
|
+
const retrieve = async (ctx) => {
|
|
163
|
+
const r = ctx.get('refund', at(ctx, 'refund'));
|
|
164
|
+
return r ? ctx.reply(r) : refundMissing(ctx, at(ctx, 'refund'));
|
|
165
|
+
};
|
|
166
|
+
// a canceled pending refund gives its money back to the charge: pending funds were already taken out
|
|
167
|
+
const cancel = async (ctx) => {
|
|
168
|
+
const id = at(ctx, 'refund');
|
|
169
|
+
const r = ctx.get('refund', id);
|
|
170
|
+
if (!r)
|
|
171
|
+
return refundMissing(ctx, id);
|
|
172
|
+
const refused = ctx.legal('refund', 'status', 'PostRefundsRefundCancel', r.status, undefined, id);
|
|
173
|
+
if (refused)
|
|
174
|
+
return ctx.refuse(refused);
|
|
175
|
+
// next_action says what a refund "needs to continue processing" "If the refund has a status of requires_action"
|
|
176
|
+
// (docs.stripe.com/api/refunds/object): a canceled one needs nothing
|
|
177
|
+
const canceled = await ctx.write('refund', id, { status: 'canceled', next_action: null }, 'refund.updated');
|
|
178
|
+
const chargeId = typeof r.charge === 'string' ? r.charge : '';
|
|
179
|
+
const ch = chargeId ? ctx.get('charge', chargeId) : undefined;
|
|
180
|
+
if (ch) {
|
|
181
|
+
const total = Number(ch.amount) || 0;
|
|
182
|
+
const back = Math.max(0, (Number(ch.amount_refunded) || 0) - (Number(r.amount) || 0));
|
|
183
|
+
const refunded = back > 0 && back >= total;
|
|
184
|
+
// a charge the canceled refund had fully refunded is no longer refunded
|
|
185
|
+
if (refunded !== (ch.refunded === true))
|
|
186
|
+
ctx.legal('charge', 'refunded', 'PostRefundsRefundCancel', String(ch.refunded === true), String(refunded), chargeId);
|
|
187
|
+
await ctx.write('charge', chargeId, { amount_refunded: back, refunded }, 'charge.update');
|
|
188
|
+
}
|
|
189
|
+
return ctx.reply(canceled);
|
|
190
|
+
};
|
|
191
|
+
export const refunds = {
|
|
192
|
+
PostRefunds: create,
|
|
193
|
+
GetRefundsRefund: retrieve,
|
|
194
|
+
PostRefundsRefundCancel: cancel,
|
|
195
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SemanticsContext } from '@volter/world-core';
|
|
2
|
+
import { type Row } from './shared.js';
|
|
3
|
+
/** A customer's now: its test clock's frozen time when it is on one (docs.stripe.com/billing/testing/test-clocks), the
|
|
4
|
+
* World's otherwise. */
|
|
5
|
+
export declare function clockNow(ctx: SemanticsContext, customer: unknown): number;
|
|
6
|
+
/** What a subscription's invoices charge: its own default payment method, else its customer's. */
|
|
7
|
+
export declare function payerOf(ctx: SemanticsContext, sub: Row): string | undefined;
|
|
8
|
+
/** An item's quantity as it bills: 1 when none is set, and 0 when 0 is (a quantity of 0 bills nothing). */
|
|
9
|
+
export declare function itemQuantity(it: Row): number;
|
|
10
|
+
/** The subscription's discounts with a `once` coupon's spent: "When a subscription uses a coupon with `duration=once`, the
|
|
11
|
+
* coupon is considered used after the invoice finalizes and is removed from the subscription's `discounts` array"
|
|
12
|
+
* (docs.stripe.com/billing/subscriptions/coupons#coupon-duration). */
|
|
13
|
+
export declare function unspentDiscounts(discounts: unknown): Row[];
|
|
14
|
+
/** A subscription whose invoice has just been finalized: the `once` coupon that invoice took is spent, and a once coupon
|
|
15
|
+
* it did not take (added after it was drafted) stays for the next (unspentDiscounts). */
|
|
16
|
+
export declare function spendOnceCoupon(ctx: SemanticsContext, invoice: Row): Promise<void>;
|
|
17
|
+
/** A subscription's discounts after `invoice` finalizes: less the `once` coupons it took. */
|
|
18
|
+
export declare function keptAfter(discounts: Row[], invoice: Row): Row[];
|
|
19
|
+
/** A draft invoice for one period of a subscription, its lines the subscription's items and any one-time lines `once`
|
|
20
|
+
* gives it (a Checkout session's one-time prices, "on the initial invoice only", docs.stripe.com/api/checkout/sessions/
|
|
21
|
+
* create#create_checkout_session-line_items), under the subscription's discount. Answers the invoice id. */
|
|
22
|
+
export declare function draftSubscriptionInvoice(ctx: SemanticsContext, sub: Row, o: {
|
|
23
|
+
start: number;
|
|
24
|
+
end: number;
|
|
25
|
+
reason: 'subscription_create' | 'subscription_cycle' | 'subscription_update';
|
|
26
|
+
trial?: boolean;
|
|
27
|
+
once?: Row[];
|
|
28
|
+
id?: string;
|
|
29
|
+
}): Promise<string>;
|
|
30
|
+
/** Finalize a subscription's draft invoice at `at` and charge what its subscription pays with. The move is time's (a
|
|
31
|
+
* renewal), the customer's (the Checkout page they paid on) or the API's (an update that ends a trial, which names the
|
|
32
|
+
* payer, may ask for no attempt, and moves the subscription itself: semantics/subscriptions.ts applyTrialEnd). Answers
|
|
33
|
+
* whether it was paid. */
|
|
34
|
+
export declare function collectSubscriptionInvoice(ctx: SemanticsContext, invoiceId: string, at: number, actor: 'time' | 'external' | 'api', opts?: {
|
|
35
|
+
payer?: string;
|
|
36
|
+
attempt?: boolean;
|
|
37
|
+
}): Promise<boolean>;
|
|
38
|
+
/** Pay a subscription's open invoice with `paymentMethod` (the customer on the portal, `external`): the charge on the
|
|
39
|
+
* invoice's PaymentIntent, the invoice paid, a past_due subscription active. Answers the decline, if the card is refused. */
|
|
40
|
+
export declare function payOpenInvoice(ctx: SemanticsContext, invoiceId: string, paymentMethod: string): Promise<{
|
|
41
|
+
code: string;
|
|
42
|
+
message: string;
|
|
43
|
+
} | undefined>;
|
|
44
|
+
/** Time's moves on subscriptions, caught up to each customer's clock in the order they fell due, each made at the
|
|
45
|
+
* moment it was due: a period that ended renews (a trial's end among them) or, set to cancel, cancels; a renewal
|
|
46
|
+
* drafted an hour or more ago is finalized and charged. */
|
|
47
|
+
export declare function advanceBilling(ctx: SemanticsContext, only?: (sub: Row) => boolean): Promise<void>;
|