@peable.to/shared-types 0.2.0 → 0.3.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/billing.js +6 -0
- package/dist/cjs/dispute.js +17 -0
- package/dist/cjs/index.js +16 -1
- package/dist/cjs/money.js +48 -8
- package/dist/cjs/paymentIntent.js +185 -9
- package/dist/cjs/settlement.js +2 -0
- package/dist/cjs/social.js +19 -0
- package/dist/esm/billing.js +3 -0
- package/dist/esm/dispute.js +16 -0
- package/dist/esm/index.js +4 -2
- package/dist/esm/money.js +45 -7
- package/dist/esm/paymentIntent.js +182 -8
- package/dist/esm/settlement.js +1 -0
- package/dist/esm/social.js +18 -1
- package/dist/types/billing.d.ts +45 -0
- package/dist/types/checkoutSession.d.ts +12 -3
- package/dist/types/dispute.d.ts +124 -0
- package/dist/types/event.d.ts +113 -9
- package/dist/types/index.d.ts +8 -5
- package/dist/types/merchant.d.ts +21 -2
- package/dist/types/money.d.ts +31 -3
- package/dist/types/paymentIntent.d.ts +75 -8
- package/dist/types/paymentLink.d.ts +19 -4
- package/dist/types/settlement.d.ts +171 -0
- package/dist/types/social.d.ts +110 -0
- package/dist/types/webhookDelivery.d.ts +38 -2
- package/package.json +1 -1
|
@@ -1,22 +1,111 @@
|
|
|
1
|
+
/** {@link PaymentIntentRail} as the tuple the database CHECK reads. */
|
|
2
|
+
export const PAYMENT_INTENT_RAILS = ['faircoin', 'card'];
|
|
3
|
+
/**
|
|
4
|
+
* Which statuses belong to which rail (ADR 0001 D5).
|
|
5
|
+
*
|
|
6
|
+
* The four chain states describe a transaction on a blockchain and the four
|
|
7
|
+
* card states describe an authorization at a card acquirer; neither set is
|
|
8
|
+
* expressible on the other rail. Mapping a card authorization onto `broadcast`
|
|
9
|
+
* would be cheaper and would be a lie — nothing was broadcast, there is no
|
|
10
|
+
* transaction, and `payment_intents_broadcast_requires_txid_check` would then
|
|
11
|
+
* be demanding a txid from a payment that can never have one.
|
|
12
|
+
*
|
|
13
|
+
* `db/schema/payments.ts` renders these two sets into CHECK constraints, so the
|
|
14
|
+
* restriction holds against a write that skipped the application.
|
|
15
|
+
*/
|
|
16
|
+
export const CHAIN_ONLY_STATUSES = [
|
|
17
|
+
'awaiting_approval',
|
|
18
|
+
'approved',
|
|
19
|
+
'broadcast',
|
|
20
|
+
'confirming',
|
|
21
|
+
];
|
|
22
|
+
export const CARD_ONLY_STATUSES = [
|
|
23
|
+
'requires_action',
|
|
24
|
+
'processing',
|
|
25
|
+
'refunded',
|
|
26
|
+
'partially_refunded',
|
|
27
|
+
];
|
|
1
28
|
/**
|
|
2
29
|
* Legal forward transitions of the PaymentIntent lifecycle. Terminal states
|
|
3
|
-
* (
|
|
30
|
+
* (refunded/expired/failed/rejected) allow no further transition here — the one
|
|
4
31
|
* documented exception, a reorg dropping `settled` back to `confirming`, is
|
|
5
32
|
* modelled by the backend state machine, not this happy-path table.
|
|
33
|
+
*
|
|
34
|
+
* ONE table for both rails, deliberately. It answers "is this a legal lifecycle
|
|
35
|
+
* edge at all"; WHICH rail may reach a given status is the separate, structural
|
|
36
|
+
* question the two sets above answer, in the database. A rail-parameterised
|
|
37
|
+
* table would put the same restriction in two places and let them disagree.
|
|
38
|
+
*
|
|
39
|
+
* Self-transitions are absent because `applyEvent` short-circuits on
|
|
40
|
+
* `current === target` before consulting this table — that is the idempotent
|
|
41
|
+
* re-poll of the settlement watcher, and a second partial refund, which change
|
|
42
|
+
* an amount without changing a status.
|
|
6
43
|
*/
|
|
7
44
|
const ALLOWED = {
|
|
8
|
-
// `created` supports
|
|
9
|
-
// approved → broadcast)
|
|
10
|
-
// and reports the txid directly (→ broadcast),
|
|
11
|
-
//
|
|
12
|
-
|
|
45
|
+
// `created` supports three paths: the rich wallet flow (→ awaiting_approval →
|
|
46
|
+
// approved → broadcast), the minimal path where the payer pays out-of-band
|
|
47
|
+
// and reports the txid directly (→ broadcast), and the card path (→
|
|
48
|
+
// requires_action for an SCA challenge, → processing, or straight to settled
|
|
49
|
+
// for a charge that confirms in one call). `failed` is reachable from here
|
|
50
|
+
// because a card can be declined on first confirmation, having never been
|
|
51
|
+
// anything else; no FairCoin path uses that edge, since `underpaid` is only
|
|
52
|
+
// emitted for an intent that already carries a txid.
|
|
53
|
+
created: [
|
|
54
|
+
'awaiting_approval',
|
|
55
|
+
'broadcast',
|
|
56
|
+
'requires_action',
|
|
57
|
+
'processing',
|
|
58
|
+
'settled',
|
|
59
|
+
'failed',
|
|
60
|
+
'rejected',
|
|
61
|
+
'expired',
|
|
62
|
+
],
|
|
13
63
|
awaiting_approval: ['approved', 'rejected', 'expired'],
|
|
14
64
|
approved: ['broadcast', 'failed'],
|
|
15
65
|
broadcast: ['confirming', 'failed'],
|
|
16
66
|
confirming: ['settled', 'failed'],
|
|
17
|
-
|
|
67
|
+
requires_action: ['processing', 'settled', 'failed', 'expired', 'rejected'],
|
|
68
|
+
processing: ['settled', 'failed'],
|
|
69
|
+
/**
|
|
70
|
+
* A failed ATTEMPT is not a failed payment, on the card rail.
|
|
71
|
+
*
|
|
72
|
+
* `payment_intent.payment_failed` means one authorization attempt was
|
|
73
|
+
* declined; Stripe returns the PaymentIntent to `requires_payment_method` and
|
|
74
|
+
* the payer can confirm it again with another card. With `failed` terminal,
|
|
75
|
+
* the later `payment_intent.succeeded` was an ILLEGAL transition — so the
|
|
76
|
+
* drain recorded a processing failure and retried it forever, the payment
|
|
77
|
+
* stayed `failed`, and a merchant who had been paid was told their payment
|
|
78
|
+
* had been declined.
|
|
79
|
+
*
|
|
80
|
+
* On the chain rail `failed` means `underpaid`, which IS terminal — coins
|
|
81
|
+
* arrived and were not enough. Nothing here opens that door: `underpaid` is
|
|
82
|
+
* emitted only by the settlement watcher, and `LEGAL_SOURCES` in
|
|
83
|
+
* `services/intentState.ts` keeps every `card_*` event to card statuses and
|
|
84
|
+
* the chain's `confirmed` to `confirming`. This table answers "is the edge
|
|
85
|
+
* legal at all"; which rail may walk it is the separate question those two
|
|
86
|
+
* guards answer.
|
|
87
|
+
*/
|
|
88
|
+
failed: ['requires_action', 'processing', 'settled'],
|
|
89
|
+
// Money coming back requires money to have arrived, so both refund states are
|
|
90
|
+
// reachable only from `settled`.
|
|
91
|
+
settled: ['refunded', 'partially_refunded'],
|
|
92
|
+
/**
|
|
93
|
+
* ...and money coming back can be UNDONE, which is why these edges run both
|
|
94
|
+
* ways.
|
|
95
|
+
*
|
|
96
|
+
* A refund is not final when the provider accepts it: a bank can reject one
|
|
97
|
+
* days later, and the provider then says so (`refund.failed`). Without a path
|
|
98
|
+
* back, a payment whose only refund failed would claim `refunded` forever —
|
|
99
|
+
* the merchant's books saying money went back that is still with them, and no
|
|
100
|
+
* transition anywhere able to correct it.
|
|
101
|
+
*
|
|
102
|
+
* The target is always recomputed from the SUM of succeeded refunds
|
|
103
|
+
* (`applyRefundToIntent`), never stepped, so these edges describe where that
|
|
104
|
+
* sum can legally land rather than a sequence anyone walks.
|
|
105
|
+
*/
|
|
106
|
+
partially_refunded: ['refunded', 'settled'],
|
|
107
|
+
refunded: ['partially_refunded', 'settled'],
|
|
18
108
|
expired: [],
|
|
19
|
-
failed: [],
|
|
20
109
|
rejected: [],
|
|
21
110
|
};
|
|
22
111
|
export function isValidStatusTransition(from, to) {
|
|
@@ -25,3 +114,88 @@ export function isValidStatusTransition(from, to) {
|
|
|
25
114
|
/** Every legal `PaymentIntentStatus`, derived from `ALLOWED`'s keys — the
|
|
26
115
|
* single source of truth consumed by the Gateway's list-route status filter. */
|
|
27
116
|
export const PAYMENT_INTENT_STATUSES = Object.keys(ALLOWED);
|
|
117
|
+
/**
|
|
118
|
+
* The statuses a payer can still complete a payment FROM.
|
|
119
|
+
*
|
|
120
|
+
* **Twice now this question has been answered by walking the transition table,
|
|
121
|
+
* and twice the table has moved underneath it.**
|
|
122
|
+
*
|
|
123
|
+
* The hosted checkout first asked leaf-ness — "has this status no outgoing
|
|
124
|
+
* edges" — which meant `settled` while `settled` had none. Adding the refund
|
|
125
|
+
* transitions (`settled → refunded | partially_refunded`) gave it edges, so a
|
|
126
|
+
* settled payment stopped looking finished, and the checkout began REUSING a
|
|
127
|
+
* remembered settled intent instead of minting a fresh one: a payer who had
|
|
128
|
+
* already paid saw their old receipt forever, with no way to pay the link
|
|
129
|
+
* again.
|
|
130
|
+
*
|
|
131
|
+
* The fix was reachability — "is `settled` still reachable" — and that held
|
|
132
|
+
* until refunds could be UNDONE. `refunded → settled` (a bank rejecting a
|
|
133
|
+
* refund days later) makes `settled` reachable from `settled` in two steps, so
|
|
134
|
+
* reachability answers `true` for a payment that is finished, and the original
|
|
135
|
+
* bug returns unchanged.
|
|
136
|
+
*
|
|
137
|
+
* So the set is stated, not derived. It is exactly the statuses in which the
|
|
138
|
+
* gateway is still waiting on the PAYER — which is what the question means, in
|
|
139
|
+
* the payer's own terms, and is the one formulation no edge added elsewhere can
|
|
140
|
+
* silently change. `paymentIntent.test.ts` pins it against the table: every
|
|
141
|
+
* member must still be able to reach `settled` without passing through a
|
|
142
|
+
* post-settlement status, and no non-member may.
|
|
143
|
+
*/
|
|
144
|
+
const PAYABLE_STATUSES = new Set([
|
|
145
|
+
'created',
|
|
146
|
+
'awaiting_approval',
|
|
147
|
+
'approved',
|
|
148
|
+
'broadcast',
|
|
149
|
+
'confirming',
|
|
150
|
+
'requires_action',
|
|
151
|
+
'processing',
|
|
152
|
+
]);
|
|
153
|
+
/**
|
|
154
|
+
* Whether a payer can still complete a payment against an intent in this
|
|
155
|
+
* status.
|
|
156
|
+
*
|
|
157
|
+
* `settled` answers `false`: it is where paying ENDS. `partially_refunded`
|
|
158
|
+
* answers `false` too — money can still move, but not from the payer.
|
|
159
|
+
*/
|
|
160
|
+
export function canStillBePaid(status) {
|
|
161
|
+
return PAYABLE_STATUSES.has(status);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The statuses a payment reaches only AFTER the payer has finished.
|
|
165
|
+
*
|
|
166
|
+
* Exported for the gate below rather than for production use: the property that
|
|
167
|
+
* makes `PAYABLE_STATUSES` checkable is that a payable status reaches `settled`
|
|
168
|
+
* without going through one of these.
|
|
169
|
+
*/
|
|
170
|
+
export const POST_SETTLEMENT_STATUSES = [
|
|
171
|
+
'settled',
|
|
172
|
+
'refunded',
|
|
173
|
+
'partially_refunded',
|
|
174
|
+
];
|
|
175
|
+
/**
|
|
176
|
+
* Whether `settled` is reachable from `status` without passing through a
|
|
177
|
+
* post-settlement status. The derivation `canStillBePaid` used to BE, kept as
|
|
178
|
+
* the thing that checks it rather than as the thing that answers it.
|
|
179
|
+
*/
|
|
180
|
+
export function reachesSettlementFromPayer(status) {
|
|
181
|
+
const blocked = new Set(POST_SETTLEMENT_STATUSES);
|
|
182
|
+
// A payment that has ALREADY settled is one the payer has finished with,
|
|
183
|
+
// whatever edges lead out of it. Checked before the walk rather than inside
|
|
184
|
+
// it, because `partially_refunded → settled` is a direct edge and the walk
|
|
185
|
+
// would find it in one step — and that edge is a refund being undone, not a
|
|
186
|
+
// payer paying.
|
|
187
|
+
if (blocked.has(status))
|
|
188
|
+
return false;
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const queue = [...ALLOWED[status]];
|
|
191
|
+
while (queue.length > 0) {
|
|
192
|
+
const next = queue.shift();
|
|
193
|
+
if (next === 'settled')
|
|
194
|
+
return true;
|
|
195
|
+
if (seen.has(next) || blocked.has(next))
|
|
196
|
+
continue;
|
|
197
|
+
seen.add(next);
|
|
198
|
+
queue.push(...ALLOWED[next]);
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/esm/social.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
// Social-receive + transaction-enrichment contracts — shared by the Peable
|
|
2
2
|
// Gateway backend, the wallet frontend, and (indirectly) the enrichment
|
|
3
3
|
// service's callers. Mirrors the Stripe-parity style of paymentIntent.ts.
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Longest `SocialPaymentSource.app` the gateway accepts. An app slug, not a
|
|
6
|
+
* label — 32 characters is more than any Oxy app id needs and short enough that
|
|
7
|
+
* the column can never become a place to put prose.
|
|
8
|
+
*
|
|
9
|
+
* Published with the contract because it is what a request is REFUSED for: a
|
|
10
|
+
* caller that cannot read the bound from the types finds it out with a 422.
|
|
11
|
+
*/
|
|
12
|
+
export const SOCIAL_SOURCE_APP_MAX_LENGTH = 32;
|
|
13
|
+
/**
|
|
14
|
+
* Longest `SocialPaymentSource.ref` the gateway accepts.
|
|
15
|
+
*
|
|
16
|
+
* The bound is the whole defence here, because the value is opaque: Peable
|
|
17
|
+
* never parses, resolves or links a `ref`, so nothing downstream would notice
|
|
18
|
+
* it growing. 128 characters holds any id an app mints (a uuid, a snowflake, a
|
|
19
|
+
* base64 hash) and refuses a payload.
|
|
20
|
+
*/
|
|
21
|
+
export const SOCIAL_SOURCE_REF_MAX_LENGTH = 128;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Recurring billing transport. Store/plan references belong to the authenticated app;
|
|
2
|
+
* they do not identify an Oxy payer or convey entitlements. */
|
|
3
|
+
export declare const BILLING_SUBSCRIPTION_STATUSES: readonly ["incomplete", "incomplete_expired", "trialing", "active", "past_due", "canceled", "unpaid", "paused"];
|
|
4
|
+
export type BillingSubscriptionStatus = (typeof BILLING_SUBSCRIPTION_STATUSES)[number];
|
|
5
|
+
export interface BillingCustomer {
|
|
6
|
+
providerCustomerId: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EnsureBillingCustomerParams {
|
|
9
|
+
storeId: string;
|
|
10
|
+
storeName: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CreateBillingCheckoutParams {
|
|
13
|
+
providerCustomerId: string;
|
|
14
|
+
providerPriceId: string;
|
|
15
|
+
trialDays: number;
|
|
16
|
+
returnUrl: string;
|
|
17
|
+
storeId: string;
|
|
18
|
+
planId: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CreateBillingPortalParams {
|
|
21
|
+
providerCustomerId: string;
|
|
22
|
+
returnUrl: string;
|
|
23
|
+
}
|
|
24
|
+
/** Sensitive owner-only URL. The gateway refuses replay after expiresAt. */
|
|
25
|
+
export interface BillingHostedSession {
|
|
26
|
+
url: string;
|
|
27
|
+
expiresAt: string;
|
|
28
|
+
}
|
|
29
|
+
/** Provider observations only. Consumers own plan/status/entitlement interpretation. */
|
|
30
|
+
export interface BillingSubscription {
|
|
31
|
+
providerSubscriptionId: string;
|
|
32
|
+
providerCustomerId: string;
|
|
33
|
+
providerPriceId: string;
|
|
34
|
+
storeId: string;
|
|
35
|
+
planId: string;
|
|
36
|
+
livemode: boolean;
|
|
37
|
+
status: BillingSubscriptionStatus;
|
|
38
|
+
interval: 'month' | 'year';
|
|
39
|
+
cancelAtPeriodEnd: boolean;
|
|
40
|
+
currentPeriodStart: string;
|
|
41
|
+
currentPeriodEnd: string;
|
|
42
|
+
trialEndsAt: string | null;
|
|
43
|
+
cancelAt: string | null;
|
|
44
|
+
cancelledAt: string | null;
|
|
45
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { CurrencyCode } from './money';
|
|
1
2
|
import type { NetworkType } from './network';
|
|
2
3
|
import type { MerchantDisplay } from './merchantDisplay';
|
|
3
|
-
import type { PaymentIntent } from './paymentIntent';
|
|
4
|
+
import type { PaymentIntent, PaymentIntentRail } from './paymentIntent';
|
|
4
5
|
/** The merchant/SDK-facing DTO — returned on create and by merchant retrieve. */
|
|
5
6
|
export interface CheckoutSession {
|
|
6
7
|
id: string;
|
|
@@ -9,7 +10,10 @@ export interface CheckoutSession {
|
|
|
9
10
|
/** The wrapped intent's `client_secret`, returned to the merchant on create. */
|
|
10
11
|
clientSecret: string;
|
|
11
12
|
amount: string;
|
|
12
|
-
|
|
13
|
+
currency: CurrencyCode;
|
|
14
|
+
rail: PaymentIntentRail;
|
|
15
|
+
/** FairCoin rail only; `null` on a card session. */
|
|
16
|
+
network: NetworkType | null;
|
|
13
17
|
metadata: Record<string, string>;
|
|
14
18
|
successUrl?: string;
|
|
15
19
|
cancelUrl?: string;
|
|
@@ -34,7 +38,12 @@ export interface CheckoutSessionPublic {
|
|
|
34
38
|
}
|
|
35
39
|
export interface CreateCheckoutSessionParams {
|
|
36
40
|
amount: string;
|
|
37
|
-
|
|
41
|
+
/** Defaults to `'faircoin'`, so pre-ADR-0001 integrations keep working. */
|
|
42
|
+
rail?: PaymentIntentRail;
|
|
43
|
+
/** Required on the faircoin rail; refused on the card rail. */
|
|
44
|
+
network?: NetworkType;
|
|
45
|
+
/** Defaults to `'FAIR'` on the faircoin rail; required on the card rail. */
|
|
46
|
+
currency?: CurrencyCode;
|
|
38
47
|
metadata?: Record<string, string>;
|
|
39
48
|
successUrl?: string;
|
|
40
49
|
cancelUrl?: string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dispute — the card network holding a settled payment's money while a
|
|
3
|
+
* cardholder contests it.
|
|
4
|
+
*
|
|
5
|
+
* Published because `payment_intent.disputed` and `payment_intent.dispute_closed`
|
|
6
|
+
* carry it as their `data.object`. It was route-local at first, with the webhook
|
|
7
|
+
* carrying the PaymentIntent and the detail reachable only through
|
|
8
|
+
* `GET /v1/payment_intents/:id/disputes`. That was inconsistent with the reason
|
|
9
|
+
* those two events exist: `disputed` is a DEADLINE, and a merchant who has to
|
|
10
|
+
* make a second call to learn when evidence is due is a merchant who can miss it
|
|
11
|
+
* while their integration is working exactly as documented.
|
|
12
|
+
*
|
|
13
|
+
* It is also what the provider does. Stripe's `charge.dispute.created` carries
|
|
14
|
+
* the dispute, not the charge — the parity was half-applied.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Where the dispute stands.
|
|
18
|
+
*
|
|
19
|
+
* `needs_response` and `under_review` are open; `won` and `lost` are terminal
|
|
20
|
+
* and are the only two that say where the money ended up. There is deliberately
|
|
21
|
+
* no `closed`: a merchant who is told a dispute "closed" without being told
|
|
22
|
+
* which way has been given a notification and no information.
|
|
23
|
+
*/
|
|
24
|
+
export type DisputeStatus = 'needs_response' | 'under_review' | 'won' | 'lost';
|
|
25
|
+
export interface Dispute {
|
|
26
|
+
id: string;
|
|
27
|
+
object: 'dispute';
|
|
28
|
+
/** The PUBLIC `pi_…` id, which is what every other contract here calls `id`. */
|
|
29
|
+
paymentIntentId: string;
|
|
30
|
+
/**
|
|
31
|
+
* Amount in the currency's smallest unit as a canonical integer string —
|
|
32
|
+
* the same encoding every other money field on these contracts uses.
|
|
33
|
+
*
|
|
34
|
+
* It is the DISPUTED amount, which may be less than the payment's: a
|
|
35
|
+
* cardholder can contest part of a charge.
|
|
36
|
+
*/
|
|
37
|
+
amount: string;
|
|
38
|
+
currency: string;
|
|
39
|
+
status: DisputeStatus;
|
|
40
|
+
/**
|
|
41
|
+
* The network's reason, verbatim and unmapped.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately an open string rather than a closed set. It is the one field a
|
|
44
|
+
* merchant argues against, and a gateway paraphrase would be the words they
|
|
45
|
+
* quote while the acquirer holds different ones. `null` when the network sent
|
|
46
|
+
* none.
|
|
47
|
+
*/
|
|
48
|
+
reason: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* When the network stops accepting evidence, ISO-8601.
|
|
51
|
+
*
|
|
52
|
+
* `null` once the dispute is closed — there is no deadline for a decision
|
|
53
|
+
* already made, and a closed dispute still showing one would put a response
|
|
54
|
+
* in front of a merchant that can no longer be given.
|
|
55
|
+
*/
|
|
56
|
+
evidenceDueAt: string | null;
|
|
57
|
+
/**
|
|
58
|
+
* When the merchant's response reached the network, ISO-8601. `null` while
|
|
59
|
+
* none has been submitted.
|
|
60
|
+
*
|
|
61
|
+
* The timestamp and nothing else: the evidence itself is forwarded to the
|
|
62
|
+
* acquirer and never stored here. What it SAID is readable there, by someone
|
|
63
|
+
* with their own authorization — this gateway does not keep a copy of a
|
|
64
|
+
* customer's name, address and correspondence.
|
|
65
|
+
*
|
|
66
|
+
* Together with `evidenceDueAt` it answers the only two questions a merchant
|
|
67
|
+
* has about a live dispute: whether they still owe a response, and by when.
|
|
68
|
+
*/
|
|
69
|
+
evidenceSubmittedAt: string | null;
|
|
70
|
+
createdAt: string;
|
|
71
|
+
updatedAt: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A merchant's response to a dispute — the whole field set, declared once.
|
|
75
|
+
*
|
|
76
|
+
* ## Why this is the wire contract and not four private copies
|
|
77
|
+
*
|
|
78
|
+
* It was four: the backend's provider port, the route's zod schema, the Stripe
|
|
79
|
+
* adapter's name map, and the SDK's params type. Every one of them is the SAME
|
|
80
|
+
* list, and the cost of them drifting is specific and silent — a field added to
|
|
81
|
+
* three of the four is one a merchant sends, the gateway accepts, and the
|
|
82
|
+
* network never sees. They find out when the dispute is decided against them.
|
|
83
|
+
*
|
|
84
|
+
* So the set lives here, with the wire contracts, and the other three are
|
|
85
|
+
* pinned to it: `provider.ts` aliases it, `routes/disputes.ts` declares its
|
|
86
|
+
* schema `satisfies Record<keyof DisputeEvidence, …>`, and the Stripe adapter's
|
|
87
|
+
* map is a total `Record<keyof DisputeEvidence, …>`. Adding a field here makes
|
|
88
|
+
* all three fail to compile until they carry it.
|
|
89
|
+
*
|
|
90
|
+
* ## What is not here
|
|
91
|
+
*
|
|
92
|
+
* The field NAMES are the network's, spelled the way the rest of these
|
|
93
|
+
* contracts spell things. They are not a Peable vocabulary and are not mapped
|
|
94
|
+
* to one: a merchant assembling a defence is reading their acquirer's guidance,
|
|
95
|
+
* and a gateway that renamed the fields would make that guidance not apply.
|
|
96
|
+
*
|
|
97
|
+
* FILE attachments are deliberately absent. They need the provider's upload
|
|
98
|
+
* API, a size and type policy, and somewhere for the bytes to live on the way
|
|
99
|
+
* through — and offering half of that would let a merchant submit a defence
|
|
100
|
+
* missing the receipt it rests on.
|
|
101
|
+
*
|
|
102
|
+
* None of it is ever stored. It carries a customer's name, their email, a
|
|
103
|
+
* billing address and correspondence; it is forwarded to the acquirer and
|
|
104
|
+
* forgotten. What is recorded is `evidenceSubmittedAt` and nothing more.
|
|
105
|
+
*/
|
|
106
|
+
export interface DisputeEvidence {
|
|
107
|
+
productDescription?: string;
|
|
108
|
+
customerName?: string;
|
|
109
|
+
customerEmailAddress?: string;
|
|
110
|
+
customerPurchaseIp?: string;
|
|
111
|
+
billingAddress?: string;
|
|
112
|
+
shippingAddress?: string;
|
|
113
|
+
shippingCarrier?: string;
|
|
114
|
+
shippingDate?: string;
|
|
115
|
+
shippingTrackingNumber?: string;
|
|
116
|
+
serviceDate?: string;
|
|
117
|
+
accessActivityLog?: string;
|
|
118
|
+
cancellationPolicyDisclosure?: string;
|
|
119
|
+
cancellationRebuttal?: string;
|
|
120
|
+
duplicateChargeExplanation?: string;
|
|
121
|
+
refundPolicyDisclosure?: string;
|
|
122
|
+
refundRefusalExplanation?: string;
|
|
123
|
+
uncategorizedText?: string;
|
|
124
|
+
}
|
package/dist/types/event.d.ts
CHANGED
|
@@ -1,11 +1,115 @@
|
|
|
1
|
+
import type { Dispute } from './dispute';
|
|
1
2
|
import type { PaymentIntent } from './paymentIntent';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import type { ConnectedAccount } from './settlement';
|
|
4
|
+
export type WebhookEventType = 'payment_intent.confirming' | 'payment_intent.settled' | 'payment_intent.failed' | 'payment_intent.rejected' | 'payment_intent.expired'
|
|
5
|
+
/**
|
|
6
|
+
* Money went back to the payer.
|
|
7
|
+
*
|
|
8
|
+
* Both of these exist for the refund a merchant did NOT initiate — one made
|
|
9
|
+
* from the provider's own dashboard, or a dispute the network resolved
|
|
10
|
+
* against the merchant. A merchant who only ever learned about their own
|
|
11
|
+
* refunds would reconcile to a balance that disagrees with the provider's,
|
|
12
|
+
* with nothing to explain the difference.
|
|
13
|
+
*
|
|
14
|
+
* Additive to a published contract: an existing consumer that switches on
|
|
15
|
+
* this union keeps compiling and simply never matches them.
|
|
16
|
+
*/
|
|
17
|
+
| 'payment_intent.refunded' | 'payment_intent.partially_refunded'
|
|
18
|
+
/**
|
|
19
|
+
* The network is holding this payment's money while a cardholder contests it.
|
|
20
|
+
*
|
|
21
|
+
* Two events and not one because the two moments demand different things of a
|
|
22
|
+
* merchant. `disputed` is a DEADLINE — evidence is due, and a merchant who
|
|
23
|
+
* learns about it late has already lost. `dispute_closed` carries the outcome,
|
|
24
|
+
* which is when the money is finally theirs or finally gone.
|
|
25
|
+
*
|
|
26
|
+
* Neither is a payment-intent STATUS. A dispute is the network's process, not
|
|
27
|
+
* a stage of the payment's lifecycle — the payment stayed `settled` throughout
|
|
28
|
+
* and may end there. That is also how Stripe models it: a charge is
|
|
29
|
+
* `disputed`, a PaymentIntent has no such status.
|
|
30
|
+
*
|
|
31
|
+
* These two carry a {@link Dispute} as their `data.object`, NOT the payment
|
|
32
|
+
* intent — see {@link WebhookEventPayload}. The `paymentIntentId` on it is
|
|
33
|
+
* how a consumer gets back to the payment.
|
|
34
|
+
*
|
|
35
|
+
* Additive to a published contract: an existing consumer that switches on
|
|
36
|
+
* this union keeps compiling and simply never matches them.
|
|
37
|
+
*/
|
|
38
|
+
| 'payment_intent.disputed' | 'payment_intent.dispute_closed'
|
|
39
|
+
/**
|
|
40
|
+
* A seller's account changed — their READINESS, in practice.
|
|
41
|
+
*
|
|
42
|
+
* The first event here that is not about a payment, and it exists because
|
|
43
|
+
* readiness is the one fact about a seller a marketplace cannot act without
|
|
44
|
+
* and could not learn: the gateway refreshed the account row from the
|
|
45
|
+
* provider and told the merchant nothing, so the only ways to find out a
|
|
46
|
+
* seller had finished onboarding were to poll `GET /v1/connected_accounts`
|
|
47
|
+
* or to attempt a settlement and read the refusal.
|
|
48
|
+
*
|
|
49
|
+
* Emitted when the fields a merchant ACTS on change — payability, the two
|
|
50
|
+
* capabilities, the requirement counts, the disabled reasons — and not on
|
|
51
|
+
* every refresh. A periodic sync sweep re-reads every account whether or not
|
|
52
|
+
* anything moved, and an event per sweep would be a stream a merchant learns
|
|
53
|
+
* to ignore.
|
|
54
|
+
*
|
|
55
|
+
* It carries a {@link ConnectedAccount} and its delivery names NO payment
|
|
56
|
+
* intent, which is why `WebhookDelivery.intentId` is nullable.
|
|
57
|
+
*
|
|
58
|
+
* Additive to a published contract: an existing consumer that switches on
|
|
59
|
+
* this union keeps compiling and simply never matches it.
|
|
60
|
+
*/
|
|
61
|
+
| 'connected_account.updated';
|
|
62
|
+
/**
|
|
63
|
+
* Which resource each event type carries under `data.object`.
|
|
64
|
+
*
|
|
65
|
+
* TOTAL over `WebhookEventType`, so a new event type cannot be added without
|
|
66
|
+
* saying what it delivers. That matters more here than anywhere else in this
|
|
67
|
+
* contract: `data.object` is the whole payload, and an event type added with
|
|
68
|
+
* the default would silently ship a PaymentIntent to a consumer expecting
|
|
69
|
+
* something else — which typechecks on both sides and fails only in production.
|
|
70
|
+
*/
|
|
71
|
+
export interface WebhookEventPayload {
|
|
72
|
+
'payment_intent.confirming': PaymentIntent;
|
|
73
|
+
'payment_intent.settled': PaymentIntent;
|
|
74
|
+
'payment_intent.failed': PaymentIntent;
|
|
75
|
+
'payment_intent.rejected': PaymentIntent;
|
|
76
|
+
'payment_intent.expired': PaymentIntent;
|
|
77
|
+
'payment_intent.refunded': PaymentIntent;
|
|
78
|
+
'payment_intent.partially_refunded': PaymentIntent;
|
|
79
|
+
'payment_intent.disputed': Dispute;
|
|
80
|
+
'payment_intent.dispute_closed': Dispute;
|
|
81
|
+
'connected_account.updated': ConnectedAccount;
|
|
11
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* One delivered event.
|
|
85
|
+
*
|
|
86
|
+
* A DISCRIMINATED union over every event type, so `data.object` is narrowed by
|
|
87
|
+
* a switch on `type` with no cast:
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* if (event.type === 'payment_intent.disputed') {
|
|
91
|
+
* event.data.object.evidenceDueAt; // Dispute, not PaymentIntent
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* That correlation is the whole point of building it this way rather than as
|
|
96
|
+
* `{ type: WebhookEventType; data: { object: PaymentIntent | Dispute } }`,
|
|
97
|
+
* which typechecks identically at the boundary and then lets a consumer read a
|
|
98
|
+
* `Dispute`'s fields off a settlement.
|
|
99
|
+
*
|
|
100
|
+
* The parameter narrows to a subset of event types — `WebhookEvent<'payment_intent.disputed'>`
|
|
101
|
+
* is the dispute event alone. It takes an event TYPE and not a payload type,
|
|
102
|
+
* because the payload is derivable from the event and the reverse is not: two
|
|
103
|
+
* event types share `Dispute`.
|
|
104
|
+
*/
|
|
105
|
+
export type WebhookEvent<K extends WebhookEventType = WebhookEventType> = {
|
|
106
|
+
[T in K]: {
|
|
107
|
+
id: string;
|
|
108
|
+
object: 'event';
|
|
109
|
+
type: T;
|
|
110
|
+
created: string;
|
|
111
|
+
data: {
|
|
112
|
+
object: WebhookEventPayload[T];
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}[K];
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
export { UNITS_PER_COIN, isBaseUnitString } from './money';
|
|
1
|
+
export { UNITS_PER_COIN, isBaseUnitString, type CurrencyCode, CURRENCY_CODES, CURRENCY_DECIMALS, decimalsFor, isCurrencyCode, } from './money';
|
|
2
2
|
export { type NetworkType } from './network';
|
|
3
|
-
export { type PaymentIntentStatus, type PaymentIntent, type CreatePaymentIntentParams, isValidStatusTransition, PAYMENT_INTENT_STATUSES, } from './paymentIntent';
|
|
4
|
-
export { type WebhookEventType, type WebhookEvent } from './event';
|
|
5
|
-
export { type
|
|
3
|
+
export { type PaymentIntentStatus, type PaymentIntentRail, type PaymentIntent, type CreatePaymentIntentParams, isValidStatusTransition, canStillBePaid, reachesSettlementFromPayer, POST_SETTLEMENT_STATUSES, PAYMENT_INTENT_STATUSES, PAYMENT_INTENT_RAILS, CHAIN_ONLY_STATUSES, CARD_ONLY_STATUSES, } from './paymentIntent';
|
|
4
|
+
export { type WebhookEventType, type WebhookEvent, type WebhookEventPayload, } from './event';
|
|
5
|
+
export { type Dispute, type DisputeEvidence, type DisputeStatus } from './dispute';
|
|
6
|
+
export { type CapabilityStatus, type ConnectedAccount, type Refund, type RefundOrigin, type RefundStatus, type Settlement, type Transfer, type TransferReversal, type TransferReversalStatus, type TransferStatus, type TransferWithReversal, } from './settlement';
|
|
7
|
+
export { type WebhookDelivery, type WebhookDeliveryStatus, } from './webhookDelivery';
|
|
6
8
|
export { signWebhook, verifyWebhook } from './webhookSigner';
|
|
7
9
|
export { type MerchantEnvironment, type Merchant, MERCHANT_ENVIRONMENTS, } from './merchant';
|
|
8
10
|
export { type MerchantDisplay } from './merchantDisplay';
|
|
9
11
|
export { type PaymentLink, type PublicPaymentLink, type CreatePaymentLinkParams, } from './paymentLink';
|
|
10
12
|
export { type CheckoutSession, type CheckoutSessionPublic, type CreateCheckoutSessionParams, } from './checkoutSession';
|
|
11
|
-
export { type SocialNextAddressResponse, type SocialReceiveCursorResponse, type EnrichmentKind, type EnrichmentResult, type EnrichRequest, type EnrichResponse, } from './social';
|
|
13
|
+
export { SOCIAL_SOURCE_APP_MAX_LENGTH, SOCIAL_SOURCE_REF_MAX_LENGTH, type SocialPaymentSource, type SocialNextAddressRequest, type SocialNextAddressResponse, type SocialReceiveCursorResponse, type SocialPaymentDirection, type SocialPayment, type SocialPaymentsResponse, type EnrichmentKind, type EnrichmentResult, type EnrichRequest, type EnrichResponse, } from './social';
|
|
14
|
+
export { BILLING_SUBSCRIPTION_STATUSES, type BillingSubscriptionStatus, type BillingCustomer, type EnsureBillingCustomerParams, type CreateBillingCheckoutParams, type CreateBillingPortalParams, type BillingHostedSession, type BillingSubscription, } from './billing';
|
package/dist/types/merchant.d.ts
CHANGED
|
@@ -6,10 +6,29 @@ export interface Merchant {
|
|
|
6
6
|
object: 'merchant';
|
|
7
7
|
oxyAppId: string;
|
|
8
8
|
environment: MerchantEnvironment;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The FairCoin chain this merchant accepts on — `null` on a CARD-ONLY
|
|
11
|
+
* merchant.
|
|
12
|
+
*
|
|
13
|
+
* Nullable TOGETHER with `xpub`: they are two halves of one capability, and
|
|
14
|
+
* the gateway refuses a registration carrying only one. Registration used to
|
|
15
|
+
* demand both unconditionally, so a merchant who only wanted to take cards
|
|
16
|
+
* had to supply a watch-only key for a chain they never intended to use.
|
|
17
|
+
*/
|
|
18
|
+
network: NetworkType | null;
|
|
19
|
+
/** `null` on a card-only merchant. Nullable together with `network`. */
|
|
20
|
+
xpub: string | null;
|
|
11
21
|
webhookUrl?: string;
|
|
12
22
|
requiredConfirmations: number;
|
|
23
|
+
/**
|
|
24
|
+
* Payer-facing branding. Absent when unset — `MerchantDisplay` is what a
|
|
25
|
+
* payer surface renders, and it substitutes a neutral fallback; these are
|
|
26
|
+
* the raw values the merchant owns.
|
|
27
|
+
*/
|
|
28
|
+
displayName?: string;
|
|
29
|
+
/** Bare Oxy file id, not a URL. `MerchantDisplay.avatarUrl` resolves it. */
|
|
30
|
+
avatarFileId?: string;
|
|
31
|
+
description?: string;
|
|
13
32
|
createdAt: string;
|
|
14
33
|
updatedAt: string;
|
|
15
34
|
}
|
package/dist/types/money.d.ts
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The currencies this gateway can denominate a payment in.
|
|
3
|
+
*
|
|
4
|
+
* A closed set, and the database mirrors it as a CHECK
|
|
5
|
+
* (`db/schema/valueSets.ts`). ADR 0001 D4: the scale of an amount is a property
|
|
6
|
+
* of its currency and is NOT implied by the field's type, which is why
|
|
7
|
+
* {@link CURRENCY_DECIMALS} exists and why {@link UNITS_PER_COIN} must never be
|
|
8
|
+
* used to interpret a fiat amount.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CURRENCY_CODES: readonly ["FAIR", "EUR", "USD"];
|
|
11
|
+
export type CurrencyCode = (typeof CURRENCY_CODES)[number];
|
|
12
|
+
/** Whether an arbitrary string is a currency this gateway knows. */
|
|
13
|
+
export declare function isCurrencyCode(value: string): value is CurrencyCode;
|
|
14
|
+
/**
|
|
15
|
+
* How many decimal places each currency's smallest unit represents — i.e. an
|
|
16
|
+
* amount of `1` means `10^-decimals` of one whole unit.
|
|
17
|
+
*
|
|
18
|
+
* FairCoin's 8 is a consensus constant and is pinned against
|
|
19
|
+
* {@link UNITS_PER_COIN} by this package's own tests, so the two spellings of
|
|
20
|
+
* one fact cannot drift.
|
|
21
|
+
*/
|
|
22
|
+
export declare const CURRENCY_DECIMALS: Readonly<Record<CurrencyCode, number>>;
|
|
23
|
+
/** The decimal places of `currency`'s smallest unit. */
|
|
24
|
+
export declare function decimalsFor(currency: CurrencyCode): number;
|
|
1
25
|
export declare const UNITS_PER_COIN = 100000000n;
|
|
2
26
|
/**
|
|
3
|
-
* True only for a canonical non-negative integer string in
|
|
4
|
-
* one or more decimal digits, no sign, no decimal point, no
|
|
5
|
-
* (except the literal "0"). Floats and negatives are rejected.
|
|
27
|
+
* True only for a canonical non-negative integer string in the currency's
|
|
28
|
+
* smallest unit: one or more decimal digits, no sign, no decimal point, no
|
|
29
|
+
* leading zeros (except the literal "0"). Floats and negatives are rejected.
|
|
30
|
+
*
|
|
31
|
+
* Currency-agnostic on purpose — the pattern is about the SPELLING of an
|
|
32
|
+
* amount, not its scale, and the same one is restated as a CHECK in the
|
|
33
|
+
* database (`BASE_UNIT_STRING_PATTERN`), byte-identical.
|
|
6
34
|
*/
|
|
7
35
|
export declare function isBaseUnitString(s: string): boolean;
|