@volter/twin-stripe 3.0.0 → 3.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 +51 -21
- package/dist/generated/pack-facts.json +3 -3
- 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 +52 -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 +4 -3
- 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 +53 -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/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,29 +1,75 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { activatedByPayment, at,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if ('answer' in loaded)
|
|
8
|
-
return loaded.answer;
|
|
9
|
-
const id = String(loaded.inv.id);
|
|
10
|
-
await ctx.write(INV, id, { deleted: true }, 'invoice.deleted');
|
|
11
|
-
return ctx.reply({ id, object: 'invoice', deleted: true });
|
|
12
|
-
}
|
|
1
|
+
import { INV, transitions } from "../engine/invoices.js";
|
|
2
|
+
import { asBool, cardError, declineFor, nowUnix } from "../engine/stripe.js";
|
|
3
|
+
import { resourceView, activatedByPayment, at, expanded, finalizeForPayment, finder, invoiceMissing, invoicesLoad, mintCharge, nothingToPayWith, send } from "./shared.js";
|
|
4
|
+
import { draft, prorationLines, sumLines } from "../engine/invoices.js";
|
|
5
|
+
import { addBillingInterval, applyCouponDiscount, priceAmount } from "../engine/stripe.js";
|
|
6
|
+
import { created, fail, newest, pendingPreviewLines, pullPendingItem } from "./shared.js";
|
|
13
7
|
export async function GetInvoicesInvoice(ctx) {
|
|
14
8
|
const id = at(ctx, 'invoice');
|
|
15
9
|
const inv = ctx.get(INV, id);
|
|
16
10
|
return inv ? ctx.reply(expanded(ctx, INV, inv)) : invoiceMissing(ctx, id);
|
|
17
11
|
}
|
|
18
|
-
// the invoice's
|
|
19
|
-
|
|
12
|
+
// paying charges a payment method (the one passed, the invoice's default, or the customer's) unless
|
|
13
|
+
// the payment is recorded out of band; a declining card answers 402 and leaves the invoice open. A
|
|
14
|
+
// paid invoice settles the PaymentIntent finalize opened, and names the charge that took the money.
|
|
15
|
+
export async function PostInvoicesInvoicePay(ctx) {
|
|
20
16
|
const id = at(ctx, 'invoice');
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
17
|
+
const found = ctx.get(INV, id);
|
|
18
|
+
if (!found)
|
|
23
19
|
return invoiceMissing(ctx, id);
|
|
24
|
-
const
|
|
25
|
-
|
|
20
|
+
const refused = ctx.legal(INV, 'status', 'PostInvoicesInvoicePay', found.status, undefined, id);
|
|
21
|
+
if (refused)
|
|
22
|
+
return ctx.refuse(refused);
|
|
23
|
+
// paying a draft finalizes it first, as its finalize does (its number, and the PaymentIntent it collects through)
|
|
24
|
+
// source: https://docs.stripe.com/invoicing/integration "To finalize a draft invoice, use the Dashboard, send it to the customer, or pay it."
|
|
25
|
+
const inv = found.status === 'draft' ? await finalizeForPayment(ctx, found, id) : found;
|
|
26
|
+
const params = ctx.params;
|
|
27
|
+
const paidOutOfBand = params.paid_out_of_band !== undefined && asBool(params.paid_out_of_band);
|
|
28
|
+
const payer = ctx.get('customer', String(inv.customer ?? ''));
|
|
29
|
+
const customerDefault = payer?.invoice_settings?.default_payment_method || payer?.default_source;
|
|
30
|
+
if (!paidOutOfBand && !params.payment_method && !params.source && !inv.default_payment_method && !inv.default_source && !customerDefault)
|
|
31
|
+
return nothingToPayWith(ctx);
|
|
32
|
+
// what it charges: the method passed, else the invoice's default, its subscription's, then its customer's
|
|
33
|
+
// (docs.stripe.com/api/invoices/pay#pay_invoice-payment_method)
|
|
34
|
+
const subscription = typeof inv.subscription === 'string' ? resourceView(ctx, 'subscription', inv.subscription) : undefined;
|
|
35
|
+
const decline = declineFor(finder(ctx), params, { payment_method: inv.default_payment_method ?? subscription?.default_payment_method ?? customerDefault });
|
|
36
|
+
const intent = typeof inv.payment_intent === 'string' ? ctx.get('payment_intent', inv.payment_intent) : undefined;
|
|
37
|
+
if (decline)
|
|
38
|
+
return send(ctx, cardError(decline, intent ? { payment_intent: intent } : {}));
|
|
39
|
+
const amountPaid = Number(inv.total ?? inv.amount_due) || 0;
|
|
40
|
+
let body = await ctx.write(INV, id, {
|
|
41
|
+
// paid outside Stripe, the invoice counts it as such: `amount_paid_off_stripe`, "Amount, in cents (or local
|
|
42
|
+
// equivalent), that was paid on the invoice outside of Stripe" (served spec, invoice), the only place the served
|
|
43
|
+
// version shows it now that basil renders no `paid_out_of_band` (../engine/version.ts)
|
|
44
|
+
status: 'paid', paid: true, paid_out_of_band: paidOutOfBand, amount_paid: amountPaid, amount_remaining: 0,
|
|
45
|
+
...(paidOutOfBand ? { amount_paid_off_stripe: amountPaid } : {}),
|
|
46
|
+
status_transitions: { ...transitions(inv), paid_at: nowUnix(ctx.occurredAt) },
|
|
47
|
+
}, 'invoice.pay');
|
|
48
|
+
if (!paidOutOfBand && typeof inv.payment_intent === 'string' && inv.payment_intent) {
|
|
49
|
+
const pi = ctx.get('payment_intent', inv.payment_intent);
|
|
50
|
+
if (pi && pi.status !== 'succeeded' && !ctx.legal('payment_intent', 'status', 'PostInvoicesInvoicePay', pi.status)) {
|
|
51
|
+
const chargeId = await mintCharge(ctx, inv.payment_intent, { ...pi, invoice: id, ...(typeof params.payment_method === 'string' ? { payment_method: params.payment_method } : {}) }, amountPaid);
|
|
52
|
+
await ctx.write('payment_intent', inv.payment_intent, { status: 'succeeded', amount_received: amountPaid, amount_capturable: 0, next_action: null, latest_charge: chargeId }, 'payment_intent.succeeded');
|
|
53
|
+
body = await ctx.write(INV, id, { charge: chargeId }, 'invoice.update');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// a subscription waiting on this invoice (incomplete on its first, past_due on a failed renewal) is active once it is paid
|
|
57
|
+
const subId = typeof inv.subscription === 'string' ? inv.subscription : undefined;
|
|
58
|
+
const sub = subId ? resourceView(ctx, 'subscription', subId) : undefined;
|
|
59
|
+
if (subId && sub && (sub.status === 'incomplete' || sub.status === 'past_due') && !ctx.legal('subscription', 'status', 'PostInvoicesInvoicePay', sub.status, 'active', subId))
|
|
60
|
+
await activatedByPayment(ctx, subId);
|
|
61
|
+
return ctx.reply(expanded(ctx, INV, ctx.get(INV, id) ?? body));
|
|
26
62
|
}
|
|
63
|
+
// sending a draft finalizes it; any other invoice keeps its status and records the send
|
|
64
|
+
export async function PostInvoicesInvoiceVoid(ctx) {
|
|
65
|
+
const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceVoid');
|
|
66
|
+
if ('answer' in loaded)
|
|
67
|
+
return loaded.answer;
|
|
68
|
+
const inv = loaded.inv;
|
|
69
|
+
const body = await ctx.write(INV, String(inv.id), { status: 'void', status_transitions: { ...transitions(inv), voided_at: nowUnix(ctx.occurredAt) } }, 'invoice.void');
|
|
70
|
+
return ctx.reply(expanded(ctx, INV, body));
|
|
71
|
+
}
|
|
72
|
+
// the invoice's lines, a page at a time
|
|
27
73
|
// a new invoice pulls in the customer's pending items only when asked: pending_invoice_items_behavior "Defaults to
|
|
28
74
|
// `exclude` if the parameter is omitted" (docs.stripe.com/api/invoices/create)
|
|
29
75
|
export async function PostInvoices(ctx) {
|
|
@@ -51,18 +97,16 @@ export async function PostInvoices(ctx) {
|
|
|
51
97
|
/**
|
|
52
98
|
* A preview of the next invoice for a customer or subscription (create_preview), computed and never
|
|
53
99
|
* stored: the subscription's items at their prices, the customer's pending items, the subscription's
|
|
54
|
-
* discount, and
|
|
55
|
-
* charge for the new one (split evenly: there is no clock here to measure the remaining fraction).
|
|
100
|
+
* discount, and an update's unused-time credit and remaining-time charge at the old and new prices.
|
|
56
101
|
*/
|
|
57
102
|
export async function PostInvoicesCreatePreview(ctx) {
|
|
58
103
|
const params = ctx.params;
|
|
59
|
-
const prorationDate = params.subscription_details?.proration_date;
|
|
60
104
|
const now = Number(nowUnix(ctx.occurredAt));
|
|
61
105
|
let subId = typeof params.subscription === 'string' ? params.subscription : '';
|
|
62
106
|
let customer = typeof params.customer === 'string' ? params.customer : '';
|
|
63
107
|
let sub;
|
|
64
108
|
if (subId) {
|
|
65
|
-
sub = ctx
|
|
109
|
+
sub = resourceView(ctx, 'subscription', subId);
|
|
66
110
|
if (!sub)
|
|
67
111
|
return fail(ctx, `No such subscription: '${subId}'`, 404, 'resource_missing');
|
|
68
112
|
customer = customer || String(sub.customer ?? '');
|
|
@@ -80,17 +124,58 @@ export async function PostInvoicesCreatePreview(ctx) {
|
|
|
80
124
|
// the upcoming invoice bills the subscription's next period, its "subscription renewal charges"
|
|
81
125
|
// (docs.stripe.com/api/invoices/create_preview): from the current period's end, one billing interval long
|
|
82
126
|
const renewsAt = sub && Number(sub.current_period_end) > 0 ? Number(sub.current_period_end) : now;
|
|
83
|
-
|
|
127
|
+
let itemRows = sub ? ctx.rows('subscription_item').filter((r) => r.subscription === subId) : [];
|
|
128
|
+
const details = params.subscription_details;
|
|
129
|
+
const previousItems = itemRows;
|
|
130
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "The default value is"
|
|
131
|
+
const behavior = details?.proration_behavior ?? 'create_prorations';
|
|
132
|
+
// source: https://docs.stripe.com/billing/subscriptions/prorations "The prorated amount is calculated as soon as the API updates the subscription."
|
|
133
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "forces the proration to be calculated as though the update was done at the specified time."
|
|
134
|
+
const prorationTime = details?.proration_date === undefined ? now : Number(details.proration_date);
|
|
135
|
+
const periodStart = Number(sub?.current_period_start ?? now);
|
|
136
|
+
const periodEnd = Number(sub?.current_period_end ?? now);
|
|
137
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "The time given must be within the current subscription period"
|
|
138
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "cannot be set to"
|
|
139
|
+
if (details?.proration_date !== undefined && (behavior === 'none' || !sub || !Array.isArray(details.items) || prorationTime < periodStart || prorationTime > periodEnd))
|
|
140
|
+
return fail(ctx, 'Invalid subscription_details[proration_date] for this subscription preview.', 400, 'parameter_invalid_integer');
|
|
141
|
+
// source: spec:/paths/~1v1~1invoices~1create_preview/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/subscription_details/description "The subscription creation or modification params to apply as a preview."
|
|
142
|
+
// Preview changes are local to this calculation: the subscription and its stored items are never changed.
|
|
143
|
+
if (Array.isArray(details?.items)) {
|
|
144
|
+
itemRows = itemRows.map(item => ({ ...item }));
|
|
145
|
+
for (const change of details.items) {
|
|
146
|
+
const index = change.id ? itemRows.findIndex(item => item.id === change.id) : -1;
|
|
147
|
+
if (change.id && index < 0)
|
|
148
|
+
return fail(ctx, `No such subscription_item: '${change.id}'`, 400, 'resource_missing');
|
|
149
|
+
if (asBool(change.deleted)) {
|
|
150
|
+
if (index >= 0)
|
|
151
|
+
itemRows.splice(index, 1);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const previous = index >= 0 ? itemRows[index] : {};
|
|
155
|
+
const priceId = typeof change.price === 'string' ? change.price : typeof previous.price === 'string' ? previous.price : previous.price?.id;
|
|
156
|
+
const price = typeof priceId === 'string' ? ctx.get('price', priceId) : undefined;
|
|
157
|
+
if (!price)
|
|
158
|
+
return fail(ctx, `No such price: '${priceId ?? ''}'`, 400, 'resource_missing');
|
|
159
|
+
const quantity = change.quantity === undefined ? Number(previous.quantity ?? 1) : Number(change.quantity);
|
|
160
|
+
const previewItem = { ...previous, price, quantity };
|
|
161
|
+
if (index >= 0)
|
|
162
|
+
itemRows[index] = previewItem;
|
|
163
|
+
else
|
|
164
|
+
itemRows.push(previewItem);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
84
167
|
const out = [];
|
|
85
168
|
let subtotal = 0;
|
|
86
169
|
let period = now + 30 * 24 * 3600;
|
|
87
170
|
if (sub) {
|
|
88
|
-
for (const
|
|
171
|
+
for (const previewItem of itemRows) {
|
|
172
|
+
const priceId = typeof previewItem.price === 'string' ? previewItem.price : String(previewItem.price?.id ?? '');
|
|
173
|
+
const quantity = Number(previewItem.quantity) || 0;
|
|
89
174
|
const price = ctx.get('price', priceId);
|
|
90
175
|
const amount = priceAmount(price, quantity);
|
|
91
176
|
const recurring = price?.recurring ?? {};
|
|
92
177
|
period = recurring.interval ? addBillingInterval(renewsAt, recurring.interval, Number(recurring.interval_count) || 1) : period;
|
|
93
|
-
const item =
|
|
178
|
+
const item = previewItem;
|
|
94
179
|
subtotal += amount;
|
|
95
180
|
out.push({
|
|
96
181
|
id: `il_twin_${priceId}`, object: 'line_item', type: 'subscription', amount, currency,
|
|
@@ -98,19 +183,32 @@ export async function PostInvoicesCreatePreview(ctx) {
|
|
|
98
183
|
period: { start: renewsAt, end: period }, description: `${quantity} × ${price?.nickname ?? priceId}`,
|
|
99
184
|
});
|
|
100
185
|
}
|
|
101
|
-
if (prorationDate !== undefined && subtotal > 0)
|
|
102
|
-
prorationLines(out, subtotal, currency, now, period);
|
|
103
|
-
}
|
|
104
|
-
for (const ii of newest(ctx, 'invoiceitem', 'date')) {
|
|
105
|
-
if (ii.customer !== customer || ii.invoice)
|
|
106
|
-
continue;
|
|
107
|
-
out.push({ id: `il_${String(ii.id)}`, object: 'line_item', type: 'invoiceitem', amount: Number(ii.amount) || 0, currency, quantity: Number(ii.quantity) || 1, proration: false, invoice_item: ii.id, period: ii.period ?? { start: now, end: now }, description: ii.description ?? null });
|
|
108
186
|
}
|
|
109
|
-
subtotal = sumLines(out);
|
|
110
187
|
const subDiscount = Array.isArray(sub?.discounts) && sub.discounts.length ? sub.discounts[0] : undefined;
|
|
111
188
|
const coupon = subDiscount && typeof subDiscount.coupon === 'object' ? subDiscount.coupon : undefined;
|
|
112
|
-
|
|
113
|
-
|
|
189
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "Disable creating prorations in this request."
|
|
190
|
+
// source: https://docs.stripe.com/billing/subscriptions/prorations "creates a non-proration debit, even when the debit covers a partial billing period."
|
|
191
|
+
if (sub && Array.isArray(details?.items) && behavior !== 'none' && sub.status !== 'trialing') {
|
|
192
|
+
const before = previousItems.map(item => {
|
|
193
|
+
const priceId = typeof item.price === 'string' ? item.price : String(item.price.id);
|
|
194
|
+
const price = ctx.get('price', priceId);
|
|
195
|
+
return { id: `il_twin_${priceId}`, object: 'line_item', type: 'subscription', amount: priceAmount(price, Number(item.quantity ?? 1)), currency,
|
|
196
|
+
price: price ?? priceId, quantity: Number(item.quantity ?? 1), subscription: subId, subscription_item: item.id,
|
|
197
|
+
period: { start: item.current_period_start ?? periodStart, end: item.current_period_end ?? periodEnd } };
|
|
198
|
+
});
|
|
199
|
+
const after = out.map(line => ({ ...line, period: { start: periodStart, end: periodEnd } }));
|
|
200
|
+
const prorations = prorationLines(before, after, prorationTime, periodStart, periodEnd, coupon);
|
|
201
|
+
// source: https://docs.stripe.com/api/invoices/create_preview.md?query=subscription_details "Always invoice immediately for prorations."
|
|
202
|
+
if (behavior === 'always_invoice' && prorations.length)
|
|
203
|
+
out.splice(0, out.length, ...prorations);
|
|
204
|
+
else
|
|
205
|
+
out.unshift(...prorations);
|
|
206
|
+
}
|
|
207
|
+
out.push(...pendingPreviewLines(ctx, customer, currency, now));
|
|
208
|
+
subtotal = sumLines(out);
|
|
209
|
+
// source: https://docs.stripe.com/billing/subscriptions/prorations "no additional discounts are applied to the proration line items themselves"
|
|
210
|
+
const discountAmount = applyCouponDiscount(Math.max(0, sumLines(out.filter(line => !line.proration))), coupon);
|
|
211
|
+
const total = subtotal - discountAmount;
|
|
114
212
|
return ctx.reply({
|
|
115
213
|
...draft(now),
|
|
116
214
|
// "For preview invoices created using the create preview endpoint, this id will be prefixed with `upcoming_in`"
|
|
@@ -119,101 +217,10 @@ export async function PostInvoicesCreatePreview(ctx) {
|
|
|
119
217
|
object: 'invoice', id: `upcoming_${ctx.mint(INV)}`, customer, subscription: subId || null, currency,
|
|
120
218
|
billing_reason: subId ? 'upcoming' : 'manual',
|
|
121
219
|
created: now, period_start: now, period_end: period,
|
|
122
|
-
subtotal, total, amount_due: total, amount_paid: 0, amount_remaining: total,
|
|
220
|
+
subtotal, total, amount_due: Math.max(0, total), amount_paid: 0, amount_remaining: Math.max(0, total),
|
|
123
221
|
starting_balance: 0, ending_balance: null,
|
|
124
222
|
discounts: discountAmount > 0 && subDiscount ? [subDiscount] : [],
|
|
125
223
|
total_discount_amounts: discountAmount > 0 ? [{ amount: discountAmount, discount: subDiscount?.id ?? null }] : [],
|
|
126
224
|
lines: { object: 'list', url: '/v1/invoices/create_preview/lines', has_more: false, total_count: out.length, data: out },
|
|
127
225
|
});
|
|
128
226
|
}
|
|
129
|
-
export async function PostInvoicesInvoiceFinalize(ctx) {
|
|
130
|
-
const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceFinalize');
|
|
131
|
-
if ('answer' in loaded)
|
|
132
|
-
return loaded.answer;
|
|
133
|
-
const inv = loaded.inv;
|
|
134
|
-
const id = String(inv.id);
|
|
135
|
-
const piId = collectsThroughIntent(inv) ? ctx.mint('payment_intent') : null;
|
|
136
|
-
const body = await ctx.write(INV, id, { ...finalizedFields(ctx, inv, Number(nowUnix(ctx.occurredAt))), status: 'open', status_transitions: { ...transitions(inv), finalized_at: nowUnix(ctx.occurredAt) }, payment_intent: piId }, 'invoice.finalize');
|
|
137
|
-
if (piId)
|
|
138
|
-
await openPaymentIntent(ctx, piId, inv, id);
|
|
139
|
-
// a subscription's invoice finalized spends the once coupon it took (renewals.ts spendOnceCoupon)
|
|
140
|
-
await spendOnceCoupon(ctx, inv);
|
|
141
|
-
return ctx.reply(expanded(ctx, INV, body));
|
|
142
|
-
}
|
|
143
|
-
// paying charges a payment method (the one passed, the invoice's default, or the customer's) unless
|
|
144
|
-
// the payment is recorded out of band; a declining card answers 402 and leaves the invoice open. A
|
|
145
|
-
// paid invoice settles the PaymentIntent finalize opened, and names the charge that took the money.
|
|
146
|
-
export async function PostInvoicesInvoicePay(ctx) {
|
|
147
|
-
const id = at(ctx, 'invoice');
|
|
148
|
-
const inv = ctx.get(INV, id);
|
|
149
|
-
if (!inv)
|
|
150
|
-
return invoiceMissing(ctx, id);
|
|
151
|
-
const refused = ctx.legal(INV, 'status', 'PostInvoicesInvoicePay', inv.status, undefined, id);
|
|
152
|
-
if (refused)
|
|
153
|
-
return ctx.refuse(refused);
|
|
154
|
-
const params = ctx.params;
|
|
155
|
-
const paidOutOfBand = params.paid_out_of_band !== undefined && asBool(params.paid_out_of_band);
|
|
156
|
-
const payer = ctx.get('customer', String(inv.customer ?? ''));
|
|
157
|
-
const customerDefault = payer?.invoice_settings?.default_payment_method || payer?.default_source;
|
|
158
|
-
if (!paidOutOfBand && !params.payment_method && !params.source && !inv.default_payment_method && !inv.default_source && !customerDefault)
|
|
159
|
-
return nothingToPayWith(ctx);
|
|
160
|
-
// what it charges: the method passed, else the invoice's default, its subscription's, then its customer's
|
|
161
|
-
// (docs.stripe.com/api/invoices/pay#pay_invoice-payment_method)
|
|
162
|
-
const subscription = typeof inv.subscription === 'string' ? ctx.get('subscription', inv.subscription) : undefined;
|
|
163
|
-
const decline = declineFor(finder(ctx), params, { payment_method: inv.default_payment_method ?? subscription?.default_payment_method ?? customerDefault });
|
|
164
|
-
const intent = typeof inv.payment_intent === 'string' ? ctx.get('payment_intent', inv.payment_intent) : undefined;
|
|
165
|
-
if (decline)
|
|
166
|
-
return send(ctx, cardError(decline, intent ? { payment_intent: intent } : {}));
|
|
167
|
-
const amountPaid = Number(inv.total ?? inv.amount_due) || 0;
|
|
168
|
-
let body = await ctx.write(INV, id, {
|
|
169
|
-
// paid outside Stripe, the invoice counts it as such: `amount_paid_off_stripe`, "Amount, in cents (or local
|
|
170
|
-
// equivalent), that was paid on the invoice outside of Stripe" (served spec, invoice), the only place the served
|
|
171
|
-
// version shows it now that basil renders no `paid_out_of_band` (../engine/version.ts)
|
|
172
|
-
status: 'paid', paid: true, paid_out_of_band: paidOutOfBand, amount_paid: amountPaid, amount_remaining: 0,
|
|
173
|
-
...(paidOutOfBand ? { amount_paid_off_stripe: amountPaid } : {}),
|
|
174
|
-
status_transitions: { ...transitions(inv), paid_at: nowUnix(ctx.occurredAt) },
|
|
175
|
-
}, 'invoice.pay');
|
|
176
|
-
if (!paidOutOfBand && typeof inv.payment_intent === 'string' && inv.payment_intent) {
|
|
177
|
-
const pi = ctx.get('payment_intent', inv.payment_intent);
|
|
178
|
-
if (pi && pi.status !== 'succeeded' && !ctx.legal('payment_intent', 'status', 'PostInvoicesInvoicePay', pi.status)) {
|
|
179
|
-
const chargeId = await mintCharge(ctx, inv.payment_intent, { ...pi, invoice: id, ...(typeof params.payment_method === 'string' ? { payment_method: params.payment_method } : {}) }, amountPaid);
|
|
180
|
-
await ctx.write('payment_intent', inv.payment_intent, { status: 'succeeded', amount_received: amountPaid, amount_capturable: 0, next_action: null, latest_charge: chargeId }, 'payment_intent.succeeded');
|
|
181
|
-
body = await ctx.write(INV, id, { charge: chargeId }, 'invoice.update');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// a subscription waiting on this invoice (incomplete on its first, past_due on a failed renewal) is active once it is paid
|
|
185
|
-
const subId = typeof inv.subscription === 'string' ? inv.subscription : undefined;
|
|
186
|
-
const sub = subId ? ctx.get('subscription', subId) : undefined;
|
|
187
|
-
if (subId && sub && (sub.status === 'incomplete' || sub.status === 'past_due') && !ctx.legal('subscription', 'status', 'PostInvoicesInvoicePay', sub.status, 'active', subId))
|
|
188
|
-
await activatedByPayment(ctx, subId);
|
|
189
|
-
return ctx.reply(expanded(ctx, INV, body));
|
|
190
|
-
}
|
|
191
|
-
// sending a draft finalizes it; any other invoice keeps its status and records the send
|
|
192
|
-
export async function PostInvoicesInvoiceSend(ctx) {
|
|
193
|
-
const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceSend');
|
|
194
|
-
if ('answer' in loaded)
|
|
195
|
-
return loaded.answer;
|
|
196
|
-
const inv = loaded.inv;
|
|
197
|
-
const id = String(inv.id);
|
|
198
|
-
const becameOpen = inv.status === 'draft';
|
|
199
|
-
const piId = collectsThroughIntent(inv) ? ctx.mint('payment_intent') : null;
|
|
200
|
-
const now = nowUnix(ctx.occurredAt);
|
|
201
|
-
const body = await ctx.write(INV, id, {
|
|
202
|
-
status: becameOpen ? 'open' : inv.status, _sent: true, _sent_at: now,
|
|
203
|
-
status_transitions: { ...transitions(inv), ...(becameOpen ? { finalized_at: now } : {}) },
|
|
204
|
-
...(becameOpen ? { payment_intent: piId } : {}),
|
|
205
|
-
}, 'invoice.sent');
|
|
206
|
-
if (piId)
|
|
207
|
-
await openPaymentIntent(ctx, piId, inv, id);
|
|
208
|
-
if (becameOpen)
|
|
209
|
-
await spendOnceCoupon(ctx, inv);
|
|
210
|
-
return ctx.reply(expanded(ctx, INV, body));
|
|
211
|
-
}
|
|
212
|
-
export async function PostInvoicesInvoiceVoid(ctx) {
|
|
213
|
-
const loaded = invoicesLoad(ctx, 'PostInvoicesInvoiceVoid');
|
|
214
|
-
if ('answer' in loaded)
|
|
215
|
-
return loaded.answer;
|
|
216
|
-
const inv = loaded.inv;
|
|
217
|
-
const body = await ctx.write(INV, String(inv.id), { status: 'void', status_transitions: { ...transitions(inv), voided_at: nowUnix(ctx.occurredAt) } }, 'invoice.void');
|
|
218
|
-
return ctx.reply(expanded(ctx, INV, body));
|
|
219
|
-
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
+
export declare function GetIssuingCardsCard(ctx: HandlerContext): Promise<Response>;
|
|
3
|
+
export declare function PostIssuingAuthorizationsAuthorizationApprove(ctx: HandlerContext): Promise<Response>;
|
|
4
|
+
export declare function PostIssuingAuthorizationsAuthorizationDecline(ctx: HandlerContext): Promise<Response>;
|
|
5
|
+
export declare function PostIssuingCards(ctx: HandlerContext): Promise<Response>;
|
|
6
|
+
export declare function PostIssuingCardsCard(ctx: HandlerContext): Promise<Response>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { CARD, CH, cardNumberOf, cvcOf } from "../engine/issuing.js";
|
|
2
|
+
import { emptySpendingControls, normalizeSpendingControls, nowUnix } from "../engine/stripe.js";
|
|
3
|
+
import { at, created, fail, issuingDecide, issuingRefusal, send } from "./shared.js";
|
|
4
|
+
export async function GetIssuingCardsCard(ctx) {
|
|
5
|
+
const id = at(ctx, 'card');
|
|
6
|
+
const card = ctx.get(CARD, id);
|
|
7
|
+
if (!card)
|
|
8
|
+
return fail(ctx, `No such card: '${id}'`, 404, 'resource_missing');
|
|
9
|
+
const expand = Array.isArray(ctx.params.expand) ? ctx.params.expand.map(String) : [];
|
|
10
|
+
// source: spec:/components/schemas/issuing.card/properties/number/description "For security reasons, this is only available for virtual cards, and will be omitted unless you explicitly request it with"
|
|
11
|
+
const virtual = card.type === 'virtual';
|
|
12
|
+
const shown = { ...card };
|
|
13
|
+
if (virtual && expand.includes('number'))
|
|
14
|
+
shown.number = cardNumberOf(await ctx.secret(`issuing-card-number:${id}`));
|
|
15
|
+
// source: spec:/components/schemas/issuing.card/properties/cvc/description "The card's CVC. For security reasons, this is only available for virtual cards"
|
|
16
|
+
if (virtual && expand.includes('cvc'))
|
|
17
|
+
shown.cvc = cvcOf(await ctx.secret(`issuing-card-cvc:${id}`));
|
|
18
|
+
return ctx.reply(ctx.expand(CARD, shown));
|
|
19
|
+
}
|
|
20
|
+
export async function PostIssuingAuthorizationsAuthorizationApprove(ctx) {
|
|
21
|
+
return issuingDecide(true)(ctx);
|
|
22
|
+
}
|
|
23
|
+
export async function PostIssuingAuthorizationsAuthorizationDecline(ctx) {
|
|
24
|
+
return issuingDecide(false)(ctx);
|
|
25
|
+
}
|
|
26
|
+
export async function PostIssuingCards(ctx) {
|
|
27
|
+
const params = ctx.params;
|
|
28
|
+
// source: spec:PostIssuingCards "The currency for the card."
|
|
29
|
+
if (params.currency === undefined || params.currency === '')
|
|
30
|
+
return fail(ctx, 'Missing required param: currency.', 400, 'parameter_missing');
|
|
31
|
+
const cardType = typeof params.type === 'string' ? params.type : '';
|
|
32
|
+
if (!cardType)
|
|
33
|
+
return fail(ctx, 'Missing required param: type.', 400, 'parameter_missing');
|
|
34
|
+
// source: spec:PostIssuingCards "The type of card to issue."
|
|
35
|
+
if (cardType !== 'virtual' && cardType !== 'physical')
|
|
36
|
+
return fail(ctx, "Invalid type: must be one of 'virtual' or 'physical'.", 400, 'parameter_invalid_string_enum');
|
|
37
|
+
// the card answers its cardholder in full (the served spec's issuing.card.cardholder is the Cardholder object)
|
|
38
|
+
const holder = typeof params.cardholder === 'string' ? params.cardholder : '';
|
|
39
|
+
const cardholder = holder ? ctx.get(CH, holder) : undefined;
|
|
40
|
+
if (holder && !cardholder)
|
|
41
|
+
return fail(ctx, `No such cardholder: '${holder}'`, 400, 'resource_missing');
|
|
42
|
+
const controls = normalizeSpendingControls(params.spending_controls, typeof params.currency === 'string' ? params.currency : null);
|
|
43
|
+
if (controls.error)
|
|
44
|
+
return send(ctx, controls.error);
|
|
45
|
+
const id = ctx.mint(CARD);
|
|
46
|
+
const number = cardNumberOf(await ctx.secret(`issuing-card-number:${id}`));
|
|
47
|
+
// Where the documentation stops and the twin decides: a card expires three years after it is issued, at the end of
|
|
48
|
+
// the month it was issued in.
|
|
49
|
+
const issued = new Date(Number(nowUnix(ctx.occurredAt)) * 1000);
|
|
50
|
+
return ctx.reply(ctx.expand(CARD, await created(ctx, CARD, { ...params, id, cardholder: holder || null, spending_controls: controls.controls ?? emptySpendingControls() }, {
|
|
51
|
+
// source: https://docs.stripe.com/api/issuing/cards/create "Whether authorizations can be approved on this card. May be blocked from activating cards depending on past-due Cardholder requirements. Defaults to inactive."
|
|
52
|
+
status: typeof params.status === 'string' ? params.status : 'inactive',
|
|
53
|
+
livemode: false, metadata: {}, brand: 'Visa', last4: number.slice(-4),
|
|
54
|
+
exp_month: issued.getUTCMonth() + 1, exp_year: issued.getUTCFullYear() + 3,
|
|
55
|
+
cancellation_reason: null, replacement_for: null, replacement_reason: null, shipping: null, wallets: null,
|
|
56
|
+
})));
|
|
57
|
+
}
|
|
58
|
+
// source: spec:PostIssuingCardsCard "Any parameters not provided will be left unchanged."
|
|
59
|
+
export async function PostIssuingCardsCard(ctx) {
|
|
60
|
+
const id = at(ctx, 'card');
|
|
61
|
+
const card = ctx.get(CARD, id);
|
|
62
|
+
if (!card)
|
|
63
|
+
return fail(ctx, `No such card: '${id}'`, 404, 'resource_missing');
|
|
64
|
+
const { expand: _expand, ...fields } = ctx.params;
|
|
65
|
+
// source: spec:/paths/~1v1~1issuing~1cards~1{card}/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/status "Dictates whether authorizations can be approved on this card."
|
|
66
|
+
if (fields.status !== undefined && !['active', 'inactive', 'canceled'].includes(String(fields.status)))
|
|
67
|
+
return issuingRefusal(ctx, 'card_status');
|
|
68
|
+
if (fields.status !== undefined) {
|
|
69
|
+
const refused = ctx.legal(CARD, 'status', 'PostIssuingCardsCard', card.status, String(fields.status), id);
|
|
70
|
+
if (refused)
|
|
71
|
+
return ctx.refuse(refused);
|
|
72
|
+
}
|
|
73
|
+
const controls = normalizeSpendingControls(fields.spending_controls, String(card.currency), card.spending_controls);
|
|
74
|
+
if (controls.error)
|
|
75
|
+
return send(ctx, controls.error);
|
|
76
|
+
return ctx.reply(ctx.expand(CARD, await ctx.write(CARD, id, { ...fields, ...(controls.controls ? { spending_controls: controls.controls } : {}) }, 'issuing.card.update')));
|
|
77
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { HandlerContext } from '@volter/world-core';
|
|
2
2
|
export declare function PostPaymentIntents(ctx: HandlerContext): Promise<Response>;
|
|
3
3
|
export declare function PostPaymentIntentsIntentCancel(ctx: HandlerContext): Promise<Response>;
|
|
4
|
-
export declare function PostPaymentIntentsIntentConfirm(ctx: HandlerContext): Promise<Response>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PI, TYPES_GIVEN } from "../engine/payment-intents.js";
|
|
2
2
|
import { asBool, mintClientSecret, nowUnix, validateMoney } from "../engine/stripe.js";
|
|
3
|
-
import {
|
|
3
|
+
import { confirmIntent, intentFields, confirmOnly, methodFromData, paymentIntentsSend } from "./shared.js";
|
|
4
|
+
import { cancelAuthorization, paymentIntentsLoad } from "./shared.js";
|
|
4
5
|
export async function PostPaymentIntents(ctx) {
|
|
5
6
|
const params = ctx.params;
|
|
6
7
|
const bad = validateMoney(params);
|
|
@@ -47,14 +48,15 @@ export async function PostPaymentIntents(ctx) {
|
|
|
47
48
|
payment_method_types,
|
|
48
49
|
...(Array.isArray(params.allowed_payment_method_types) ? { allowed_payment_method_types: payment_method_types } : {}),
|
|
49
50
|
payment_method_options: params.payment_method_options ?? {},
|
|
50
|
-
...rest,
|
|
51
|
+
...intentFields(rest),
|
|
51
52
|
}, 'payment_intent.create');
|
|
52
53
|
if (Array.isArray(given))
|
|
53
54
|
await ctx.record(TYPES_GIVEN, { payment_intent: id }, id);
|
|
54
55
|
if (asBool(confirmNow))
|
|
55
|
-
return confirmIntent(ctx, body, {});
|
|
56
|
+
return confirmIntent(ctx, body, { ...params, payment_method: rest.payment_method });
|
|
56
57
|
return ctx.reply(ctx.expand(PI, body));
|
|
57
58
|
}
|
|
59
|
+
// source: spec:GetPaymentIntentsSearch "Search for PaymentIntents"
|
|
58
60
|
export async function PostPaymentIntentsIntentCancel(ctx) {
|
|
59
61
|
const loaded = paymentIntentsLoad(ctx, 'PostPaymentIntentsIntentCancel');
|
|
60
62
|
if ('answer' in loaded)
|
|
@@ -67,13 +69,3 @@ export async function PostPaymentIntentsIntentCancel(ctx) {
|
|
|
67
69
|
await cancelAuthorization(ctx, held);
|
|
68
70
|
return ctx.reply(ctx.expand(PI, await ctx.write(PI, String(loaded.pi.id), { status: 'canceled', cancellation_reason, amount_capturable: 0 }, 'payment_intent.canceled')));
|
|
69
71
|
}
|
|
70
|
-
export async function PostPaymentIntentsIntentConfirm(ctx) {
|
|
71
|
-
const loaded = paymentIntentsLoad(ctx, 'PostPaymentIntentsIntentConfirm');
|
|
72
|
-
if ('answer' in loaded)
|
|
73
|
-
return loaded.answer;
|
|
74
|
-
const given = ctx.params;
|
|
75
|
-
const { expand: _e, payment_method_data: data, mandate_data: _mandate, ...params } = given;
|
|
76
|
-
// a confirm's payment_method_data makes the method it pays with; neither it nor mandate_data is a field of the intent
|
|
77
|
-
const made = await methodFromData(ctx, data);
|
|
78
|
-
return confirmIntent(ctx, loaded.pi, made ? { ...params, payment_method: made } : params);
|
|
79
|
-
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import type { HandlerContext } from '@volter/world-core';
|
|
2
2
|
export declare function GetPaymentMethodsPaymentMethod(ctx: HandlerContext): Promise<Response>;
|
|
3
|
-
export declare function PostPaymentMethods(ctx: HandlerContext): Promise<Response>;
|
|
4
|
-
export declare function PostPaymentMethodsPaymentMethodAttach(ctx: HandlerContext): Promise<Response>;
|
|
5
3
|
export declare function PostPaymentMethodsPaymentMethodDetach(ctx: HandlerContext): Promise<Response>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { afterSuccessOf, at, attachable, attachPaymentMethod, created, fail, pmMissing } from "./shared.js";
|
|
1
|
+
import { PM } from "../engine/payment-methods.js";
|
|
2
|
+
import { at, pmMissing } from "./shared.js";
|
|
4
3
|
export async function GetPaymentMethodsPaymentMethod(ctx) {
|
|
5
4
|
const m = ctx.get(PM, at(ctx, 'payment_method'));
|
|
6
5
|
return m ? ctx.reply(m) : pmMissing(ctx, at(ctx, 'payment_method'));
|
|
@@ -8,29 +7,6 @@ export async function GetPaymentMethodsPaymentMethod(ctx) {
|
|
|
8
7
|
// the raw card number, cvc and bank account number are never echoed: the modeled sub-object is
|
|
9
8
|
// what the PaymentMethod carries, with the decline outcome its card implies, so a later charge
|
|
10
9
|
// naming it only by id declines the same way
|
|
11
|
-
export async function PostPaymentMethods(ctx) {
|
|
12
|
-
const params = ctx.params;
|
|
13
|
-
const pmType = typeof params.type === 'string' ? params.type : 'card';
|
|
14
|
-
const { card: _card, sepa_debit: _sepa, us_bank_account: _ach, type: _t, ...rest } = params;
|
|
15
|
-
const sub = paymentMethodSubObject(pmType, params);
|
|
16
|
-
const declineOutcome = pmType === 'card' ? (declineFor(() => undefined, params) ?? null) : null;
|
|
17
|
-
// and what Stripe does after its card succeeds (a dispute, a review), which a later charge naming it by id brings
|
|
18
|
-
const afterSuccess = pmType === 'card' ? afterSuccessOf(ctx, params.card) : undefined;
|
|
19
|
-
// the billing details the create gives are the method's (docs.stripe.com/api/payment_methods/create#create_payment_method-billing_details)
|
|
20
|
-
const given = rest.billing_details && typeof rest.billing_details === 'object' ? rest.billing_details : {};
|
|
21
|
-
return ctx.reply(await created(ctx, PM, { ...rest, type: pmType, customer: null, livemode: false, billing_details: { ...noBilling, ...given }, ...sub, _declineOutcome: declineOutcome, ...(afterSuccess ? { _afterSuccess: afterSuccess } : {}) }, {}));
|
|
22
|
-
}
|
|
23
|
-
export async function PostPaymentMethodsPaymentMethodAttach(ctx) {
|
|
24
|
-
const id = at(ctx, 'payment_method');
|
|
25
|
-
const customer = typeof ctx.params.customer === 'string' ? ctx.params.customer : '';
|
|
26
|
-
if (!customer)
|
|
27
|
-
return fail(ctx, 'Missing required param: customer.', 400, 'parameter_missing');
|
|
28
|
-
if (!ctx.get('customer', customer))
|
|
29
|
-
return fail(ctx, `No such customer: '${customer}'`, 400, 'resource_missing');
|
|
30
|
-
if (!attachable(ctx, id))
|
|
31
|
-
return pmMissing(ctx, id);
|
|
32
|
-
return ctx.reply(await attachPaymentMethod(ctx, id, customer));
|
|
33
|
-
}
|
|
34
10
|
export async function PostPaymentMethodsPaymentMethodDetach(ctx) {
|
|
35
11
|
const id = at(ctx, 'payment_method');
|
|
36
12
|
if (!ctx.get(PM, id))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateMoney } from "../engine/stripe.js";
|
|
2
|
-
import { account, created, fail, noExternalAccount, payoutBank, payoutDefaults, refusePayout, send, settlePayout } from "./shared.js";
|
|
2
|
+
import { account, asOf, created, kept, list, newest, fail, noExternalAccount, payoutBank, payoutDefaults, refusePayout, send, settlePayout } from "./shared.js";
|
|
3
3
|
export async function PostPayouts(ctx) {
|
|
4
4
|
const bad = validateMoney(ctx.params);
|
|
5
5
|
if (bad)
|
|
@@ -19,3 +19,10 @@ export async function PostPayouts(ctx) {
|
|
|
19
19
|
const bt = await settlePayout(ctx, id, -amount, currency);
|
|
20
20
|
return ctx.reply(await created(ctx, 'payout', { ...ctx.params, id }, { status: 'pending', ...payoutDefaults(ctx), balance_transaction: bt, destination: acct ? payoutBank(ctx, acct, currency) : null, ...(acct ? { _account: acct } : {}) }));
|
|
21
21
|
}
|
|
22
|
+
// source: https://docs.stripe.com/connect/authentication "Stripe-Account header"
|
|
23
|
+
// source: https://docs.stripe.com/payouts "paid"
|
|
24
|
+
export async function GetPayouts(ctx) {
|
|
25
|
+
const acct = account(ctx);
|
|
26
|
+
const payouts = newest(ctx, 'payout').filter((p) => (kept(ctx, 'payout', p, '_account') ?? null) === (acct ?? null));
|
|
27
|
+
return list(ctx, 'payout', payouts.map((p) => asOf(ctx, p)));
|
|
28
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
-
export declare function DeleteProductsId(ctx: HandlerContext): Promise<Response>;
|
|
3
2
|
export declare function GetProducts(ctx: HandlerContext): Promise<Response>;
|
|
4
3
|
export declare function PostProducts(ctx: HandlerContext): Promise<Response>;
|
|
5
|
-
/** `GET /v1/products/search?query=`: the products Stripe's search language selects
|
|
6
|
-
export declare function GetProductsSearch(ctx: HandlerContext): Promise<Response>;
|
|
4
|
+
/** `GET /v1/products/search?query=`: the products Stripe's search language selects. */
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { asBool, nowUnix } from "../engine/stripe.js";
|
|
2
|
-
import {
|
|
3
|
-
export async function DeleteProductsId(ctx) {
|
|
4
|
-
const id = at(ctx, 'id');
|
|
5
|
-
if (!ctx.get('product', id))
|
|
6
|
-
return productMissing(ctx, id);
|
|
7
|
-
if (ctx.rows('price').some((p) => p.product === id))
|
|
8
|
-
return pricedProductRefused(ctx);
|
|
9
|
-
await ctx.write('product', id, { deleted: true }, 'product.delete');
|
|
10
|
-
return ctx.reply({ id, object: 'product', deleted: true });
|
|
11
|
-
}
|
|
2
|
+
import { created, list, newest, where } from "./shared.js";
|
|
12
3
|
export async function GetProducts(ctx) {
|
|
13
4
|
return list(ctx, 'product', where(ctx, newest(ctx, 'product'), {
|
|
14
5
|
active: (p, v) => asBool(p.active) === asBool(v),
|
|
@@ -18,8 +9,6 @@ export async function GetProducts(ctx) {
|
|
|
18
9
|
export async function PostProducts(ctx) {
|
|
19
10
|
return ctx.reply(await created(ctx, 'product', ctx.params, { active: true, livemode: false, images: [], marketing_features: [], metadata: {}, updated: nowUnix(ctx.occurredAt) }));
|
|
20
11
|
}
|
|
21
|
-
/** `GET /v1/products/search?query=`: the products Stripe's search language selects
|
|
12
|
+
/** `GET /v1/products/search?query=`: the products Stripe's search language selects. */
|
|
22
13
|
// source: spec:GetProductsSearch "Search for products you’ve previously created using Stripe’s"
|
|
23
|
-
|
|
24
|
-
return search(ctx, 'product');
|
|
25
|
-
}
|
|
14
|
+
// source: spec:PostProductsProductFeatures "Creates a product_feature"
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export declare function GetRefundsRefund(ctx: HandlerContext): Promise<Response>;
|
|
1
|
+
import { type HandlerContext } from '@volter/world-core';
|
|
3
2
|
export declare function PostRefunds(ctx: HandlerContext): Promise<Response>;
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export async function GetRefundsRefund(ctx) {
|
|
3
|
-
const r = ctx.get('refund', at(ctx, 'refund'));
|
|
4
|
-
return r ? ctx.reply(r) : refundMissing(ctx, at(ctx, 'refund'));
|
|
5
|
-
}
|
|
1
|
+
import { refundCharge, target } from "./shared.js";
|
|
6
2
|
export async function PostRefunds(ctx) {
|
|
7
3
|
const ch = target(ctx);
|
|
8
4
|
return ch instanceof Response ? ch : refundCharge(ctx, ch, ctx.params);
|