@peable.to/shared-types 0.2.0 → 0.3.1-oxy-one.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.
@@ -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
- * (settled/expired/failed/rejected) allow no further transition here — the one
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 two paths: the rich wallet flow (→ awaiting_approval →
9
- // approved → broadcast) AND the minimal path where the payer pays out-of-band
10
- // and reports the txid directly (→ broadcast), or the merchant cancels an
11
- // unpaid intent (→ rejected). Both are legal forward transitions.
12
- created: ['awaiting_approval', 'broadcast', 'rejected', 'expired'],
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
- settled: [],
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 {};
@@ -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
- export {};
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,86 @@
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
+ /** Discovery only; invoice must still be read and proven paid. */
33
+ latestInvoiceId?: string | null;
34
+ providerCustomerId: string;
35
+ providerPriceId: string;
36
+ storeId: string;
37
+ planId: string;
38
+ livemode: boolean;
39
+ status: BillingSubscriptionStatus;
40
+ interval: 'month' | 'year';
41
+ cancelAtPeriodEnd: boolean;
42
+ currentPeriodStart: string;
43
+ currentPeriodEnd: string;
44
+ trialEndsAt: string | null;
45
+ cancelAt: string | null;
46
+ cancelledAt: string | null;
47
+ }
48
+ /** Owned recurring checkout correlation; completing checkout is not paid evidence. */
49
+ export interface BillingCheckoutSession extends BillingHostedSession {
50
+ id: string;
51
+ }
52
+ export interface BillingCheckoutObservation {
53
+ id: string;
54
+ status: 'open' | 'complete' | 'expired';
55
+ storeId: string;
56
+ planId: string;
57
+ providerCustomerId: string;
58
+ providerPriceId: string;
59
+ subscription: BillingSubscription | null;
60
+ }
61
+ /** Authenticated authoritative read, one settled full non-prorated line only.
62
+ * Tax figures are provider observations, not a tax quote or seller configuration. */
63
+ export interface BillingInvoiceState extends BillingPaidInvoice {
64
+ chargeId: string;
65
+ amountRefunded: string;
66
+ state: 'paid' | 'fully_refunded' | 'partially_refunded';
67
+ }
68
+ export interface BillingPaidInvoice {
69
+ invoiceId: string;
70
+ lineId: string;
71
+ paymentIntentId: string;
72
+ providerSubscriptionId: string;
73
+ providerCustomerId: string;
74
+ providerPriceId: string;
75
+ storeId: string;
76
+ planId: string;
77
+ livemode: boolean;
78
+ currency: string;
79
+ amountPaid: string;
80
+ netAmount: string | null;
81
+ taxAmount: string | null;
82
+ periodStart: string;
83
+ periodEnd: string;
84
+ paidAt: string;
85
+ observedAt: string;
86
+ }
@@ -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
- network: NetworkType;
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
- network: NetworkType;
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
+ }
@@ -1,11 +1,124 @@
1
+ import type { Dispute } from './dispute';
1
2
  import type { PaymentIntent } from './paymentIntent';
2
- export type WebhookEventType = 'payment_intent.confirming' | 'payment_intent.settled' | 'payment_intent.failed' | 'payment_intent.rejected' | 'payment_intent.expired';
3
- export interface WebhookEvent<T = PaymentIntent> {
4
- id: string;
5
- object: 'event';
6
- type: WebhookEventType;
7
- created: string;
8
- data: {
9
- object: T;
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' | 'billing.observation.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
+ /** Wake-up only. Re-read the owner-bound SDK resource; this is never paid or grant authority. */
72
+ export interface BillingObservation {
73
+ object: 'billing_observation';
74
+ resourceKind: 'subscription' | 'invoice';
75
+ resourceId: string;
76
+ revision: number;
77
+ observedAt: string;
78
+ }
79
+ export interface WebhookEventPayload {
80
+ 'billing.observation.updated': BillingObservation;
81
+ 'payment_intent.confirming': PaymentIntent;
82
+ 'payment_intent.settled': PaymentIntent;
83
+ 'payment_intent.failed': PaymentIntent;
84
+ 'payment_intent.rejected': PaymentIntent;
85
+ 'payment_intent.expired': PaymentIntent;
86
+ 'payment_intent.refunded': PaymentIntent;
87
+ 'payment_intent.partially_refunded': PaymentIntent;
88
+ 'payment_intent.disputed': Dispute;
89
+ 'payment_intent.dispute_closed': Dispute;
90
+ 'connected_account.updated': ConnectedAccount;
11
91
  }
92
+ /**
93
+ * One delivered event.
94
+ *
95
+ * A DISCRIMINATED union over every event type, so `data.object` is narrowed by
96
+ * a switch on `type` with no cast:
97
+ *
98
+ * ```ts
99
+ * if (event.type === 'payment_intent.disputed') {
100
+ * event.data.object.evidenceDueAt; // Dispute, not PaymentIntent
101
+ * }
102
+ * ```
103
+ *
104
+ * That correlation is the whole point of building it this way rather than as
105
+ * `{ type: WebhookEventType; data: { object: PaymentIntent | Dispute } }`,
106
+ * which typechecks identically at the boundary and then lets a consumer read a
107
+ * `Dispute`'s fields off a settlement.
108
+ *
109
+ * The parameter narrows to a subset of event types — `WebhookEvent<'payment_intent.disputed'>`
110
+ * is the dispute event alone. It takes an event TYPE and not a payload type,
111
+ * because the payload is derivable from the event and the reverse is not: two
112
+ * event types share `Dispute`.
113
+ */
114
+ export type WebhookEvent<K extends WebhookEventType = WebhookEventType> = {
115
+ [T in K]: {
116
+ id: string;
117
+ object: 'event';
118
+ type: T;
119
+ created: string;
120
+ data: {
121
+ object: WebhookEventPayload[T];
122
+ };
123
+ };
124
+ }[K];
@@ -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 WebhookDelivery } from './webhookDelivery';
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, type BillingObservation, } 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 BillingCheckoutSession, type BillingCheckoutObservation, type BillingPaidInvoice, type BillingInvoiceState, type BillingCustomer, type EnsureBillingCustomerParams, type CreateBillingCheckoutParams, type CreateBillingPortalParams, type BillingHostedSession, type BillingSubscription, } from './billing';
@@ -6,10 +6,29 @@ export interface Merchant {
6
6
  object: 'merchant';
7
7
  oxyAppId: string;
8
8
  environment: MerchantEnvironment;
9
- network: NetworkType;
10
- xpub: string;
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
  }