@volter/twin-stripe 3.0.0 → 3.0.2
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 +75 -21
- package/dist/journeys/coverage.json +596 -0
- package/dist/journeys/customer-life.json +6108 -0
- package/dist/journeys/customer-life.outline.md +371 -0
- package/dist/journeys/decisions.json +677 -0
- package/dist/journeys/demand.json +706 -0
- package/dist/journeys/score.json +853 -0
- package/dist/journeys/unreachable.json +102 -0
- package/dist/journeys/vendor-examples.json +8010 -0
- package/dist/spec/SOURCE.md +5 -1
- package/dist/spec/doc-examples.json +445 -0
- package/dist/spec/sources.json +135 -207
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +0 -2
- package/dist/src/engine/after-payment.d.ts +0 -1
- package/dist/src/engine/after-payment.js +0 -1
- package/dist/src/engine/common.d.ts +5 -5
- package/dist/src/engine/common.js +13 -11
- package/dist/src/engine/connect.d.ts +0 -21
- package/dist/src/engine/connect.js +1 -82
- package/dist/src/engine/customers.d.ts +0 -8
- package/dist/src/engine/customers.js +0 -22
- package/dist/src/engine/display.d.ts +0 -3
- package/dist/src/engine/display.js +0 -3
- package/dist/src/engine/files.d.ts +0 -7
- package/dist/src/engine/files.js +0 -10
- package/dist/src/engine/invoices.d.ts +6 -9
- package/dist/src/engine/invoices.js +56 -29
- package/dist/src/engine/issuing.d.ts +6 -8
- package/dist/src/engine/issuing.js +19 -10
- package/dist/src/engine/ledger.d.ts +0 -1
- package/dist/src/engine/ledger.js +0 -1
- package/dist/src/engine/oauth.js +1 -2
- package/dist/src/engine/refunds.js +2 -27
- package/dist/src/engine/stripe.d.ts +2 -7
- package/dist/src/engine/stripe.js +34 -83
- package/dist/src/engine/subscriptions.d.ts +0 -1
- package/dist/src/engine/subscriptions.js +0 -1
- package/dist/src/engine/version.d.ts +0 -2
- package/dist/src/engine/version.js +17 -6
- package/dist/src/fetch.js +2 -2
- package/dist/src/generated/surface.gen.json +1 -1
- package/dist/src/generated/ui.gen.json +1 -1
- package/dist/src/index.js +5 -3
- package/dist/src/manifest.js +348 -220
- package/dist/src/screens/billing-portal.js +5 -5
- package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
- package/dist/src/screens/checkout-js.js +84 -0
- package/dist/src/screens/connect-onboarding.js +3 -3
- package/dist/src/screens/dashboard.d.ts +1 -1
- package/dist/src/screens/dashboard.js +56 -53
- package/dist/src/screens/stripe-js.js +53 -93
- package/dist/src/semantics/account.d.ts +2 -0
- package/dist/src/semantics/account.js +8 -0
- package/dist/src/semantics/accounts.js +3 -1
- package/dist/src/semantics/around.js +3 -6
- package/dist/src/semantics/billing_portal.d.ts +0 -1
- package/dist/src/semantics/billing_portal.js +1 -7
- package/dist/src/semantics/checkout.d.ts +1 -1
- package/dist/src/semantics/checkout.js +18 -15
- package/dist/src/semantics/clock.js +1 -7
- package/dist/src/semantics/customers.d.ts +1 -2
- package/dist/src/semantics/customers.js +32 -38
- package/dist/src/semantics/entitlements.d.ts +0 -2
- package/dist/src/semantics/entitlements.js +4 -50
- package/dist/src/semantics/events.d.ts +2 -3
- package/dist/src/semantics/events.js +6 -11
- package/dist/src/semantics/index.js +16 -38
- package/dist/src/semantics/invoices.d.ts +3 -8
- package/dist/src/semantics/invoices.js +134 -127
- package/dist/src/semantics/issuing.d.ts +6 -0
- package/dist/src/semantics/issuing.js +77 -0
- package/dist/src/semantics/payment_intents.d.ts +0 -1
- package/dist/src/semantics/payment_intents.js +5 -13
- package/dist/src/semantics/payment_methods.d.ts +0 -2
- package/dist/src/semantics/payment_methods.js +2 -26
- package/dist/src/semantics/payouts.d.ts +1 -0
- package/dist/src/semantics/payouts.js +8 -1
- package/dist/src/semantics/products.d.ts +1 -3
- package/dist/src/semantics/products.js +3 -14
- package/dist/src/semantics/refunds.d.ts +1 -2
- package/dist/src/semantics/refunds.js +1 -5
- package/dist/src/semantics/shared.d.ts +64 -336
- package/dist/src/semantics/shared.js +326 -1336
- package/dist/src/semantics/states.d.ts +20 -89
- package/dist/src/semantics/states.js +116 -334
- package/dist/src/semantics/subscriptions.d.ts +1 -2
- package/dist/src/semantics/subscriptions.js +23 -145
- package/dist/src/semantics/test_helpers.d.ts +2 -5
- package/dist/src/semantics/test_helpers.js +121 -43
- package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
- package/dist/src/semantics/webhook_endpoints.js +26 -0
- package/generated/pack-facts.json +3 -3
- package/journeys/coverage.json +596 -0
- package/journeys/customer-life.json +6108 -0
- package/journeys/customer-life.outline.md +371 -0
- package/journeys/decisions.json +677 -0
- package/journeys/demand.json +706 -0
- package/journeys/score.json +853 -0
- package/journeys/unreachable.json +102 -0
- package/journeys/vendor-examples.json +8010 -0
- package/package.json +11 -10
- package/spec/SOURCE.md +5 -1
- package/spec/doc-examples.json +445 -0
- package/spec/sources.json +135 -207
- package/src/cli.ts +0 -3
- package/src/engine/after-payment.ts +0 -2
- package/src/engine/balance.ts +0 -1
- package/src/engine/checkout.ts +0 -1
- package/src/engine/common.ts +19 -17
- package/src/engine/connect.ts +1 -79
- package/src/engine/customers.ts +0 -25
- package/src/engine/display.ts +2 -3
- package/src/engine/files.ts +0 -13
- package/src/engine/invoices.ts +64 -33
- package/src/engine/issuing.ts +16 -11
- package/src/engine/ledger.ts +0 -2
- package/src/engine/oauth.ts +1 -2
- package/src/engine/onboarding.ts +0 -1
- package/src/engine/params.ts +0 -1
- package/src/engine/payment-intents.ts +0 -1
- package/src/engine/payment-methods.ts +0 -1
- package/src/engine/refunds.ts +2 -28
- package/src/engine/renewals.ts +0 -1
- package/src/engine/stripe.ts +35 -83
- package/src/engine/subscriptions.ts +0 -3
- package/src/engine/transfers.ts +0 -1
- package/src/engine/version.ts +15 -7
- package/src/engine/webhooks.ts +0 -2
- package/src/fetch.ts +2 -2
- package/src/generated/surface.gen.json +1 -1
- package/src/generated/ui.gen.json +1 -1
- package/src/index.ts +6 -4
- package/src/manifest.ts +348 -220
- package/src/screens/billing-portal.tsx +5 -7
- package/src/screens/checkout-js.ts +80 -0
- package/src/screens/checkout.tsx +0 -2
- package/src/screens/connect-onboarding.tsx +3 -5
- package/src/screens/dashboard.tsx +55 -73
- package/src/screens/stripe-js.ts +57 -125
- package/src/semantics/account.ts +10 -0
- package/src/semantics/account_links.ts +0 -1
- package/src/semantics/accounts.ts +8 -7
- package/src/semantics/apps.ts +1 -2
- package/src/semantics/around.ts +3 -6
- package/src/semantics/balance.ts +0 -1
- package/src/semantics/balance_transactions.ts +1 -3
- package/src/semantics/billing_portal.ts +1 -7
- package/src/semantics/charges.ts +1 -6
- package/src/semantics/checkout.ts +16 -12
- package/src/semantics/clock.ts +1 -8
- package/src/semantics/coupons.ts +0 -1
- package/src/semantics/customers.ts +32 -35
- package/src/semantics/doors.ts +2 -3
- package/src/semantics/entitlements.ts +1 -45
- package/src/semantics/events.ts +7 -14
- package/src/semantics/index.ts +16 -38
- package/src/semantics/invoice_payments.ts +1 -3
- package/src/semantics/invoices.ts +124 -119
- package/src/semantics/issuing.ts +74 -0
- package/src/semantics/payment_intents.ts +10 -17
- package/src/semantics/payment_methods.ts +2 -25
- package/src/semantics/payouts.ts +9 -3
- package/src/semantics/prices.ts +0 -1
- package/src/semantics/products.ts +4 -14
- package/src/semantics/promotion_codes.ts +0 -1
- package/src/semantics/radar.ts +1 -3
- package/src/semantics/refunds.ts +2 -11
- package/src/semantics/shared.ts +350 -1382
- package/src/semantics/states.ts +109 -330
- package/src/semantics/subscriptions.ts +25 -145
- package/src/semantics/test_helpers.ts +109 -43
- package/src/semantics/transfers.ts +1 -3
- package/src/semantics/webhook_endpoints.ts +27 -0
- package/src/server.ts +0 -1
- package/dist/generated/pack-facts.json +0 -94
- package/dist/src/engine/billing.d.ts +0 -5
- package/dist/src/engine/billing.js +0 -6
- package/dist/src/engine/charges.d.ts +0 -6
- package/dist/src/engine/charges.js +0 -13
- package/dist/src/engine/connect-settings.d.ts +0 -16
- package/dist/src/engine/connect-settings.js +0 -26
- package/dist/src/engine/coupons.d.ts +0 -0
- package/dist/src/engine/coupons.js +0 -5
- package/dist/src/engine/credit-notes.d.ts +0 -1
- package/dist/src/engine/credit-notes.js +0 -5
- package/dist/src/engine/disputes.d.ts +0 -0
- package/dist/src/engine/disputes.js +0 -9
- package/dist/src/engine/entitlements.d.ts +0 -1
- package/dist/src/engine/entitlements.js +0 -4
- package/dist/src/engine/ephemeral-keys.d.ts +0 -0
- package/dist/src/engine/ephemeral-keys.js +0 -4
- package/dist/src/engine/front.d.ts +0 -5
- package/dist/src/engine/front.js +0 -25
- package/dist/src/engine/payment-links.d.ts +0 -1
- package/dist/src/engine/payment-links.js +0 -5
- package/dist/src/engine/plans.d.ts +0 -6
- package/dist/src/engine/plans.js +0 -26
- package/dist/src/engine/platform.d.ts +0 -39
- package/dist/src/engine/platform.js +0 -34
- package/dist/src/engine/products.d.ts +0 -0
- package/dist/src/engine/products.js +0 -4
- package/dist/src/engine/setup-intents.d.ts +0 -1
- package/dist/src/engine/setup-intents.js +0 -4
- package/dist/src/engine/subscription-schedules.d.ts +0 -3
- package/dist/src/engine/subscription-schedules.js +0 -1
- package/dist/src/engine/tax.d.ts +0 -7
- package/dist/src/engine/tax.js +0 -28
- package/dist/src/engine/terminal.d.ts +0 -4
- package/dist/src/engine/terminal.js +0 -20
- package/dist/src/engine/test-clocks.d.ts +0 -5
- package/dist/src/engine/test-clocks.js +0 -9
- package/dist/src/engine/tokens.d.ts +0 -7
- package/dist/src/engine/tokens.js +0 -20
- package/dist/src/engine/treasury.d.ts +0 -23
- package/dist/src/engine/treasury.js +0 -45
- package/dist/src/screens/connect-oauth.js +0 -333
- package/dist/src/semantics/disputes.d.ts +0 -2
- package/dist/src/semantics/disputes.js +0 -5
- package/dist/src/semantics/invoiceitems.d.ts +0 -3
- package/dist/src/semantics/invoiceitems.js +0 -59
- package/dist/src/semantics/setup_intents.d.ts +0 -3
- package/dist/src/semantics/setup_intents.js +0 -41
- package/dist/src/semantics/subscription_items.d.ts +0 -5
- package/dist/src/semantics/subscription_items.js +0 -55
- package/dist/src/semantics/subscription_schedules.d.ts +0 -4
- package/dist/src/semantics/subscription_schedules.js +0 -60
- package/src/engine/billing.ts +0 -16
- package/src/engine/charges.ts +0 -19
- package/src/engine/connect-settings.ts +0 -23
- package/src/engine/coupons.ts +0 -5
- package/src/engine/credit-notes.ts +0 -6
- package/src/engine/disputes.ts +0 -9
- package/src/engine/entitlements.ts +0 -5
- package/src/engine/ephemeral-keys.ts +0 -3
- package/src/engine/front.ts +0 -31
- package/src/engine/payment-links.ts +0 -6
- package/src/engine/plans.ts +0 -42
- package/src/engine/platform.ts +0 -47
- package/src/engine/products.ts +0 -3
- package/src/engine/setup-intents.ts +0 -5
- package/src/engine/subscription-schedules.ts +0 -10
- package/src/engine/tax.ts +0 -39
- package/src/engine/terminal.ts +0 -23
- package/src/engine/test-clocks.ts +0 -11
- package/src/engine/tokens.ts +0 -22
- package/src/engine/treasury.ts +0 -47
- package/src/screens/connect-oauth.tsx +0 -376
- package/src/semantics/disputes.ts +0 -12
- package/src/semantics/invoiceitems.ts +0 -59
- package/src/semantics/setup_intents.ts +0 -45
- package/src/semantics/subscription_items.ts +0 -54
- package/src/semantics/subscription_schedules.ts +0 -66
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// Stripe's subscriptions operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
|
|
2
2
|
// handler contract's context, computing with the engine (../engine/).
|
|
3
3
|
import type { HandlerContext } from '@volter/world-core';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { type Row } from '../engine/common.ts';
|
|
5
|
+
import { keptAfter } from '../engine/renewals.ts';
|
|
6
|
+
import { asBool, nowUnix, subscriptionCouponParam, subscriptionHasPrice, subscriptionItemEntries } from '../engine/stripe.ts';
|
|
7
7
|
import { pauseCollection, SUB } from '../engine/subscriptions.ts';
|
|
8
|
-
import { advanceBilling, applyItemChanges, applyTrialEnd, at, clockNow, collectSubscriptionInvoice,
|
|
9
|
-
|
|
8
|
+
import { resourceView, advanceBilling, applyItemChanges, applyTrialEnd, at, clockNow, collectSubscriptionInvoice, expanded, itemChangesRefused, list, newest, redeemCoupon, replacedDiscounts, subMissing, trialEndBlocked, trialEndRefused, where } from './shared.ts';
|
|
9
|
+
import { deletedDiscount } from '../engine/common.ts';
|
|
10
|
+
import { fail } from './shared.ts';
|
|
10
11
|
// canceling keeps the subscription, now canceled, stamped with when it was canceled and ended
|
|
11
12
|
// (https://docs.stripe.com/api/subscriptions/object#subscription_object-canceled_at, #subscription_object-ended_at)
|
|
12
13
|
export async function DeleteSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
|
|
13
14
|
const id = at(ctx, 'subscription_exposed_id');
|
|
14
|
-
const sub = ctx
|
|
15
|
+
const sub = resourceView(ctx, SUB, id);
|
|
15
16
|
if (!sub) return subMissing(ctx, id);
|
|
16
17
|
const refused = ctx.legal(SUB, 'status', 'DeleteSubscriptionsSubscriptionExposedId', sub.status, undefined, id);
|
|
17
18
|
if (refused) return ctx.refuse(refused);
|
|
@@ -21,7 +22,7 @@ export async function DeleteSubscriptionsSubscriptionExposedId(ctx: HandlerConte
|
|
|
21
22
|
// `"reason": "cancellation_requested"` (docs.stripe.com/api/subscriptions/cancel)
|
|
22
23
|
const given = ctx.params.cancellation_details && typeof ctx.params.cancellation_details === 'object' ? (ctx.params.cancellation_details as Row) : {};
|
|
23
24
|
const cancellation_details = { comment: given.comment ?? null, feedback: given.feedback ?? null, reason: 'cancellation_requested' };
|
|
24
|
-
return ctx.reply(await ctx.write(SUB, id, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details }, 'subscription.cancel'));
|
|
25
|
+
return ctx.reply(ctx.expand(SUB, await ctx.write(SUB, id, { status: 'canceled', canceled_at: now, ended_at: now, cancellation_details }, 'subscription.cancel')));
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export async function GetSubscriptions(ctx: HandlerContext): Promise<Response> {
|
|
@@ -33,153 +34,18 @@ export async function GetSubscriptions(ctx: HandlerContext): Promise<Response> {
|
|
|
33
34
|
}));
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
export async function GetSubscriptionsSearch(ctx: HandlerContext): Promise<Response> {
|
|
37
|
-
await advanceBilling(ctx);
|
|
38
|
-
return search(ctx, SUB);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
37
|
export async function GetSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
|
|
42
38
|
const id = at(ctx, 'subscription_exposed_id');
|
|
43
39
|
await advanceBilling(ctx, (sub) => sub.id === id);
|
|
44
|
-
const sub = ctx
|
|
40
|
+
const sub = resourceView(ctx, SUB, id);
|
|
45
41
|
return sub ? ctx.reply(expanded(ctx, SUB, sub)) : subMissing(ctx, id);
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
export async function PostSubscriptions(ctx: HandlerContext): Promise<Response> {
|
|
49
|
-
const params = ctx.params;
|
|
50
|
-
const customer = typeof params.customer === 'string' ? params.customer : '';
|
|
51
|
-
if (!customer) return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
|
|
52
|
-
if (!ctx.get('customer', customer)) return fail(ctx, `No such customer: '${customer}'`, 404, 'resource_missing');
|
|
53
|
-
const now = clockNow(ctx, customer);
|
|
54
|
-
const badTrial = trialEndRefused(ctx, params.trial_end, now);
|
|
55
|
-
if (badTrial) return badTrial;
|
|
56
|
-
// a coupon (or discounts[0][coupon]) must exist; it becomes the subscription's discount
|
|
57
|
-
const couponId = subscriptionCouponParam(params);
|
|
58
|
-
let discounts: Row[] = [];
|
|
59
|
-
if (couponId) {
|
|
60
|
-
const coupon = ctx.get('coupon', couponId);
|
|
61
|
-
if (!coupon) return fail(ctx, `No such coupon: '${couponId}'`, 400, 'resource_missing');
|
|
62
|
-
discounts = [buildDiscount(coupon, customer, now)];
|
|
63
|
-
}
|
|
64
|
-
// a trial is the current period; otherwise one billing interval of the first item's price. The
|
|
65
|
-
// period sits top-level: this twin's default API version predates basil moving it onto items.
|
|
66
|
-
const trial = resolveTrial(params, now);
|
|
67
|
-
const itemEntries = subscriptionItemEntries(params.items);
|
|
68
|
-
const { interval, interval_count } = resolveSubscriptionBillingInterval(itemEntries, finder(ctx));
|
|
69
|
-
const periodStart = trial ? trial.start : now;
|
|
70
|
-
const periodEnd = trial ? trial.end : addBillingInterval(now, interval, interval_count);
|
|
71
|
-
const subId = typeof params.id === 'string' && params.id ? params.id : ctx.mint(SUB);
|
|
72
|
-
const itemsList = buildSubscriptionItemsList(itemEntries, subId, now, finder(ctx));
|
|
73
|
-
const itemsData = itemsList.data as Row[];
|
|
74
|
-
const currency = typeof params.currency === 'string' && params.currency ? params.currency : 'usd';
|
|
75
|
-
|
|
76
|
-
// Stripe opens a subscription's first invoice at once (a trialing one has nothing due yet) and, by
|
|
77
|
-
// payment_behavior (docs.stripe.com/api/subscriptions/create#create_subscription-payment_behavior):
|
|
78
|
-
// allow_incomplete (the default) charges the default payment method now, and without one, or on a decline,
|
|
79
|
-
// leaves the subscription incomplete with the invoice open; error_if_incomplete refuses the create instead;
|
|
80
|
-
// default_incomplete never charges, leaving a PaymentIntent the frontend confirms.
|
|
81
|
-
const behavior = typeof params.payment_behavior === 'string' ? params.payment_behavior : 'allow_incomplete';
|
|
82
|
-
const needsFirstInvoice = !trial;
|
|
83
|
-
const holder = ctx.get('customer', customer) ?? {};
|
|
84
|
-
const payWith = behavior === 'default_incomplete' ? undefined
|
|
85
|
-
: (typeof params.default_payment_method === 'string' ? params.default_payment_method : undefined) ?? ((holder.invoice_settings as Row | undefined)?.default_payment_method as string | undefined) ?? undefined;
|
|
86
|
-
const declined = payWith ? declineFor(finder(ctx), { payment_method: payWith }) : undefined;
|
|
87
|
-
let subtotal = 0;
|
|
88
|
-
for (const it of itemsData) {
|
|
89
|
-
const price = it.price && typeof it.price === 'object' ? (it.price as Row) : undefined;
|
|
90
|
-
subtotal += priceAmount(price, itemQuantity(it));
|
|
91
|
-
}
|
|
92
|
-
const coupon = discounts[0]?.coupon as Row | undefined;
|
|
93
|
-
const discountAmount = applyCouponDiscount(subtotal, coupon);
|
|
94
|
-
const invoiceTotal = Math.max(0, subtotal - discountAmount);
|
|
95
|
-
// a $0 first invoice is paid at once and needs no PaymentIntent
|
|
96
|
-
const isFree = invoiceTotal <= 0;
|
|
97
|
-
const pays = needsFirstInvoice && !isFree && !!payWith && !declined;
|
|
98
|
-
if (needsFirstInvoice && !isFree && !pays && behavior === 'error_if_incomplete') return firstPaymentRefused(ctx, declined);
|
|
99
|
-
// minted first so the subscription names its latest_invoice in its own create (a trial's $0 invoice too)
|
|
100
|
-
const invoiceId = needsFirstInvoice ? ctx.mint('invoice') : null;
|
|
101
|
-
const trialInvoiceId = needsFirstInvoice ? null : ctx.mint('invoice');
|
|
102
|
-
const piId = needsFirstInvoice && !isFree ? ctx.mint('payment_intent') : null;
|
|
103
|
-
|
|
104
|
-
const { coupon: _c, discounts: _ds, trial_period_days: _tpd, trial_end: _te, items: _items, id: _id, ...rest } = params;
|
|
105
|
-
const sub = await created(ctx, SUB, { ...rest, id: subId }, {
|
|
106
|
-
status: trial ? 'trialing' : needsFirstInvoice && !isFree && !pays ? 'incomplete' : 'active',
|
|
107
|
-
livemode: false, currency, collection_method: 'charge_automatically',
|
|
108
|
-
// a trial's end is the billing anchor: "The `billing_cycle_anchor` will be updated to the `trial_end` value"
|
|
109
|
-
// (docs.stripe.com/api/subscriptions/create#create_subscription-trial_end)
|
|
110
|
-
cancel_at_period_end: false, start_date: now, billing_cycle_anchor: trial ? trial.end : now, metadata: {},
|
|
111
|
-
discounts: discounts.map((d) => ({ ...d, subscription: subId })), billing_schedules: [],
|
|
112
|
-
...(trial ? { trial_start: trial.start, trial_end: trial.end } : { trial_start: null, trial_end: null }),
|
|
113
|
-
current_period_start: periodStart, current_period_end: periodEnd,
|
|
114
|
-
items: itemsList,
|
|
115
|
-
latest_invoice: invoiceId ?? trialInvoiceId,
|
|
116
|
-
automatic_tax: { enabled: false, liability: null },
|
|
117
|
-
billing_mode: { type: 'classic' },
|
|
118
|
-
invoice_settings: { issuer: { type: 'self' } },
|
|
119
|
-
});
|
|
120
|
-
await storeItems(ctx, subId, itemsData);
|
|
121
|
-
if (discounts.length) await redeemCoupon(ctx, couponId);
|
|
122
|
-
// a customer billed for the first time takes the subscription's currency
|
|
123
|
-
if (!holder.currency) await ctx.write('customer', customer, { currency }, 'customer.update');
|
|
124
|
-
if (!invoiceId) {
|
|
125
|
-
// a trial's subscription still opens an invoice at once: "An immediate invoice is still created, but the amount is 0"
|
|
126
|
-
// (docs.stripe.com/billing/subscriptions/trials/free-trials); finalizing it spends a once coupon (renewals.ts)
|
|
127
|
-
const trialInvoice = await draftSubscriptionInvoice(ctx, ctx.get(SUB, subId) ?? sub, { start: periodStart, end: periodEnd, reason: 'subscription_create', trial: true, ...(trialInvoiceId ? { id: trialInvoiceId } : {}) });
|
|
128
|
-
await collectSubscriptionInvoice(ctx, trialInvoice, now, 'api');
|
|
129
|
-
return ctx.reply(expanded(ctx, SUB, ctx.get(SUB, subId) ?? sub));
|
|
130
|
-
}
|
|
131
|
-
// the default payment method pays the first invoice now: its charge (with its ledger entry) comes first
|
|
132
|
-
const chargeId = pays && piId ? await mintCharge(ctx, piId, { currency, customer, payment_method: payWith, invoice: invoiceId }, invoiceTotal) : null;
|
|
133
|
-
|
|
134
|
-
const invoiceLines = itemsData.map((it, i) => {
|
|
135
|
-
const price = it.price && typeof it.price === 'object' ? (it.price as Row) : undefined;
|
|
136
|
-
const quantity = itemQuantity(it);
|
|
137
|
-
return {
|
|
138
|
-
id: `il_twin_${invoiceId}_${i + 1}`, object: 'line_item', type: 'subscription',
|
|
139
|
-
amount: priceAmount(price, quantity), currency, quantity, proration: false,
|
|
140
|
-
price: it.price ?? null, subscription: subId, invoice_item: null, subscription_item: it.id ?? null,
|
|
141
|
-
period: { start: periodStart, end: periodEnd }, description: null,
|
|
142
|
-
};
|
|
143
|
-
});
|
|
144
|
-
await created(ctx, 'invoice', {
|
|
145
|
-
id: invoiceId,
|
|
146
|
-
...finalizedFields(ctx, { id: invoiceId, customer }, now),
|
|
147
|
-
status: isFree || pays ? 'paid' : 'open', livemode: false, currency, collection_method: 'charge_automatically',
|
|
148
|
-
auto_advance: !(isFree || pays), attempt_count: payWith && !isFree ? 1 : 0, attempted: !isFree && behavior !== 'default_incomplete',
|
|
149
|
-
customer, subscription: subId, billing_reason: 'subscription_create', charge: chargeId,
|
|
150
|
-
amount_due: invoiceTotal, amount_paid: isFree || pays ? invoiceTotal : 0, amount_remaining: isFree || pays ? 0 : invoiceTotal,
|
|
151
|
-
amount_overpaid: 0, amount_paid_off_stripe: 0, amount_shipping: 0,
|
|
152
|
-
subtotal, total: invoiceTotal, starting_balance: 0,
|
|
153
|
-
post_payment_credit_notes_amount: 0, pre_payment_credit_notes_amount: 0,
|
|
154
|
-
period_start: periodStart, period_end: periodEnd,
|
|
155
|
-
default_tax_rates: [], discounts: discounts[0] ? [discounts[0]] : [],
|
|
156
|
-
lines: { object: 'list', data: invoiceLines, has_more: false, total_count: invoiceLines.length, url: `/v1/invoices/${invoiceId}/lines` },
|
|
157
|
-
automatic_tax: { enabled: false, liability: null, status: null },
|
|
158
|
-
status_transitions: { finalized_at: now, marked_uncollectible_at: null, paid_at: isFree || pays ? now : null, voided_at: null },
|
|
159
|
-
issuer: { type: 'self' },
|
|
160
|
-
payment_settings: { default_mandate: null, payment_method_options: null, payment_method_types: null },
|
|
161
|
-
payment_intent: piId,
|
|
162
|
-
}, {});
|
|
163
|
-
if (piId) {
|
|
164
|
-
// paid by the default payment method, or waiting on one (declined, absent, or collected client-side)
|
|
165
|
-
await created(ctx, 'payment_intent', { id: piId, amount: invoiceTotal, currency, customer, invoice: invoiceId, ...(payWith ? { payment_method: pays ? payWith : null } : {}) }, {
|
|
166
|
-
status: pays ? 'succeeded' : 'requires_payment_method', client_secret: mintClientSecret(piId, await ctx.secret(`client-secret:${piId}`)), livemode: false,
|
|
167
|
-
capture_method: 'automatic', amount_capturable: 0, amount_received: pays ? invoiceTotal : 0, next_action: null,
|
|
168
|
-
latest_charge: chargeId,
|
|
169
|
-
last_payment_error: declined ? { type: 'card_error', code: declined.code, decline_code: declined.decline_code ?? null, message: declined.message, payment_method: { id: payWith } } : null,
|
|
170
|
-
automatic_payment_methods: null, payment_method_types: ['card', 'link'], payment_method_options: {},
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
// the first invoice is finalized: the `once` coupon it took is spent (renewals.ts spendOnceCoupon)
|
|
174
|
-
await spendOnceCoupon(ctx, ctx.get('invoice', invoiceId) ?? {});
|
|
175
|
-
return ctx.reply(expanded(ctx, SUB, ctx.get(SUB, subId) ?? sub));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
44
|
export async function PostSubscriptionsSubscriptionExposedId(ctx: HandlerContext): Promise<Response> {
|
|
179
45
|
const id = at(ctx, 'subscription_exposed_id');
|
|
180
46
|
// a period that has already ended renews (or a trial that has, ends) before the update applies (renewals.ts)
|
|
181
47
|
await advanceBilling(ctx, (s) => s.id === id);
|
|
182
|
-
const sub = ctx
|
|
48
|
+
const sub = resourceView(ctx, SUB, id);
|
|
183
49
|
if (!sub) return subMissing(ctx, id);
|
|
184
50
|
const params = ctx.params;
|
|
185
51
|
const now = clockNow(ctx, sub.customer);
|
|
@@ -223,5 +89,19 @@ export async function PostSubscriptionsSubscriptionExposedId(ctx: HandlerContext
|
|
|
223
89
|
fields.status = pays ? 'active' : 'past_due';
|
|
224
90
|
if (fields.discounts !== undefined) fields.discounts = keptAfter(fields.discounts as Row[], ctx.get('invoice', trial.invoice) ?? {});
|
|
225
91
|
}
|
|
226
|
-
return ctx.reply(await ctx.write(SUB, id, fields, op));
|
|
92
|
+
return ctx.reply(ctx.expand(SUB, await ctx.write(SUB, id, fields, op)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
export async function DeleteSubscriptionsSubscriptionExposedIdDiscount(ctx: HandlerContext): Promise<Response> {
|
|
98
|
+
const id = at(ctx, 'subscription_exposed_id');
|
|
99
|
+
const sub = resourceView(ctx, SUB, id);
|
|
100
|
+
if (!sub) return subMissing(ctx, id);
|
|
101
|
+
// source: spec:DeleteSubscriptionsSubscriptionExposedIdDiscount "Removes the currently applied discount on a subscription."
|
|
102
|
+
const current = ((sub.discounts as unknown[] | undefined) ?? [])[0] ?? sub.discount;
|
|
103
|
+
// Where the documentation stops and the twin decides: a subscription with no discount answers its absence as a missing resource.
|
|
104
|
+
if (!current) return fail(ctx, `No active discount for subscription: '${id}'`, 404, 'resource_missing');
|
|
105
|
+
await ctx.write(SUB, id, { discounts: [] }, 'subscription.update');
|
|
106
|
+
return ctx.reply(deletedDiscount(typeof current === 'string' ? { id: current } : { ...(current as Row), subscription: id }, nowUnix(ctx.occurredAt)));
|
|
227
107
|
}
|
|
@@ -4,50 +4,116 @@ import type { HandlerContext } from '@volter/world-core';
|
|
|
4
4
|
import type { Row } from '../engine/common.ts';
|
|
5
5
|
import { AUTH, CARD, cardId, CH, historyEntry, holderId, merchantData, TXN } from '../engine/issuing.ts';
|
|
6
6
|
import { isValidApiVersion, nowUnix, spendingControlsViolation, TWIN_API_VERSION } from '../engine/stripe.ts';
|
|
7
|
-
import { READER } from '../engine/terminal.ts';
|
|
8
|
-
import { CLOCK } from '../engine/test-clocks.ts';
|
|
9
|
-
import { FA, TX } from '../engine/treasury.ts';
|
|
10
7
|
import { stripeEventMatches } from '../engine/webhooks.ts';
|
|
11
|
-
import { answeredByEndpoint, approvedAtOnce, askForAuthorization, at, authMissing, balanceOf, capture,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
if (!
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
import { answeredByEndpoint, approvedAtOnce, askForAuthorization, at, authMissing, balanceOf, capture, created, fail, issuingRefusal, withCard } from './shared.ts';
|
|
9
|
+
export async function PostTestHelpersIssuingAuthorizations(ctx: HandlerContext): Promise<Response> {
|
|
10
|
+
const params = ctx.params;
|
|
11
|
+
const card = typeof params.card === 'string' ? params.card : '';
|
|
12
|
+
if (!card) return fail(ctx, 'Missing required param: card.', 400, 'parameter_missing');
|
|
13
|
+
const c = ctx.get(CARD, card);
|
|
14
|
+
if (!c) return fail(ctx, `No such card: '${card}'`, 400, 'resource_missing');
|
|
15
|
+
if (params.amount === undefined) return fail(ctx, 'Missing required param: amount.', 400, 'parameter_missing');
|
|
16
|
+
const amount = Math.trunc(Number(params.amount) || 0);
|
|
17
|
+
const currency = String(params.currency ?? c.currency ?? 'usd');
|
|
18
|
+
const createdSec = Number(nowUnix(ctx.occurredAt));
|
|
19
|
+
const cardholderId = holderId(c);
|
|
20
|
+
const merchant = merchantData(params.merchant_data);
|
|
21
|
+
const isAmountControllable = params.is_amount_controllable === true;
|
|
22
|
+
const authMethod = typeof params.authorization_method === 'string' ? params.authorization_method : 'online';
|
|
23
|
+
if (!['chip', 'contactless', 'keyed_in', 'online', 'swipe'].includes(authMethod)) return issuingRefusal(ctx, 'authorization_method');
|
|
24
|
+
const merchantAmount = params.merchant_amount !== undefined ? Math.trunc(Number(params.merchant_amount) || 0) : amount;
|
|
25
|
+
const merchantCurrency = typeof params.merchant_currency === 'string' ? params.merchant_currency : currency;
|
|
26
|
+
const authId = ctx.mint(AUTH);
|
|
27
|
+
const baseFields: Row = {
|
|
28
|
+
amount, amount_details: null, authorization_method: authMethod,
|
|
29
|
+
balance_transactions: [], transactions: [],
|
|
30
|
+
currency, fleet: null, fuel: null, livemode: false,
|
|
31
|
+
merchant_amount: merchantAmount, merchant_currency: merchantCurrency, merchant_data: merchant,
|
|
32
|
+
metadata: params.metadata && typeof params.metadata === 'object' ? params.metadata : {},
|
|
33
|
+
network_data: null,
|
|
34
|
+
verification_data: {
|
|
35
|
+
address_line1_check: 'not_provided', address_postal_code_check: 'not_provided',
|
|
36
|
+
authentication_exemption: null, cvc_check: 'match', expiry_check: 'match',
|
|
37
|
+
postal_code: null, three_d_secure: null,
|
|
38
|
+
},
|
|
39
|
+
wallet: null,
|
|
40
|
+
};
|
|
41
|
+
const subject = { id: authId, card, cardholder: cardholderId };
|
|
42
|
+
const entry = (o: { amount: number; approved: boolean; reason: string; reasonMessage: string | null }) => historyEntry({ ...o, currency, createdSec, merchantAmount, merchantCurrency });
|
|
43
|
+
const decline = async (reason: string, reasonMessage: string | null) =>
|
|
44
|
+
ctx.reply(withCard(ctx, await created(ctx, AUTH, subject, { ...baseFields, status: 'closed', approved: false, pending_request: null, request_history: [entry({ amount, approved: false, reason, reasonMessage })] })));
|
|
45
|
+
if (c.status === 'canceled') return decline('card_canceled', 'The card has been canceled.');
|
|
46
|
+
// source: https://docs.stripe.com/api/issuing/cards/create "Defaults to inactive"
|
|
47
|
+
// source: spec:/components/schemas/issuing_authorization_request/properties/reason/enum "card_inactive"
|
|
48
|
+
if (c.status !== 'active') return decline('card_inactive', 'The card is inactive.');
|
|
49
|
+
// what earlier approved authorizations on the card (or cardholder) already spent, for the limits
|
|
50
|
+
const prior = (key: 'card' | 'cardholder', value: string) =>
|
|
51
|
+
ctx.rows(AUTH).filter((a) => a[key] === value && a.approved === true)
|
|
52
|
+
.map((a) => ({ amount: Number(a.amount) || 0, created: Number(a.created) || 0, category: String((a.merchant_data as Row | undefined)?.category ?? '') }));
|
|
53
|
+
const controlsOpts = { amount, category: String(merchant.category ?? ''), country: merchant.country == null ? null : String(merchant.country), nowSec: createdSec };
|
|
54
|
+
const cardholderRow = cardholderId ? ctx.get(CH, cardholderId) : undefined;
|
|
55
|
+
const violation =
|
|
56
|
+
spendingControlsViolation(c.spending_controls, { ...controlsOpts, priorApproved: prior('card', card) }) ??
|
|
57
|
+
spendingControlsViolation(cardholderRow?.spending_controls, { ...controlsOpts, priorApproved: prior('cardholder', cardholderId) });
|
|
58
|
+
if (violation) return decline('spending_controls', violation);
|
|
59
|
+
// an authorization is paid from the Issuing balance, and declined when it cannot cover it
|
|
60
|
+
// (docs.stripe.com/issuing/funding/balance)
|
|
61
|
+
if ((balanceOf(ctx).issuing.get(currency) ?? 0) < amount) return decline('insufficient_funds', 'Your Issuing balance does not have enough funds for this authorization.');
|
|
62
|
+
const pendingRequest = {
|
|
63
|
+
amount, amount_details: null, currency, is_amount_controllable: isAmountControllable,
|
|
64
|
+
merchant_amount: merchantAmount, merchant_currency: merchantCurrency, network_risk_score: null,
|
|
65
|
+
};
|
|
66
|
+
const endpoint = ctx.rows('webhook_endpoint').find((w) => w.status === 'enabled' && stripeEventMatches(w.enabled_events, 'issuing_authorization.request'));
|
|
67
|
+
// no real-time enrollment: "If you don't have a real-time authorization webhook, we approve the authorization without
|
|
68
|
+
// sending the issuing_authorization.request" (docs.stripe.com/issuing/purchases/authorizations), its outcome
|
|
69
|
+
// card_active, "approved according to your default Issuing settings"
|
|
70
|
+
if (!endpoint) return approvedAtOnce(ctx, subject, baseFields, amount, entry({ amount, approved: true, reason: 'card_active', reasonMessage: null }));
|
|
71
|
+
// the request event's authorization carries pending_request, non-null only during the request
|
|
72
|
+
const requestObject = withCard(ctx, { object: 'issuing.authorization', id: authId, created: createdSec, ...baseFields, card, cardholder: cardholderId, status: 'pending', approved: false, pending_request: pendingRequest, request_history: [] });
|
|
73
|
+
// the request asked of the endpoint and the stored event are one event under one id
|
|
74
|
+
const pinned = ctx.call.request.headers.get('stripe-version');
|
|
75
|
+
const requestEvent = {
|
|
76
|
+
id: ctx.mint('event'), object: 'event', api_version: pinned && isValidApiVersion(pinned) ? pinned : TWIN_API_VERSION, created: createdSec,
|
|
77
|
+
data: { object: requestObject }, livemode: false, pending_webhooks: 1, request: { id: null, idempotency_key: null }, type: 'issuing_authorization.request',
|
|
78
|
+
};
|
|
79
|
+
await ctx.write('event', requestEvent.id, requestEvent, 'event.record');
|
|
80
|
+
const outcome = await askForAuthorization(ctx, String(endpoint.url), requestEvent, String(endpoint.secret ?? ''));
|
|
81
|
+
// a request no one answered (the World refused the delivery, it failed, or it timed out) is not a decision: the
|
|
82
|
+
// authorization stays pending with its request, for the deprecated approve or decline "within the timeout window of
|
|
83
|
+
// the real-time authorization flow" (the served spec), until lapseRealtimeRequests decides it when the window ends
|
|
84
|
+
if (outcome.kind === 'timeout') return ctx.reply(withCard(ctx, await created(ctx, AUTH, subject, { ...baseFields, status: 'pending', approved: false, request_history: [], pending_request: pendingRequest })));
|
|
85
|
+
return answeredByEndpoint(ctx, outcome, { subject, fields: baseFields, amount, controllable: isAmountControllable, entry, decline });
|
|
28
86
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function PostTestHelpersTestClocksTestClockAdvance(ctx: HandlerContext): Promise<Response> {
|
|
39
|
-
const id = at(ctx, 'test_clock');
|
|
40
|
-
const clock = ctx.get(CLOCK, id);
|
|
41
|
-
if (!clock) return fail(ctx, `No such test clock: '${id}'`, 404, 'resource_missing');
|
|
42
|
-
if (ctx.params.frozen_time === undefined) return fail(ctx, 'Missing required param: frozen_time.', 400, 'parameter_missing');
|
|
43
|
-
const to = Math.trunc(Number(ctx.params.frozen_time) || 0);
|
|
44
|
-
if (to <= Number(clock.frozen_time)) return fail(ctx, 'frozen_time must be after the current clock time.', 400, 'parameter_invalid');
|
|
45
|
-
const refused = ctx.legal(CLOCK, 'status', 'PostTestHelpersTestClocksTestClockAdvance', clock.status, 'advancing', id);
|
|
87
|
+
export async function PostTestHelpersIssuingAuthorizationsAuthorizationCapture(ctx: HandlerContext): Promise<Response> {
|
|
88
|
+
const id = at(ctx, 'authorization');
|
|
89
|
+
const auth = ctx.get(AUTH, id);
|
|
90
|
+
if (!auth) return authMissing(ctx, id);
|
|
91
|
+
if (auth.approved !== true || auth.status !== 'pending') return issuingRefusal(ctx, 'not_capturable', auth);
|
|
92
|
+
const closeAuthorization = ctx.params.close_authorization !== false;
|
|
93
|
+
const refused = ctx.legal(AUTH, 'status', 'PostTestHelpersIssuingAuthorizationsAuthorizationCapture', auth.status, closeAuthorization ? 'closed' : 'pending');
|
|
46
94
|
if (refused) return ctx.refuse(refused);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
const txns = Array.isArray(auth.transactions) ? auth.transactions : [];
|
|
96
|
+
const capturedSoFar = txns.map((t) => ctx.get(TXN, String(t))).reduce((s, t) => s + Math.abs(Number(t?.amount) || 0), 0);
|
|
97
|
+
const remaining = (Number(auth.amount) || 0) - capturedSoFar;
|
|
98
|
+
const captureAmount = ctx.params.capture_amount !== undefined ? Math.trunc(Number(ctx.params.capture_amount) || 0) : remaining;
|
|
99
|
+
if (captureAmount <= 0 || captureAmount > remaining) return fail(ctx, 'Invalid capture_amount: must be a positive integer no greater than the uncaptured authorized amount.', 400, 'parameter_invalid_integer');
|
|
100
|
+
// the capture releases what the approval held and debits the transaction; a capture that closes the authorization
|
|
101
|
+
// releases ALL it still holds, the uncaptured rest included: the amount is held "until the authorization is either
|
|
102
|
+
// captured, voided, or expired without capture" (docs.stripe.com/issuing/purchases/authorizations), and a closed
|
|
103
|
+
// authorization can be captured no further (close_authorization "Defaults to true. Set to false to enable
|
|
104
|
+
// multi-capture flows", the served spec). A capture that keeps it open releases only what it captured.
|
|
105
|
+
const currency = typeof auth.currency === 'string' ? auth.currency : 'usd';
|
|
106
|
+
const released = closeAuthorization ? remaining : captureAmount;
|
|
107
|
+
const release = await created(ctx, 'balance_transaction', {}, {
|
|
108
|
+
amount: released, currency, fee: 0, net: released, type: 'issuing_authorization_release',
|
|
109
|
+
status: 'available', balance_type: 'issuing', reporting_category: 'issuing_authorization_release',
|
|
110
|
+
available_on: nowUnix(ctx.occurredAt), fee_details: [], source: id,
|
|
111
|
+
});
|
|
112
|
+
const captured = await capture(ctx, id, { ...auth, card: cardId(auth) }, captureAmount);
|
|
113
|
+
return ctx.reply(withCard(ctx, await ctx.write(AUTH, id, {
|
|
114
|
+
status: closeAuthorization ? 'closed' : 'pending',
|
|
115
|
+
transactions: [...txns, captured.txnId],
|
|
116
|
+
balance_transactions: [...(Array.isArray(auth.balance_transactions) ? auth.balance_transactions : []), release.id, captured.btId],
|
|
117
|
+
}, 'issuing_authorization.updated'),
|
|
118
|
+
));
|
|
52
119
|
}
|
|
53
|
-
|
|
@@ -4,8 +4,7 @@ import type { HandlerContext } from '@volter/world-core';
|
|
|
4
4
|
import type { Row } from '../engine/common.ts';
|
|
5
5
|
import { nowUnix, validateMoney } from '../engine/stripe.ts';
|
|
6
6
|
import { emptyList } from '../engine/transfers.ts';
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import { created, fail, refusePayout, send, settledSourceRefused, settleTransfer, transfersInactive } from './shared.ts';
|
|
9
8
|
export async function PostTransfers(ctx: HandlerContext): Promise<Response> {
|
|
10
9
|
const bad = validateMoney(ctx.params);
|
|
11
10
|
if (bad) return send(ctx, bad);
|
|
@@ -51,4 +50,3 @@ export async function PostTransfers(ctx: HandlerContext): Promise<Response> {
|
|
|
51
50
|
}),
|
|
52
51
|
);
|
|
53
52
|
}
|
|
54
|
-
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Stripe's webhook_endpoints operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
|
|
2
|
+
// handler contract's context, computing with the engine (../engine/).
|
|
3
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
4
|
+
import { asBool } from '../engine/stripe.ts';
|
|
5
|
+
import { WE } from '../engine/webhook-endpoints.ts';
|
|
6
|
+
import { created, fail } from './shared.ts';
|
|
7
|
+
// an endpoint is enabled at once and signs each event with a secret of its own, answered by this create alone (the
|
|
8
|
+
// around answers no other read of it)
|
|
9
|
+
export async function PostWebhookEndpoints(ctx: HandlerContext): Promise<Response> {
|
|
10
|
+
const params = ctx.params;
|
|
11
|
+
// source: spec:PostWebhookEndpoints "The URL of the webhook endpoint."
|
|
12
|
+
if (typeof params.url !== 'string' || params.url === '') return fail(ctx, 'Missing required param: url.', 400, 'parameter_missing');
|
|
13
|
+
// source: spec:PostWebhookEndpoints "The list of events to enable for this endpoint."
|
|
14
|
+
if (params.enabled_events === undefined || params.enabled_events === '' || (Array.isArray(params.enabled_events) && !params.enabled_events.length)) return fail(ctx, 'Missing required param: enabled_events.', 400, 'parameter_missing');
|
|
15
|
+
const id = ctx.mint(WE);
|
|
16
|
+
// source: spec:/components/schemas/webhook_endpoint/properties/secret/description "The endpoint's secret, used to generate"
|
|
17
|
+
const secret = `whsec_${(await ctx.secret(`webhook-endpoint-secret:${id}`)).replace(/[^A-Za-z0-9]/g, '').slice(0, 32)}`;
|
|
18
|
+
return ctx.reply(await created(ctx, WE, { ...params, id }, {
|
|
19
|
+
url: params.url, enabled_events: Array.isArray(params.enabled_events) ? params.enabled_events.map(String) : [String(params.enabled_events)],
|
|
20
|
+
// source: spec:/components/schemas/webhook_endpoint/properties/status/description "The status of the webhook."
|
|
21
|
+
status: 'enabled', livemode: false, metadata: {}, application: null,
|
|
22
|
+
api_version: typeof params.api_version === 'string' ? params.api_version : null,
|
|
23
|
+
description: typeof params.description === 'string' ? params.description : null,
|
|
24
|
+
// source: spec:PostWebhookEndpoints "Whether this endpoint should receive events from connected accounts"
|
|
25
|
+
connect: asBool(params.connect), secret,
|
|
26
|
+
}));
|
|
27
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// FETCH-FIRST (runtime contract R12b): a fixed file, the kernel's serve seam around the pack's fetch (./fetch.ts).
|
|
2
2
|
import { serveHttp, type PackFetchOptions } from '@volter/world-core';
|
|
3
3
|
import { createStripeFetch } from './fetch.ts';
|
|
4
|
-
|
|
5
4
|
export async function createStripeTwinServer(options: PackFetchOptions & { port?: number } = {}): Promise<{ port: number; stop: () => void }> {
|
|
6
5
|
const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch: createStripeFetch(options) });
|
|
7
6
|
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"//": "GENERATED by scripts/pack-facts.ts — current pack descriptors and explicit world-runtime server backings; no prior-artifact entries are retained.",
|
|
3
|
-
"packs": {
|
|
4
|
-
"stripe": {
|
|
5
|
-
"adoption": {
|
|
6
|
-
"pypi": [
|
|
7
|
-
"stripe"
|
|
8
|
-
],
|
|
9
|
-
"sdks": [
|
|
10
|
-
"stripe"
|
|
11
|
-
],
|
|
12
|
-
"envStems": [
|
|
13
|
-
"STRIPE",
|
|
14
|
-
"NEXTPRIVATESTRIPE"
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
"hosts": [
|
|
18
|
-
{
|
|
19
|
-
"host": "api.stripe.com"
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"host": "checkout.stripe.com",
|
|
23
|
-
"pathPattern": "^/c/pay/"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"host": "billing.stripe.com",
|
|
27
|
-
"pathPattern": "^/p/session/"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"host": "connect.stripe.com",
|
|
31
|
-
"pathPattern": "^/setup/|^/oauth/(authorize|token|deauthorize)/?$"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"host": "dashboard.stripe.com",
|
|
35
|
-
"pathPattern": "^/settings/public/?$|^/(test/)?settings/connect(/onboarding-options/oauth)?/?$"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"host": "verify.stripe.com",
|
|
39
|
-
"pathPattern": "^/start/"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"host": "js.stripe.com",
|
|
43
|
-
"pathPattern": "^/v3/financial-connections/|^/v3/?$|^/(v3|[a-z]+)/stripe\\.js$"
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"archetype": "crud",
|
|
47
|
-
"protocol": {
|
|
48
|
-
"declared": "3",
|
|
49
|
-
"major": 3,
|
|
50
|
-
"standing": "current"
|
|
51
|
-
},
|
|
52
|
-
"transport": "rest",
|
|
53
|
-
"resources": [
|
|
54
|
-
"charge",
|
|
55
|
-
"customer",
|
|
56
|
-
"payment_intent",
|
|
57
|
-
"setup_intent",
|
|
58
|
-
"payment_method",
|
|
59
|
-
"subscription",
|
|
60
|
-
"price",
|
|
61
|
-
"product",
|
|
62
|
-
"invoice",
|
|
63
|
-
"invoiceitem",
|
|
64
|
-
"refund",
|
|
65
|
-
"event"
|
|
66
|
-
],
|
|
67
|
-
"credentialDoor": {
|
|
68
|
-
"path": "/_twin/app-credentials",
|
|
69
|
-
"body": {},
|
|
70
|
-
"fill": {
|
|
71
|
-
"STRIPE_SECRET_KEY": "secret_key",
|
|
72
|
-
"STRIPE_API_KEY": "secret_key",
|
|
73
|
-
"STRIPE_PRIVATE_KEY": "secret_key",
|
|
74
|
-
"BILLING_STRIPE_API_KEY": "secret_key",
|
|
75
|
-
"STRIPE_PUBLISHABLE_KEY": "publishable_key",
|
|
76
|
-
"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY": "publishable_key",
|
|
77
|
-
"NEXT_PUBLIC_STRIPE_PUBLIC_KEY": "publishable_key",
|
|
78
|
-
"NEXT_PUBLIC_STRIPE_KEY": "publishable_key",
|
|
79
|
-
"STRIPE_WEBHOOK_SECRET": "webhook_secret",
|
|
80
|
-
"STRIPE_WEBHOOK_SIGNING_SECRET": "webhook_secret",
|
|
81
|
-
"STRIPE_ENDPOINT_SECRET": "webhook_secret",
|
|
82
|
-
"STRIPE_WEBHOOK_SECRET_KEY": "webhook_secret",
|
|
83
|
-
"NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET": "webhook_secret",
|
|
84
|
-
"BILLING_STRIPE_WEBHOOK_SECRET": "webhook_secret",
|
|
85
|
-
"STRIPE_CONNECT_WEBHOOK_SECRET": "connect_webhook_secret",
|
|
86
|
-
"STRIPE_WEBHOOK_SECRET_CONNECT": "connect_webhook_secret",
|
|
87
|
-
"STRIPE_CONNECT_WEBHOOK_SIGNING_SECRET": "connect_webhook_secret"
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
"serveExport": "createStripeTwinServer"
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
"undeclared": []
|
|
94
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export const METER = 'billing.meter';
|
|
2
|
-
export const objectOf = (v) => (v && typeof v === 'object' ? v : {});
|
|
3
|
-
// ── credit grants: a customer's prepaid credit, paid or promotional, in a monetary amount ──
|
|
4
|
-
export const GRANT = 'billing.credit_grant';
|
|
5
|
-
// ── alerts: a usage threshold on a meter ──
|
|
6
|
-
export const ALERT = 'billing.alert';
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { Row } from './common.js';
|
|
2
|
-
/** The create's parameters as the charge keeps them: without `card`, the request's card details ("A token, like the ones
|
|
3
|
-
* returned by Stripe.js", or a hash of the number, expiry and CVC). The Charge object has no `card` property (served
|
|
4
|
-
* spec, charge); the card it was made with is its payment_method_details. Kept, it answered the card's full number and
|
|
5
|
-
* CVC back on every read of the charge. */
|
|
6
|
-
export declare function withoutCard(params: Row): Row;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// A charge is written as the event Stripe sends for it (docs.stripe.com/api/events/types; Stripe has no
|
|
2
|
-
// `charge.created`): charge.succeeded, "Occurs whenever a charge is successful", for one made or authorized
|
|
3
|
-
// (capture=false: its status is succeeded, captured false), and charge.failed, "Occurs whenever a failed charge attempt
|
|
4
|
-
// occurs", for a declined attempt. The twin makes no pending charge (charge.pending, "Occurs whenever a pending charge is
|
|
5
|
-
// created"): its charges settle when made.
|
|
6
|
-
/** The create's parameters as the charge keeps them: without `card`, the request's card details ("A token, like the ones
|
|
7
|
-
* returned by Stripe.js", or a hash of the number, expiry and CVC). The Charge object has no `card` property (served
|
|
8
|
-
* spec, charge); the card it was made with is its payment_method_details. Kept, it answered the card's full number and
|
|
9
|
-
* CVC back on every read of the charge. */
|
|
10
|
-
export function withoutCard(params) {
|
|
11
|
-
const { card: _card, ...rest } = params;
|
|
12
|
-
return rest;
|
|
13
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/** The platform's test client_id: the World's one platform (acct_twin_self) is the application it names. */
|
|
2
|
-
export declare const PLATFORM_CLIENT_ID = "ca_twin_self";
|
|
3
|
-
export declare const SETTINGS_URL = "https://dashboard.stripe.com/settings/connect/onboarding-options/oauth";
|
|
4
|
-
/** The page's addresses: the docs' own, its test-mode link, and Connect's settings root. */
|
|
5
|
-
export declare const CONNECT_SETTINGS_PATH: RegExp;
|
|
6
|
-
export declare const SETTINGS = "_connect_oauth_settings";
|
|
7
|
-
export type ConnectOAuthSettings = {
|
|
8
|
-
oauth_enabled: boolean;
|
|
9
|
-
redirect_uris: string[];
|
|
10
|
-
};
|
|
11
|
-
/** The redirect URIs a form's field names, or the first one refused and why. */
|
|
12
|
-
export declare function redirectUris(field: string): {
|
|
13
|
-
uris: string[];
|
|
14
|
-
} | {
|
|
15
|
-
refused: string;
|
|
16
|
-
};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// The platform's Connect OAuth settings (its client_id, whether OAuth is on, its redirect URIs), which the Dashboard's
|
|
2
|
-
// settings page saves (../screens/dashboard.tsx) and connect.stripe.com/oauth/authorize reads (../screens/connect-oauth.tsx).
|
|
3
|
-
/** The platform's test client_id: the World's one platform (acct_twin_self) is the application it names. */
|
|
4
|
-
export const PLATFORM_CLIENT_ID = 'ca_twin_self';
|
|
5
|
-
export const SETTINGS_URL = 'https://dashboard.stripe.com/settings/connect/onboarding-options/oauth';
|
|
6
|
-
/** The page's addresses: the docs' own, its test-mode link, and Connect's settings root. */
|
|
7
|
-
export const CONNECT_SETTINGS_PATH = /^\/(test\/)?settings\/connect(\/onboarding-options\/oauth)?\/?$/;
|
|
8
|
-
export const SETTINGS = '_connect_oauth_settings';
|
|
9
|
-
/** The redirect URIs a form's field names, or the first one refused and why. */
|
|
10
|
-
export function redirectUris(field) {
|
|
11
|
-
const uris = [...new Set(field.split(/[\n,]/).map((s) => s.trim()).filter(Boolean))];
|
|
12
|
-
for (const uri of uris) {
|
|
13
|
-
let url;
|
|
14
|
-
try {
|
|
15
|
-
url = new URL(uri);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return { refused: `${uri} is not a valid URL.` };
|
|
19
|
-
}
|
|
20
|
-
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
21
|
-
return { refused: `${uri} must use http or https.` };
|
|
22
|
-
if (uri.includes('#'))
|
|
23
|
-
return { refused: `${uri} must not contain a fragment.` };
|
|
24
|
-
}
|
|
25
|
-
return { uris };
|
|
26
|
-
}
|
|
File without changes
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Coupon and PromotionCode semantics. A coupon is exactly one of percent_off or amount_off (with
|
|
3
|
-
// its currency) over a duration; a promotion code is a customer-facing code for an existing
|
|
4
|
-
// coupon, active until switched off (the machine in ../manifest.ts). Coupon list, retrieve, update
|
|
5
|
-
// and delete and promotion-code retrieve and update (`active`, the machine) are the derived core's.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const CN = "credit_note";
|