@peable.to/shared-types 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/catalog.js +53 -0
- package/dist/cjs/charge.js +2 -0
- package/dist/cjs/customer.js +2 -0
- package/dist/cjs/event.js +7 -0
- package/dist/cjs/index.js +29 -8
- package/dist/cjs/paymentIntent.js +90 -188
- package/dist/cjs/paymentMethod.js +2 -0
- package/dist/cjs/setupIntent.js +11 -0
- package/dist/cjs/webhookEndpoint.js +20 -0
- package/dist/cjs/webhookSigner.js +35 -12
- package/dist/esm/catalog.js +50 -0
- package/dist/esm/charge.js +1 -0
- package/dist/esm/customer.js +1 -0
- package/dist/esm/event.js +6 -1
- package/dist/esm/index.js +5 -1
- package/dist/esm/paymentIntent.js +89 -186
- package/dist/esm/paymentMethod.js +1 -0
- package/dist/esm/setupIntent.js +8 -0
- package/dist/esm/webhookEndpoint.js +17 -0
- package/dist/esm/webhookSigner.js +35 -12
- package/dist/types/catalog.d.ts +390 -0
- package/dist/types/charge.d.ts +56 -0
- package/dist/types/customer.d.ts +39 -0
- package/dist/types/dispute.d.ts +2 -2
- package/dist/types/event.d.ts +120 -89
- package/dist/types/index.d.ts +8 -2
- package/dist/types/merchant.d.ts +0 -1
- package/dist/types/paymentIntent.d.ts +215 -84
- package/dist/types/paymentMethod.d.ts +33 -0
- package/dist/types/settlement.d.ts +0 -9
- package/dist/types/setupIntent.d.ts +39 -0
- package/dist/types/webhookDelivery.d.ts +15 -32
- package/dist/types/webhookEndpoint.d.ts +81 -0
- package/dist/types/webhookSigner.d.ts +13 -5
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CATALOG_LIMITS = exports.COUPON_DURATIONS = exports.TRANSFORM_QUANTITY_ROUNDS = exports.PRICE_TAX_BEHAVIORS = exports.RECURRING_USAGE_TYPES = exports.RECURRING_INTERVALS = exports.PRICE_TYPES = exports.PRICE_TIERS_MODES = exports.PRICE_BILLING_SCHEMES = void 0;
|
|
4
|
+
/** How a price's amount is computed from a quantity. */
|
|
5
|
+
exports.PRICE_BILLING_SCHEMES = ['per_unit', 'tiered'];
|
|
6
|
+
/**
|
|
7
|
+
* How a tiered price reads its tiers.
|
|
8
|
+
*
|
|
9
|
+
* - `volume` — the WHOLE quantity is priced at the tier it falls into.
|
|
10
|
+
* - `graduated` — each unit is priced at the tier it falls into, so a
|
|
11
|
+
* quantity crossing two tiers pays each tier's rate for its
|
|
12
|
+
* share, plus the flat amount of every tier it reaches.
|
|
13
|
+
*/
|
|
14
|
+
exports.PRICE_TIERS_MODES = ['graduated', 'volume'];
|
|
15
|
+
/** `recurring` when the price has a recurring component, `one_time` otherwise. */
|
|
16
|
+
exports.PRICE_TYPES = ['one_time', 'recurring'];
|
|
17
|
+
exports.RECURRING_INTERVALS = ['day', 'week', 'month', 'year'];
|
|
18
|
+
/**
|
|
19
|
+
* `licensed` bills the quantity on the subscription. `metered` bills reported
|
|
20
|
+
* usage and needs a meter; meters are not part of Orbit yet, so the gateway
|
|
21
|
+
* refuses a metered price rather than store one nothing can bill.
|
|
22
|
+
*/
|
|
23
|
+
exports.RECURRING_USAGE_TYPES = ['licensed', 'metered'];
|
|
24
|
+
/**
|
|
25
|
+
* Whether a price includes tax (Peable Tax, ADR 0011). `unspecified` defers to
|
|
26
|
+
* the merchant's default tax behavior; once a price says `inclusive` or
|
|
27
|
+
* `exclusive` it can never change.
|
|
28
|
+
*/
|
|
29
|
+
exports.PRICE_TAX_BEHAVIORS = ['inclusive', 'exclusive', 'unspecified'];
|
|
30
|
+
exports.TRANSFORM_QUANTITY_ROUNDS = ['up', 'down'];
|
|
31
|
+
/** How long a coupon's discount lasts once applied to a subscription. */
|
|
32
|
+
exports.COUPON_DURATIONS = ['once', 'repeating', 'forever'];
|
|
33
|
+
/** The bounds the gateway enforces, published so a form can say them first. */
|
|
34
|
+
exports.CATALOG_LIMITS = Object.freeze({
|
|
35
|
+
productNameMax: 250,
|
|
36
|
+
productDescriptionMax: 40000,
|
|
37
|
+
productImagesMax: 8,
|
|
38
|
+
urlMax: 2048,
|
|
39
|
+
unitLabelMax: 12,
|
|
40
|
+
statementDescriptorMax: 22,
|
|
41
|
+
marketingFeaturesMax: 15,
|
|
42
|
+
marketingFeatureNameMax: 80,
|
|
43
|
+
priceNicknameMax: 250,
|
|
44
|
+
lookupKeyMax: 200,
|
|
45
|
+
lookupKeysFilterMax: 10,
|
|
46
|
+
priceTiersMax: 250,
|
|
47
|
+
couponIdMax: 64,
|
|
48
|
+
couponNameMax: 40,
|
|
49
|
+
couponAppliesToProductsMax: 100,
|
|
50
|
+
couponDurationInMonthsMax: 1200,
|
|
51
|
+
promotionCodeMax: 64,
|
|
52
|
+
idsFilterMax: 100,
|
|
53
|
+
});
|
package/dist/cjs/event.js
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PEABLE_API_VERSION = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The wire date every response names in `Peable-Version`, and the
|
|
6
|
+
* `api_version` an event is stored under (ADR 0014 D2/D4). One constant, so
|
|
7
|
+
* the header and the stored events cannot name two different wires.
|
|
8
|
+
*/
|
|
9
|
+
exports.PEABLE_API_VERSION = '2026-11-03';
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TAX_LOCATION_EVIDENCE_TYPES = exports.TAX_REGISTRATION_SCHEMES = exports.PEABLE_BILLING_AUTHORITY_KEYS = exports.canonicalBillingTaxQuote = exports.canonicalBillingAuthority = exports.BILLING_SUBSCRIPTION_STATUSES = exports.SOCIAL_SOURCE_REF_MAX_LENGTH = exports.SOCIAL_SOURCE_APP_MAX_LENGTH = exports.MERCHANT_ENVIRONMENTS = exports.verifyWebhook = exports.signWebhook = exports.canExpire = exports.
|
|
3
|
+
exports.TAX_LOCATION_EVIDENCE_TYPES = exports.TAX_REGISTRATION_SCHEMES = exports.PEABLE_BILLING_AUTHORITY_KEYS = exports.canonicalBillingTaxQuote = exports.canonicalBillingAuthority = exports.BILLING_SUBSCRIPTION_STATUSES = exports.SOCIAL_SOURCE_REF_MAX_LENGTH = exports.SOCIAL_SOURCE_APP_MAX_LENGTH = exports.MERCHANT_ENVIRONMENTS = exports.verifyWebhook = exports.signWebhook = exports.CATALOG_LIMITS = exports.COUPON_DURATIONS = exports.TRANSFORM_QUANTITY_ROUNDS = exports.PRICE_TAX_BEHAVIORS = exports.RECURRING_USAGE_TYPES = exports.RECURRING_INTERVALS = exports.PRICE_TYPES = exports.PRICE_TIERS_MODES = exports.PRICE_BILLING_SCHEMES = exports.WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS = exports.WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT = exports.WEBHOOK_ENDPOINT_DISABLED_REASONS = exports.WEBHOOK_ENDPOINT_STATUSES = exports.PEABLE_API_VERSION = exports.SETUP_INTENT_STATUSES = exports.isValidStatusTransition = exports.canStillBePaid = exports.canExpire = exports.TERMINAL_STATUSES = exports.PAYMENT_INTENT_STATUSES = exports.PAYMENT_INTENT_SETUP_FUTURE_USAGES = exports.PAYMENT_INTENT_RAILS = exports.PAYMENT_INTENT_CAPTURE_METHODS = exports.PAYMENT_INTENT_CANCELLATION_REASONS = exports.MERCHANT_CANCELLATION_REASONS = exports.EXPIRABLE_STATUSES = exports.CARD_ONLY_STATUSES = exports.isCurrencyCode = exports.decimalsFor = exports.CURRENCY_DECIMALS = exports.CURRENCY_CODES = exports.isBaseUnitString = exports.UNITS_PER_COIN = void 0;
|
|
4
4
|
// @peable.to/shared-types — public API for the Peable Gateway contract.
|
|
5
5
|
var money_1 = require("./money");
|
|
6
6
|
Object.defineProperty(exports, "UNITS_PER_COIN", { enumerable: true, get: function () { return money_1.UNITS_PER_COIN; } });
|
|
@@ -10,16 +10,37 @@ Object.defineProperty(exports, "CURRENCY_DECIMALS", { enumerable: true, get: fun
|
|
|
10
10
|
Object.defineProperty(exports, "decimalsFor", { enumerable: true, get: function () { return money_1.decimalsFor; } });
|
|
11
11
|
Object.defineProperty(exports, "isCurrencyCode", { enumerable: true, get: function () { return money_1.isCurrencyCode; } });
|
|
12
12
|
var paymentIntent_1 = require("./paymentIntent");
|
|
13
|
-
Object.defineProperty(exports, "isValidStatusTransition", { enumerable: true, get: function () { return paymentIntent_1.isValidStatusTransition; } });
|
|
14
|
-
Object.defineProperty(exports, "canStillBePaid", { enumerable: true, get: function () { return paymentIntent_1.canStillBePaid; } });
|
|
15
|
-
Object.defineProperty(exports, "reachesSettlementFromPayer", { enumerable: true, get: function () { return paymentIntent_1.reachesSettlementFromPayer; } });
|
|
16
|
-
Object.defineProperty(exports, "POST_SETTLEMENT_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.POST_SETTLEMENT_STATUSES; } });
|
|
17
|
-
Object.defineProperty(exports, "PAYMENT_INTENT_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_STATUSES; } });
|
|
18
|
-
Object.defineProperty(exports, "PAYMENT_INTENT_RAILS", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_RAILS; } });
|
|
19
|
-
Object.defineProperty(exports, "CHAIN_ONLY_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.CHAIN_ONLY_STATUSES; } });
|
|
20
13
|
Object.defineProperty(exports, "CARD_ONLY_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.CARD_ONLY_STATUSES; } });
|
|
21
14
|
Object.defineProperty(exports, "EXPIRABLE_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.EXPIRABLE_STATUSES; } });
|
|
15
|
+
Object.defineProperty(exports, "MERCHANT_CANCELLATION_REASONS", { enumerable: true, get: function () { return paymentIntent_1.MERCHANT_CANCELLATION_REASONS; } });
|
|
16
|
+
Object.defineProperty(exports, "PAYMENT_INTENT_CANCELLATION_REASONS", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_CANCELLATION_REASONS; } });
|
|
17
|
+
Object.defineProperty(exports, "PAYMENT_INTENT_CAPTURE_METHODS", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_CAPTURE_METHODS; } });
|
|
18
|
+
Object.defineProperty(exports, "PAYMENT_INTENT_RAILS", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_RAILS; } });
|
|
19
|
+
Object.defineProperty(exports, "PAYMENT_INTENT_SETUP_FUTURE_USAGES", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_SETUP_FUTURE_USAGES; } });
|
|
20
|
+
Object.defineProperty(exports, "PAYMENT_INTENT_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.PAYMENT_INTENT_STATUSES; } });
|
|
21
|
+
Object.defineProperty(exports, "TERMINAL_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.TERMINAL_STATUSES; } });
|
|
22
22
|
Object.defineProperty(exports, "canExpire", { enumerable: true, get: function () { return paymentIntent_1.canExpire; } });
|
|
23
|
+
Object.defineProperty(exports, "canStillBePaid", { enumerable: true, get: function () { return paymentIntent_1.canStillBePaid; } });
|
|
24
|
+
Object.defineProperty(exports, "isValidStatusTransition", { enumerable: true, get: function () { return paymentIntent_1.isValidStatusTransition; } });
|
|
25
|
+
var setupIntent_1 = require("./setupIntent");
|
|
26
|
+
Object.defineProperty(exports, "SETUP_INTENT_STATUSES", { enumerable: true, get: function () { return setupIntent_1.SETUP_INTENT_STATUSES; } });
|
|
27
|
+
var event_1 = require("./event");
|
|
28
|
+
Object.defineProperty(exports, "PEABLE_API_VERSION", { enumerable: true, get: function () { return event_1.PEABLE_API_VERSION; } });
|
|
29
|
+
var webhookEndpoint_1 = require("./webhookEndpoint");
|
|
30
|
+
Object.defineProperty(exports, "WEBHOOK_ENDPOINT_STATUSES", { enumerable: true, get: function () { return webhookEndpoint_1.WEBHOOK_ENDPOINT_STATUSES; } });
|
|
31
|
+
Object.defineProperty(exports, "WEBHOOK_ENDPOINT_DISABLED_REASONS", { enumerable: true, get: function () { return webhookEndpoint_1.WEBHOOK_ENDPOINT_DISABLED_REASONS; } });
|
|
32
|
+
Object.defineProperty(exports, "WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT", { enumerable: true, get: function () { return webhookEndpoint_1.WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT; } });
|
|
33
|
+
Object.defineProperty(exports, "WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS", { enumerable: true, get: function () { return webhookEndpoint_1.WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS; } });
|
|
34
|
+
var catalog_1 = require("./catalog");
|
|
35
|
+
Object.defineProperty(exports, "PRICE_BILLING_SCHEMES", { enumerable: true, get: function () { return catalog_1.PRICE_BILLING_SCHEMES; } });
|
|
36
|
+
Object.defineProperty(exports, "PRICE_TIERS_MODES", { enumerable: true, get: function () { return catalog_1.PRICE_TIERS_MODES; } });
|
|
37
|
+
Object.defineProperty(exports, "PRICE_TYPES", { enumerable: true, get: function () { return catalog_1.PRICE_TYPES; } });
|
|
38
|
+
Object.defineProperty(exports, "RECURRING_INTERVALS", { enumerable: true, get: function () { return catalog_1.RECURRING_INTERVALS; } });
|
|
39
|
+
Object.defineProperty(exports, "RECURRING_USAGE_TYPES", { enumerable: true, get: function () { return catalog_1.RECURRING_USAGE_TYPES; } });
|
|
40
|
+
Object.defineProperty(exports, "PRICE_TAX_BEHAVIORS", { enumerable: true, get: function () { return catalog_1.PRICE_TAX_BEHAVIORS; } });
|
|
41
|
+
Object.defineProperty(exports, "TRANSFORM_QUANTITY_ROUNDS", { enumerable: true, get: function () { return catalog_1.TRANSFORM_QUANTITY_ROUNDS; } });
|
|
42
|
+
Object.defineProperty(exports, "COUPON_DURATIONS", { enumerable: true, get: function () { return catalog_1.COUPON_DURATIONS; } });
|
|
43
|
+
Object.defineProperty(exports, "CATALOG_LIMITS", { enumerable: true, get: function () { return catalog_1.CATALOG_LIMITS; } });
|
|
23
44
|
var webhookSigner_1 = require("./webhookSigner");
|
|
24
45
|
Object.defineProperty(exports, "signWebhook", { enumerable: true, get: function () { return webhookSigner_1.signWebhook; } });
|
|
25
46
|
Object.defineProperty(exports, "verifyWebhook", { enumerable: true, get: function () { return webhookSigner_1.verifyWebhook; } });
|
|
@@ -1,231 +1,133 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.PAYMENT_INTENT_SETUP_FUTURE_USAGES = exports.PAYMENT_INTENT_CAPTURE_METHODS = exports.MERCHANT_CANCELLATION_REASONS = exports.PAYMENT_INTENT_CANCELLATION_REASONS = exports.EXPIRABLE_STATUSES = exports.TERMINAL_STATUSES = exports.PAYMENT_INTENT_STATUSES = exports.CARD_ONLY_STATUSES = exports.PAYMENT_INTENT_RAILS = void 0;
|
|
4
4
|
exports.isValidStatusTransition = isValidStatusTransition;
|
|
5
5
|
exports.canStillBePaid = canStillBePaid;
|
|
6
|
-
exports.reachesSettlementFromPayer = reachesSettlementFromPayer;
|
|
7
6
|
exports.canExpire = canExpire;
|
|
8
7
|
/** {@link PaymentIntentRail} as the tuple the database CHECK reads. */
|
|
9
8
|
exports.PAYMENT_INTENT_RAILS = ['faircoin', 'card'];
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
10
|
+
* The statuses only a CARD payment can reach (ADR 0019).
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* be demanding a txid from a payment that can never have one.
|
|
19
|
-
*
|
|
20
|
-
* `db/schema/payments.ts` renders these two sets into CHECK constraints, so the
|
|
21
|
-
* restriction holds against a write that skipped the application.
|
|
12
|
+
* A FairCoin payment has no payment method to require (the payer's wallet signs
|
|
13
|
+
* on their own device), nothing to confirm server-side, and no authorization
|
|
14
|
+
* to capture — coins either arrive or they do not. `db/schema/payments.ts`
|
|
15
|
+
* renders this set into a CHECK, so the restriction holds against a write that
|
|
16
|
+
* skipped the application.
|
|
22
17
|
*/
|
|
23
|
-
exports.CHAIN_ONLY_STATUSES = [
|
|
24
|
-
'awaiting_approval',
|
|
25
|
-
'approved',
|
|
26
|
-
'broadcast',
|
|
27
|
-
'confirming',
|
|
28
|
-
];
|
|
29
18
|
exports.CARD_ONLY_STATUSES = [
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'partially_refunded',
|
|
19
|
+
'requires_payment_method',
|
|
20
|
+
'requires_confirmation',
|
|
21
|
+
'requires_capture',
|
|
34
22
|
];
|
|
35
23
|
/**
|
|
36
|
-
* Legal
|
|
37
|
-
* (refunded/expired/failed/rejected) allow no further transition here — the one
|
|
38
|
-
* documented exception, a reorg dropping `settled` back to `confirming`, is
|
|
39
|
-
* modelled by the backend state machine, not this happy-path table.
|
|
24
|
+
* Legal transitions of the PaymentIntent lifecycle.
|
|
40
25
|
*
|
|
41
|
-
* ONE table for both rails
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
26
|
+
* ONE table for both rails. It answers "is this a legal lifecycle edge at all";
|
|
27
|
+
* WHICH rail may reach a given status is the separate, structural question
|
|
28
|
+
* {@link CARD_ONLY_STATUSES} answers in the database, and which EVENT may walk
|
|
29
|
+
* an edge is `LEGAL_SOURCES` in the backend's `services/intentState.ts`.
|
|
45
30
|
*
|
|
46
31
|
* Self-transitions are absent because `applyEvent` short-circuits on
|
|
47
|
-
* `current === target`
|
|
48
|
-
*
|
|
49
|
-
*
|
|
32
|
+
* `current === target` — the idempotent re-poll of the settlement watcher.
|
|
33
|
+
*
|
|
34
|
+
* `succeeded` has no outgoing edge here. Its one exception — a chain reorg
|
|
35
|
+
* dropping a FairCoin payment back below its confirmation threshold,
|
|
36
|
+
* `succeeded → processing` — is modelled by the backend state machine, not this
|
|
37
|
+
* table. Refunds are not statuses: money coming back is `amount_refunded` and
|
|
38
|
+
* the refunds themselves (ADR 0019).
|
|
50
39
|
*/
|
|
51
40
|
const ALLOWED = {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
'broadcast',
|
|
41
|
+
requires_payment_method: [
|
|
42
|
+
'requires_confirmation',
|
|
43
|
+
'requires_action',
|
|
44
|
+
'processing',
|
|
45
|
+
'requires_capture',
|
|
46
|
+
'succeeded',
|
|
47
|
+
'canceled',
|
|
48
|
+
],
|
|
49
|
+
requires_confirmation: [
|
|
50
|
+
'requires_payment_method',
|
|
63
51
|
'requires_action',
|
|
64
52
|
'processing',
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'expired',
|
|
53
|
+
'requires_capture',
|
|
54
|
+
'succeeded',
|
|
55
|
+
'canceled',
|
|
69
56
|
],
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* had been declined.
|
|
86
|
-
*
|
|
87
|
-
* On the chain rail `failed` means `underpaid`, which IS terminal — coins
|
|
88
|
-
* arrived and were not enough. Nothing here opens that door: `underpaid` is
|
|
89
|
-
* emitted only by the settlement watcher, and `LEGAL_SOURCES` in
|
|
90
|
-
* `services/intentState.ts` keeps every `card_*` event to card statuses and
|
|
91
|
-
* the chain's `confirmed` to `confirming`. This table answers "is the edge
|
|
92
|
-
* legal at all"; which rail may walk it is the separate question those two
|
|
93
|
-
* guards answer.
|
|
94
|
-
*/
|
|
95
|
-
failed: ['requires_action', 'processing', 'settled'],
|
|
96
|
-
// Money coming back requires money to have arrived, so both refund states are
|
|
97
|
-
// reachable only from `settled`.
|
|
98
|
-
settled: ['refunded', 'partially_refunded'],
|
|
99
|
-
/**
|
|
100
|
-
* ...and money coming back can be UNDONE, which is why these edges run both
|
|
101
|
-
* ways.
|
|
102
|
-
*
|
|
103
|
-
* A refund is not final when the provider accepts it: a bank can reject one
|
|
104
|
-
* days later, and the provider then says so (`refund.failed`). Without a path
|
|
105
|
-
* back, a payment whose only refund failed would claim `refunded` forever —
|
|
106
|
-
* the merchant's books saying money went back that is still with them, and no
|
|
107
|
-
* transition anywhere able to correct it.
|
|
108
|
-
*
|
|
109
|
-
* The target is always recomputed from the SUM of succeeded refunds
|
|
110
|
-
* (`applyRefundToIntent`), never stepped, so these edges describe where that
|
|
111
|
-
* sum can legally land rather than a sequence anyone walks.
|
|
112
|
-
*/
|
|
113
|
-
partially_refunded: ['refunded', 'settled'],
|
|
114
|
-
refunded: ['partially_refunded', 'settled'],
|
|
115
|
-
expired: [],
|
|
116
|
-
rejected: [],
|
|
57
|
+
// FairCoin: the payer broadcasts (→ processing), or the intent is cancelled
|
|
58
|
+
// or expires (→ canceled). Card: an SCA challenge resolved either way.
|
|
59
|
+
requires_action: [
|
|
60
|
+
'requires_payment_method',
|
|
61
|
+
'processing',
|
|
62
|
+
'requires_capture',
|
|
63
|
+
'succeeded',
|
|
64
|
+
'canceled',
|
|
65
|
+
],
|
|
66
|
+
// FairCoin: confirmed (→ succeeded) or underpaid (→ canceled). Card: an async
|
|
67
|
+
// method settled, was authorized, or failed back to `requires_payment_method`.
|
|
68
|
+
processing: ['requires_payment_method', 'requires_capture', 'succeeded', 'canceled'],
|
|
69
|
+
requires_capture: ['processing', 'succeeded', 'canceled'],
|
|
70
|
+
succeeded: [],
|
|
71
|
+
canceled: [],
|
|
117
72
|
};
|
|
118
73
|
function isValidStatusTransition(from, to) {
|
|
119
74
|
return ALLOWED[from].includes(to);
|
|
120
75
|
}
|
|
121
|
-
/** Every
|
|
122
|
-
* single source of truth consumed by the Gateway's list-route status filter. */
|
|
76
|
+
/** Every `PaymentIntentStatus`, derived from `ALLOWED`'s keys. */
|
|
123
77
|
exports.PAYMENT_INTENT_STATUSES = Object.keys(ALLOWED);
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
*
|
|
127
|
-
* **Twice now this question has been answered by walking the transition table,
|
|
128
|
-
* and twice the table has moved underneath it.**
|
|
129
|
-
*
|
|
130
|
-
* The hosted checkout first asked leaf-ness — "has this status no outgoing
|
|
131
|
-
* edges" — which meant `settled` while `settled` had none. Adding the refund
|
|
132
|
-
* transitions (`settled → refunded | partially_refunded`) gave it edges, so a
|
|
133
|
-
* settled payment stopped looking finished, and the checkout began REUSING a
|
|
134
|
-
* remembered settled intent instead of minting a fresh one: a payer who had
|
|
135
|
-
* already paid saw their old receipt forever, with no way to pay the link
|
|
136
|
-
* again.
|
|
137
|
-
*
|
|
138
|
-
* The fix was reachability — "is `settled` still reachable" — and that held
|
|
139
|
-
* until refunds could be UNDONE. `refunded → settled` (a bank rejecting a
|
|
140
|
-
* refund days later) makes `settled` reachable from `settled` in two steps, so
|
|
141
|
-
* reachability answers `true` for a payment that is finished, and the original
|
|
142
|
-
* bug returns unchanged.
|
|
143
|
-
*
|
|
144
|
-
* So the set is stated, not derived. It is exactly the statuses in which the
|
|
145
|
-
* gateway is still waiting on the PAYER — which is what the question means, in
|
|
146
|
-
* the payer's own terms, and is the one formulation no edge added elsewhere can
|
|
147
|
-
* silently change. `paymentIntent.test.ts` pins it against the table: every
|
|
148
|
-
* member must still be able to reach `settled` without passing through a
|
|
149
|
-
* post-settlement status, and no non-member may.
|
|
150
|
-
*/
|
|
151
|
-
const PAYABLE_STATUSES = new Set([
|
|
152
|
-
'created',
|
|
153
|
-
'awaiting_approval',
|
|
154
|
-
'approved',
|
|
155
|
-
'broadcast',
|
|
156
|
-
'confirming',
|
|
157
|
-
'requires_action',
|
|
158
|
-
'processing',
|
|
159
|
-
]);
|
|
78
|
+
/** The statuses that end a payment: nothing leaves them (bar the reorg exception). */
|
|
79
|
+
exports.TERMINAL_STATUSES = ['succeeded', 'canceled'];
|
|
160
80
|
/**
|
|
161
81
|
* Whether a payer can still complete a payment against an intent in this
|
|
162
|
-
* status.
|
|
163
|
-
*
|
|
164
|
-
* `settled` answers `false`: it is where paying ENDS. `partially_refunded`
|
|
165
|
-
* answers `false` too — money can still move, but not from the payer.
|
|
82
|
+
* status — every non-terminal one. Stated, not derived from reachability:
|
|
83
|
+
* `paymentIntent.test.ts` pins it against the table.
|
|
166
84
|
*/
|
|
167
85
|
function canStillBePaid(status) {
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* The statuses a payment reaches only AFTER the payer has finished.
|
|
172
|
-
*
|
|
173
|
-
* Exported for the gate below rather than for production use: the property that
|
|
174
|
-
* makes `PAYABLE_STATUSES` checkable is that a payable status reaches `settled`
|
|
175
|
-
* without going through one of these.
|
|
176
|
-
*/
|
|
177
|
-
exports.POST_SETTLEMENT_STATUSES = [
|
|
178
|
-
'settled',
|
|
179
|
-
'refunded',
|
|
180
|
-
'partially_refunded',
|
|
181
|
-
];
|
|
182
|
-
/**
|
|
183
|
-
* Whether `settled` is reachable from `status` without passing through a
|
|
184
|
-
* post-settlement status. The derivation `canStillBePaid` used to BE, kept as
|
|
185
|
-
* the thing that checks it rather than as the thing that answers it.
|
|
186
|
-
*/
|
|
187
|
-
function reachesSettlementFromPayer(status) {
|
|
188
|
-
const blocked = new Set(exports.POST_SETTLEMENT_STATUSES);
|
|
189
|
-
// A payment that has ALREADY settled is one the payer has finished with,
|
|
190
|
-
// whatever edges lead out of it. Checked before the walk rather than inside
|
|
191
|
-
// it, because `partially_refunded → settled` is a direct edge and the walk
|
|
192
|
-
// would find it in one step — and that edge is a refund being undone, not a
|
|
193
|
-
// payer paying.
|
|
194
|
-
if (blocked.has(status))
|
|
195
|
-
return false;
|
|
196
|
-
const seen = new Set();
|
|
197
|
-
const queue = [...ALLOWED[status]];
|
|
198
|
-
while (queue.length > 0) {
|
|
199
|
-
const next = queue.shift();
|
|
200
|
-
if (next === 'settled')
|
|
201
|
-
return true;
|
|
202
|
-
if (seen.has(next) || blocked.has(next))
|
|
203
|
-
continue;
|
|
204
|
-
seen.add(next);
|
|
205
|
-
queue.push(...ALLOWED[next]);
|
|
206
|
-
}
|
|
207
|
-
return false;
|
|
86
|
+
return !exports.TERMINAL_STATUSES.includes(status);
|
|
208
87
|
}
|
|
209
88
|
/**
|
|
210
89
|
* The statuses an intent may still EXPIRE from (ADR 0012 D9).
|
|
211
90
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* intent NEVER expires, whatever `
|
|
216
|
-
* settlement watcher owns the row.
|
|
217
|
-
*
|
|
218
|
-
*
|
|
91
|
+
* The gateway is waiting on a PERSON who has not paid: a FairCoin payer who has
|
|
92
|
+
* not broadcast (`requires_action`), or a card payer who has not completed a
|
|
93
|
+
* payment method or a challenge. Once a FairCoin payer has reported a
|
|
94
|
+
* transaction (`processing`) the intent NEVER expires, whatever `expires_at`
|
|
95
|
+
* says — coins are moving and the settlement watcher owns the row. An
|
|
96
|
+
* authorization held for capture (`requires_capture`) is the merchant's to
|
|
97
|
+
* capture or cancel and does not expire either.
|
|
219
98
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
99
|
+
* Expiry is a cancellation with `cancellation_reason: 'abandoned'`: a platform
|
|
100
|
+
* releases a reservation on `payment_intent.canceled`.
|
|
222
101
|
*/
|
|
223
102
|
exports.EXPIRABLE_STATUSES = [
|
|
224
|
-
'
|
|
225
|
-
'
|
|
103
|
+
'requires_payment_method',
|
|
104
|
+
'requires_confirmation',
|
|
226
105
|
'requires_action',
|
|
227
106
|
];
|
|
228
107
|
/** Whether an intent in this status can still expire. See {@link EXPIRABLE_STATUSES}. */
|
|
229
108
|
function canExpire(status) {
|
|
230
109
|
return exports.EXPIRABLE_STATUSES.includes(status);
|
|
231
110
|
}
|
|
111
|
+
exports.PAYMENT_INTENT_CANCELLATION_REASONS = [
|
|
112
|
+
'abandoned',
|
|
113
|
+
'automatic',
|
|
114
|
+
'duplicate',
|
|
115
|
+
'fraudulent',
|
|
116
|
+
'requested_by_customer',
|
|
117
|
+
'underpaid',
|
|
118
|
+
];
|
|
119
|
+
/** The reasons a MERCHANT may give `POST /v1/payment_intents/:id/cancel`. */
|
|
120
|
+
exports.MERCHANT_CANCELLATION_REASONS = [
|
|
121
|
+
'abandoned',
|
|
122
|
+
'duplicate',
|
|
123
|
+
'fraudulent',
|
|
124
|
+
'requested_by_customer',
|
|
125
|
+
];
|
|
126
|
+
exports.PAYMENT_INTENT_CAPTURE_METHODS = [
|
|
127
|
+
'automatic',
|
|
128
|
+
'manual',
|
|
129
|
+
];
|
|
130
|
+
exports.PAYMENT_INTENT_SETUP_FUTURE_USAGES = [
|
|
131
|
+
'on_session',
|
|
132
|
+
'off_session',
|
|
133
|
+
];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SETUP_INTENT_STATUSES = void 0;
|
|
4
|
+
exports.SETUP_INTENT_STATUSES = [
|
|
5
|
+
'requires_payment_method',
|
|
6
|
+
'requires_confirmation',
|
|
7
|
+
'requires_action',
|
|
8
|
+
'processing',
|
|
9
|
+
'canceled',
|
|
10
|
+
'succeeded',
|
|
11
|
+
];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS = exports.WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT = exports.WEBHOOK_ENDPOINT_DISABLED_REASONS = exports.WEBHOOK_ENDPOINT_STATUSES = void 0;
|
|
4
|
+
exports.WEBHOOK_ENDPOINT_STATUSES = ['enabled', 'disabled'];
|
|
5
|
+
/**
|
|
6
|
+
* Why an endpoint is `disabled`.
|
|
7
|
+
*
|
|
8
|
+
* - `disabled_by_merchant` — its owner set `disabled: true`.
|
|
9
|
+
* - `delivery_failures` — every delivery to it failed for three days
|
|
10
|
+
* straight, so Peable stopped sending. Re-enabling it (`disabled: false`)
|
|
11
|
+
* starts the clock again.
|
|
12
|
+
*/
|
|
13
|
+
exports.WEBHOOK_ENDPOINT_DISABLED_REASONS = [
|
|
14
|
+
'disabled_by_merchant',
|
|
15
|
+
'delivery_failures',
|
|
16
|
+
];
|
|
17
|
+
/** At most this many endpoints per merchant (per mode). */
|
|
18
|
+
exports.WEBHOOK_ENDPOINTS_MAX_PER_MERCHANT = 16;
|
|
19
|
+
/** The longest a rolled secret may keep signing beside its replacement. */
|
|
20
|
+
exports.WEBHOOK_SECRET_ROLL_MAX_EXPIRES_IN_SECONDS = 24 * 60 * 60;
|
|
@@ -13,32 +13,40 @@ function computeSignature(secret, rawBody, timestamp) {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Produce a Stripe-style signature header for an outbound webhook:
|
|
16
|
-
* `t=${timestamp},v1=${
|
|
16
|
+
* `t=${timestamp},v1=${hex}` — with one `v1=` per secret.
|
|
17
|
+
*
|
|
18
|
+
* Several secrets are what make a rotation gapless: while an endpoint's rolled
|
|
19
|
+
* secret is still inside its overlap window, every delivery is signed with
|
|
20
|
+
* BOTH, so a receiver verifying with either one accepts it.
|
|
17
21
|
*/
|
|
18
22
|
function signWebhook(secret, rawBody, timestamp) {
|
|
19
|
-
|
|
23
|
+
const secrets = typeof secret === 'string' ? [secret] : secret;
|
|
24
|
+
if (secrets.length === 0)
|
|
25
|
+
throw new Error('signWebhook needs at least one secret');
|
|
26
|
+
const signatures = secrets.map((s) => `v1=${computeSignature(s, rawBody, timestamp)}`);
|
|
27
|
+
return `t=${timestamp},${signatures.join(',')}`;
|
|
20
28
|
}
|
|
21
29
|
function parseHeader(header) {
|
|
22
30
|
let timestamp = null;
|
|
23
|
-
|
|
31
|
+
const signatures = [];
|
|
24
32
|
for (const part of header.split(',')) {
|
|
25
33
|
const separator = part.indexOf('=');
|
|
26
34
|
if (separator === -1)
|
|
27
35
|
continue;
|
|
28
|
-
const key = part.slice(0, separator);
|
|
29
|
-
const value = part.slice(separator + 1);
|
|
36
|
+
const key = part.slice(0, separator).trim();
|
|
37
|
+
const value = part.slice(separator + 1).trim();
|
|
30
38
|
if (key === 't') {
|
|
31
39
|
const parsed = Number(value);
|
|
32
40
|
if (Number.isInteger(parsed))
|
|
33
41
|
timestamp = parsed;
|
|
34
42
|
}
|
|
35
43
|
else if (key === 'v1') {
|
|
36
|
-
|
|
44
|
+
signatures.push(value);
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
|
-
if (timestamp === null ||
|
|
47
|
+
if (timestamp === null || signatures.length === 0)
|
|
40
48
|
return null;
|
|
41
|
-
return { timestamp,
|
|
49
|
+
return { timestamp, signatures };
|
|
42
50
|
}
|
|
43
51
|
/**
|
|
44
52
|
* Constant-time hex-string comparison. Returns false for unequal-length inputs
|
|
@@ -53,9 +61,13 @@ function constantTimeEqualHex(a, b) {
|
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
63
|
* Verify a webhook signature header against the raw body.
|
|
56
|
-
*
|
|
64
|
+
*
|
|
65
|
+
* - Parses `t` and EVERY `v1` from the header.
|
|
57
66
|
* - Rejects if the timestamp drifts more than `toleranceSec` from `nowSec`.
|
|
58
|
-
* -
|
|
67
|
+
* - Accepts when any `v1` matches the signature computed with any of
|
|
68
|
+
* `secret` — so a receiver mid-rotation can pass both of its secrets, and a
|
|
69
|
+
* delivery signed during the sender's overlap window verifies against
|
|
70
|
+
* either.
|
|
59
71
|
*/
|
|
60
72
|
function verifyWebhook(secret, rawBody, header, toleranceSec, nowSec) {
|
|
61
73
|
const parsed = parseHeader(header);
|
|
@@ -63,6 +75,17 @@ function verifyWebhook(secret, rawBody, header, toleranceSec, nowSec) {
|
|
|
63
75
|
return false;
|
|
64
76
|
if (Math.abs(nowSec - parsed.timestamp) > toleranceSec)
|
|
65
77
|
return false;
|
|
66
|
-
const
|
|
67
|
-
|
|
78
|
+
const secrets = typeof secret === 'string' ? [secret] : secret;
|
|
79
|
+
let matched = false;
|
|
80
|
+
// Every comparison runs, so the time taken does not say which one matched.
|
|
81
|
+
for (const candidate of secrets) {
|
|
82
|
+
if (candidate.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
const expected = computeSignature(candidate, rawBody, parsed.timestamp);
|
|
85
|
+
for (const signature of parsed.signatures) {
|
|
86
|
+
if (constantTimeEqualHex(expected, signature))
|
|
87
|
+
matched = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return matched;
|
|
68
91
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** How a price's amount is computed from a quantity. */
|
|
2
|
+
export const PRICE_BILLING_SCHEMES = ['per_unit', 'tiered'];
|
|
3
|
+
/**
|
|
4
|
+
* How a tiered price reads its tiers.
|
|
5
|
+
*
|
|
6
|
+
* - `volume` — the WHOLE quantity is priced at the tier it falls into.
|
|
7
|
+
* - `graduated` — each unit is priced at the tier it falls into, so a
|
|
8
|
+
* quantity crossing two tiers pays each tier's rate for its
|
|
9
|
+
* share, plus the flat amount of every tier it reaches.
|
|
10
|
+
*/
|
|
11
|
+
export const PRICE_TIERS_MODES = ['graduated', 'volume'];
|
|
12
|
+
/** `recurring` when the price has a recurring component, `one_time` otherwise. */
|
|
13
|
+
export const PRICE_TYPES = ['one_time', 'recurring'];
|
|
14
|
+
export const RECURRING_INTERVALS = ['day', 'week', 'month', 'year'];
|
|
15
|
+
/**
|
|
16
|
+
* `licensed` bills the quantity on the subscription. `metered` bills reported
|
|
17
|
+
* usage and needs a meter; meters are not part of Orbit yet, so the gateway
|
|
18
|
+
* refuses a metered price rather than store one nothing can bill.
|
|
19
|
+
*/
|
|
20
|
+
export const RECURRING_USAGE_TYPES = ['licensed', 'metered'];
|
|
21
|
+
/**
|
|
22
|
+
* Whether a price includes tax (Peable Tax, ADR 0011). `unspecified` defers to
|
|
23
|
+
* the merchant's default tax behavior; once a price says `inclusive` or
|
|
24
|
+
* `exclusive` it can never change.
|
|
25
|
+
*/
|
|
26
|
+
export const PRICE_TAX_BEHAVIORS = ['inclusive', 'exclusive', 'unspecified'];
|
|
27
|
+
export const TRANSFORM_QUANTITY_ROUNDS = ['up', 'down'];
|
|
28
|
+
/** How long a coupon's discount lasts once applied to a subscription. */
|
|
29
|
+
export const COUPON_DURATIONS = ['once', 'repeating', 'forever'];
|
|
30
|
+
/** The bounds the gateway enforces, published so a form can say them first. */
|
|
31
|
+
export const CATALOG_LIMITS = Object.freeze({
|
|
32
|
+
productNameMax: 250,
|
|
33
|
+
productDescriptionMax: 40000,
|
|
34
|
+
productImagesMax: 8,
|
|
35
|
+
urlMax: 2048,
|
|
36
|
+
unitLabelMax: 12,
|
|
37
|
+
statementDescriptorMax: 22,
|
|
38
|
+
marketingFeaturesMax: 15,
|
|
39
|
+
marketingFeatureNameMax: 80,
|
|
40
|
+
priceNicknameMax: 250,
|
|
41
|
+
lookupKeyMax: 200,
|
|
42
|
+
lookupKeysFilterMax: 10,
|
|
43
|
+
priceTiersMax: 250,
|
|
44
|
+
couponIdMax: 64,
|
|
45
|
+
couponNameMax: 40,
|
|
46
|
+
couponAppliesToProductsMax: 100,
|
|
47
|
+
couponDurationInMonthsMax: 1200,
|
|
48
|
+
promotionCodeMax: 64,
|
|
49
|
+
idsFilterMax: 100,
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|