@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.
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BILLING_SUBSCRIPTION_STATUSES = void 0;
4
+ /** Recurring billing transport. Store/plan references belong to the authenticated app;
5
+ * they do not identify an Oxy payer or convey entitlements. */
6
+ exports.BILLING_SUBSCRIPTION_STATUSES = ['incomplete', 'incomplete_expired', 'trialing', 'active', 'past_due', 'canceled', 'unpaid', 'paused'];
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ /**
3
+ * A dispute — the card network holding a settled payment's money while a
4
+ * cardholder contests it.
5
+ *
6
+ * Published because `payment_intent.disputed` and `payment_intent.dispute_closed`
7
+ * carry it as their `data.object`. It was route-local at first, with the webhook
8
+ * carrying the PaymentIntent and the detail reachable only through
9
+ * `GET /v1/payment_intents/:id/disputes`. That was inconsistent with the reason
10
+ * those two events exist: `disputed` is a DEADLINE, and a merchant who has to
11
+ * make a second call to learn when evidence is due is a merchant who can miss it
12
+ * while their integration is working exactly as documented.
13
+ *
14
+ * It is also what the provider does. Stripe's `charge.dispute.created` carries
15
+ * the dispute, not the charge — the parity was half-applied.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/cjs/index.js CHANGED
@@ -1,15 +1,30 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MERCHANT_ENVIRONMENTS = exports.verifyWebhook = exports.signWebhook = exports.PAYMENT_INTENT_STATUSES = exports.isValidStatusTransition = exports.isBaseUnitString = exports.UNITS_PER_COIN = void 0;
3
+ exports.BILLING_SUBSCRIPTION_STATUSES = exports.SOCIAL_SOURCE_REF_MAX_LENGTH = exports.SOCIAL_SOURCE_APP_MAX_LENGTH = exports.MERCHANT_ENVIRONMENTS = exports.verifyWebhook = exports.signWebhook = exports.CARD_ONLY_STATUSES = exports.CHAIN_ONLY_STATUSES = exports.PAYMENT_INTENT_RAILS = exports.PAYMENT_INTENT_STATUSES = exports.POST_SETTLEMENT_STATUSES = exports.reachesSettlementFromPayer = exports.canStillBePaid = exports.isValidStatusTransition = 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; } });
7
7
  Object.defineProperty(exports, "isBaseUnitString", { enumerable: true, get: function () { return money_1.isBaseUnitString; } });
8
+ Object.defineProperty(exports, "CURRENCY_CODES", { enumerable: true, get: function () { return money_1.CURRENCY_CODES; } });
9
+ Object.defineProperty(exports, "CURRENCY_DECIMALS", { enumerable: true, get: function () { return money_1.CURRENCY_DECIMALS; } });
10
+ Object.defineProperty(exports, "decimalsFor", { enumerable: true, get: function () { return money_1.decimalsFor; } });
11
+ Object.defineProperty(exports, "isCurrencyCode", { enumerable: true, get: function () { return money_1.isCurrencyCode; } });
8
12
  var paymentIntent_1 = require("./paymentIntent");
9
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; } });
10
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
+ Object.defineProperty(exports, "CARD_ONLY_STATUSES", { enumerable: true, get: function () { return paymentIntent_1.CARD_ONLY_STATUSES; } });
11
21
  var webhookSigner_1 = require("./webhookSigner");
12
22
  Object.defineProperty(exports, "signWebhook", { enumerable: true, get: function () { return webhookSigner_1.signWebhook; } });
13
23
  Object.defineProperty(exports, "verifyWebhook", { enumerable: true, get: function () { return webhookSigner_1.verifyWebhook; } });
14
24
  var merchant_1 = require("./merchant");
15
25
  Object.defineProperty(exports, "MERCHANT_ENVIRONMENTS", { enumerable: true, get: function () { return merchant_1.MERCHANT_ENVIRONMENTS; } });
26
+ var social_1 = require("./social");
27
+ Object.defineProperty(exports, "SOCIAL_SOURCE_APP_MAX_LENGTH", { enumerable: true, get: function () { return social_1.SOCIAL_SOURCE_APP_MAX_LENGTH; } });
28
+ Object.defineProperty(exports, "SOCIAL_SOURCE_REF_MAX_LENGTH", { enumerable: true, get: function () { return social_1.SOCIAL_SOURCE_REF_MAX_LENGTH; } });
29
+ var billing_1 = require("./billing");
30
+ Object.defineProperty(exports, "BILLING_SUBSCRIPTION_STATUSES", { enumerable: true, get: function () { return billing_1.BILLING_SUBSCRIPTION_STATUSES; } });
package/dist/cjs/money.js CHANGED
@@ -1,20 +1,60 @@
1
1
  "use strict";
2
- // Amount helpers over FairCoin base units (m⊜). One coin is UNITS_PER_COIN
3
- // base units. Amounts move through the system as bigint conceptually; the
4
- // PaymentIntent DTO stores them as a base-unit integer string so no float ever
5
- // touches a monetary value.
2
+ // Amount helpers. Every monetary value moves through this gateway as a
3
+ // canonical integer string in the currency's SMALLEST unit — m⊜ for FairCoin,
4
+ // cents for a fiat currency — so no float ever touches one.
6
5
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.UNITS_PER_COIN = void 0;
6
+ exports.UNITS_PER_COIN = exports.CURRENCY_DECIMALS = exports.CURRENCY_CODES = void 0;
7
+ exports.isCurrencyCode = isCurrencyCode;
8
+ exports.decimalsFor = decimalsFor;
8
9
  exports.isBaseUnitString = isBaseUnitString;
10
+ /**
11
+ * The currencies this gateway can denominate a payment in.
12
+ *
13
+ * A closed set, and the database mirrors it as a CHECK
14
+ * (`db/schema/valueSets.ts`). ADR 0001 D4: the scale of an amount is a property
15
+ * of its currency and is NOT implied by the field's type, which is why
16
+ * {@link CURRENCY_DECIMALS} exists and why {@link UNITS_PER_COIN} must never be
17
+ * used to interpret a fiat amount.
18
+ */
19
+ exports.CURRENCY_CODES = ['FAIR', 'EUR', 'USD'];
20
+ /** Whether an arbitrary string is a currency this gateway knows. */
21
+ function isCurrencyCode(value) {
22
+ return exports.CURRENCY_CODES.includes(value);
23
+ }
24
+ /**
25
+ * How many decimal places each currency's smallest unit represents — i.e. an
26
+ * amount of `1` means `10^-decimals` of one whole unit.
27
+ *
28
+ * FairCoin's 8 is a consensus constant and is pinned against
29
+ * {@link UNITS_PER_COIN} by this package's own tests, so the two spellings of
30
+ * one fact cannot drift.
31
+ */
32
+ exports.CURRENCY_DECIMALS = Object.freeze({
33
+ FAIR: 8,
34
+ EUR: 2,
35
+ USD: 2,
36
+ });
37
+ /** The decimal places of `currency`'s smallest unit. */
38
+ function decimalsFor(currency) {
39
+ return exports.CURRENCY_DECIMALS[currency];
40
+ }
9
41
  // Frozen FairCoin consensus constant (smallest units per FAIR). Mirrors
10
42
  // @fairco.in/core's branding.ts; inlined here (rather than imported) to keep
11
43
  // @peable.to/shared-types zero-runtime-dep for a clean CJS+ESM publish — must
12
44
  // stay a bigint, never converted to number.
45
+ //
46
+ // FAIRCOIN ONLY. A fiat amount has its own scale (`CURRENCY_DECIMALS`), and
47
+ // dividing a EUR amount by this constant is off by six orders of magnitude with
48
+ // nothing failing.
13
49
  exports.UNITS_PER_COIN = 100000000n;
14
50
  /**
15
- * True only for a canonical non-negative integer string in base units:
16
- * one or more decimal digits, no sign, no decimal point, no leading zeros
17
- * (except the literal "0"). Floats and negatives are rejected.
51
+ * True only for a canonical non-negative integer string in the currency's
52
+ * smallest unit: one or more decimal digits, no sign, no decimal point, no
53
+ * leading zeros (except the literal "0"). Floats and negatives are rejected.
54
+ *
55
+ * Currency-agnostic on purpose — the pattern is about the SPELLING of an
56
+ * amount, not its scale, and the same one is restated as a CHECK in the
57
+ * database (`BASE_UNIT_STRING_PATTERN`), byte-identical.
18
58
  */
19
59
  function isBaseUnitString(s) {
20
60
  return /^(0|[1-9][0-9]*)$/.test(s);
@@ -1,26 +1,117 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PAYMENT_INTENT_STATUSES = void 0;
3
+ exports.POST_SETTLEMENT_STATUSES = exports.PAYMENT_INTENT_STATUSES = exports.CARD_ONLY_STATUSES = exports.CHAIN_ONLY_STATUSES = exports.PAYMENT_INTENT_RAILS = void 0;
4
4
  exports.isValidStatusTransition = isValidStatusTransition;
5
+ exports.canStillBePaid = canStillBePaid;
6
+ exports.reachesSettlementFromPayer = reachesSettlementFromPayer;
7
+ /** {@link PaymentIntentRail} as the tuple the database CHECK reads. */
8
+ exports.PAYMENT_INTENT_RAILS = ['faircoin', 'card'];
9
+ /**
10
+ * Which statuses belong to which rail (ADR 0001 D5).
11
+ *
12
+ * The four chain states describe a transaction on a blockchain and the four
13
+ * card states describe an authorization at a card acquirer; neither set is
14
+ * expressible on the other rail. Mapping a card authorization onto `broadcast`
15
+ * would be cheaper and would be a lie — nothing was broadcast, there is no
16
+ * transaction, and `payment_intents_broadcast_requires_txid_check` would then
17
+ * be demanding a txid from a payment that can never have one.
18
+ *
19
+ * `db/schema/payments.ts` renders these two sets into CHECK constraints, so the
20
+ * restriction holds against a write that skipped the application.
21
+ */
22
+ exports.CHAIN_ONLY_STATUSES = [
23
+ 'awaiting_approval',
24
+ 'approved',
25
+ 'broadcast',
26
+ 'confirming',
27
+ ];
28
+ exports.CARD_ONLY_STATUSES = [
29
+ 'requires_action',
30
+ 'processing',
31
+ 'refunded',
32
+ 'partially_refunded',
33
+ ];
5
34
  /**
6
35
  * Legal forward transitions of the PaymentIntent lifecycle. Terminal states
7
- * (settled/expired/failed/rejected) allow no further transition here — the one
36
+ * (refunded/expired/failed/rejected) allow no further transition here — the one
8
37
  * documented exception, a reorg dropping `settled` back to `confirming`, is
9
38
  * modelled by the backend state machine, not this happy-path table.
39
+ *
40
+ * ONE table for both rails, deliberately. It answers "is this a legal lifecycle
41
+ * edge at all"; WHICH rail may reach a given status is the separate, structural
42
+ * question the two sets above answer, in the database. A rail-parameterised
43
+ * table would put the same restriction in two places and let them disagree.
44
+ *
45
+ * Self-transitions are absent because `applyEvent` short-circuits on
46
+ * `current === target` before consulting this table — that is the idempotent
47
+ * re-poll of the settlement watcher, and a second partial refund, which change
48
+ * an amount without changing a status.
10
49
  */
11
50
  const ALLOWED = {
12
- // `created` supports two paths: the rich wallet flow (→ awaiting_approval →
13
- // approved → broadcast) AND the minimal path where the payer pays out-of-band
14
- // and reports the txid directly (→ broadcast), or the merchant cancels an
15
- // unpaid intent (→ rejected). Both are legal forward transitions.
16
- created: ['awaiting_approval', 'broadcast', 'rejected', 'expired'],
51
+ // `created` supports three paths: the rich wallet flow (→ awaiting_approval →
52
+ // approved → broadcast), the minimal path where the payer pays out-of-band
53
+ // and reports the txid directly (→ broadcast), and the card path (→
54
+ // requires_action for an SCA challenge, → processing, or straight to settled
55
+ // for a charge that confirms in one call). `failed` is reachable from here
56
+ // because a card can be declined on first confirmation, having never been
57
+ // anything else; no FairCoin path uses that edge, since `underpaid` is only
58
+ // emitted for an intent that already carries a txid.
59
+ created: [
60
+ 'awaiting_approval',
61
+ 'broadcast',
62
+ 'requires_action',
63
+ 'processing',
64
+ 'settled',
65
+ 'failed',
66
+ 'rejected',
67
+ 'expired',
68
+ ],
17
69
  awaiting_approval: ['approved', 'rejected', 'expired'],
18
70
  approved: ['broadcast', 'failed'],
19
71
  broadcast: ['confirming', 'failed'],
20
72
  confirming: ['settled', 'failed'],
21
- settled: [],
73
+ requires_action: ['processing', 'settled', 'failed', 'expired', 'rejected'],
74
+ processing: ['settled', 'failed'],
75
+ /**
76
+ * A failed ATTEMPT is not a failed payment, on the card rail.
77
+ *
78
+ * `payment_intent.payment_failed` means one authorization attempt was
79
+ * declined; Stripe returns the PaymentIntent to `requires_payment_method` and
80
+ * the payer can confirm it again with another card. With `failed` terminal,
81
+ * the later `payment_intent.succeeded` was an ILLEGAL transition — so the
82
+ * drain recorded a processing failure and retried it forever, the payment
83
+ * stayed `failed`, and a merchant who had been paid was told their payment
84
+ * had been declined.
85
+ *
86
+ * On the chain rail `failed` means `underpaid`, which IS terminal — coins
87
+ * arrived and were not enough. Nothing here opens that door: `underpaid` is
88
+ * emitted only by the settlement watcher, and `LEGAL_SOURCES` in
89
+ * `services/intentState.ts` keeps every `card_*` event to card statuses and
90
+ * the chain's `confirmed` to `confirming`. This table answers "is the edge
91
+ * legal at all"; which rail may walk it is the separate question those two
92
+ * guards answer.
93
+ */
94
+ failed: ['requires_action', 'processing', 'settled'],
95
+ // Money coming back requires money to have arrived, so both refund states are
96
+ // reachable only from `settled`.
97
+ settled: ['refunded', 'partially_refunded'],
98
+ /**
99
+ * ...and money coming back can be UNDONE, which is why these edges run both
100
+ * ways.
101
+ *
102
+ * A refund is not final when the provider accepts it: a bank can reject one
103
+ * days later, and the provider then says so (`refund.failed`). Without a path
104
+ * back, a payment whose only refund failed would claim `refunded` forever —
105
+ * the merchant's books saying money went back that is still with them, and no
106
+ * transition anywhere able to correct it.
107
+ *
108
+ * The target is always recomputed from the SUM of succeeded refunds
109
+ * (`applyRefundToIntent`), never stepped, so these edges describe where that
110
+ * sum can legally land rather than a sequence anyone walks.
111
+ */
112
+ partially_refunded: ['refunded', 'settled'],
113
+ refunded: ['partially_refunded', 'settled'],
22
114
  expired: [],
23
- failed: [],
24
115
  rejected: [],
25
116
  };
26
117
  function isValidStatusTransition(from, to) {
@@ -29,3 +120,88 @@ function isValidStatusTransition(from, to) {
29
120
  /** Every legal `PaymentIntentStatus`, derived from `ALLOWED`'s keys — the
30
121
  * single source of truth consumed by the Gateway's list-route status filter. */
31
122
  exports.PAYMENT_INTENT_STATUSES = Object.keys(ALLOWED);
123
+ /**
124
+ * The statuses a payer can still complete a payment FROM.
125
+ *
126
+ * **Twice now this question has been answered by walking the transition table,
127
+ * and twice the table has moved underneath it.**
128
+ *
129
+ * The hosted checkout first asked leaf-ness — "has this status no outgoing
130
+ * edges" — which meant `settled` while `settled` had none. Adding the refund
131
+ * transitions (`settled → refunded | partially_refunded`) gave it edges, so a
132
+ * settled payment stopped looking finished, and the checkout began REUSING a
133
+ * remembered settled intent instead of minting a fresh one: a payer who had
134
+ * already paid saw their old receipt forever, with no way to pay the link
135
+ * again.
136
+ *
137
+ * The fix was reachability — "is `settled` still reachable" — and that held
138
+ * until refunds could be UNDONE. `refunded → settled` (a bank rejecting a
139
+ * refund days later) makes `settled` reachable from `settled` in two steps, so
140
+ * reachability answers `true` for a payment that is finished, and the original
141
+ * bug returns unchanged.
142
+ *
143
+ * So the set is stated, not derived. It is exactly the statuses in which the
144
+ * gateway is still waiting on the PAYER — which is what the question means, in
145
+ * the payer's own terms, and is the one formulation no edge added elsewhere can
146
+ * silently change. `paymentIntent.test.ts` pins it against the table: every
147
+ * member must still be able to reach `settled` without passing through a
148
+ * post-settlement status, and no non-member may.
149
+ */
150
+ const PAYABLE_STATUSES = new Set([
151
+ 'created',
152
+ 'awaiting_approval',
153
+ 'approved',
154
+ 'broadcast',
155
+ 'confirming',
156
+ 'requires_action',
157
+ 'processing',
158
+ ]);
159
+ /**
160
+ * Whether a payer can still complete a payment against an intent in this
161
+ * status.
162
+ *
163
+ * `settled` answers `false`: it is where paying ENDS. `partially_refunded`
164
+ * answers `false` too — money can still move, but not from the payer.
165
+ */
166
+ function canStillBePaid(status) {
167
+ return PAYABLE_STATUSES.has(status);
168
+ }
169
+ /**
170
+ * The statuses a payment reaches only AFTER the payer has finished.
171
+ *
172
+ * Exported for the gate below rather than for production use: the property that
173
+ * makes `PAYABLE_STATUSES` checkable is that a payable status reaches `settled`
174
+ * without going through one of these.
175
+ */
176
+ exports.POST_SETTLEMENT_STATUSES = [
177
+ 'settled',
178
+ 'refunded',
179
+ 'partially_refunded',
180
+ ];
181
+ /**
182
+ * Whether `settled` is reachable from `status` without passing through a
183
+ * post-settlement status. The derivation `canStillBePaid` used to BE, kept as
184
+ * the thing that checks it rather than as the thing that answers it.
185
+ */
186
+ function reachesSettlementFromPayer(status) {
187
+ const blocked = new Set(exports.POST_SETTLEMENT_STATUSES);
188
+ // A payment that has ALREADY settled is one the payer has finished with,
189
+ // whatever edges lead out of it. Checked before the walk rather than inside
190
+ // it, because `partially_refunded → settled` is a direct edge and the walk
191
+ // would find it in one step — and that edge is a refund being undone, not a
192
+ // payer paying.
193
+ if (blocked.has(status))
194
+ return false;
195
+ const seen = new Set();
196
+ const queue = [...ALLOWED[status]];
197
+ while (queue.length > 0) {
198
+ const next = queue.shift();
199
+ if (next === 'settled')
200
+ return true;
201
+ if (seen.has(next) || blocked.has(next))
202
+ continue;
203
+ seen.add(next);
204
+ queue.push(...ALLOWED[next]);
205
+ }
206
+ return false;
207
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -3,3 +3,22 @@
3
3
  // Gateway backend, the wallet frontend, and (indirectly) the enrichment
4
4
  // service's callers. Mirrors the Stripe-parity style of paymentIntent.ts.
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SOCIAL_SOURCE_REF_MAX_LENGTH = exports.SOCIAL_SOURCE_APP_MAX_LENGTH = void 0;
7
+ /**
8
+ * Longest `SocialPaymentSource.app` the gateway accepts. An app slug, not a
9
+ * label — 32 characters is more than any Oxy app id needs and short enough that
10
+ * the column can never become a place to put prose.
11
+ *
12
+ * Published with the contract because it is what a request is REFUSED for: a
13
+ * caller that cannot read the bound from the types finds it out with a 422.
14
+ */
15
+ exports.SOCIAL_SOURCE_APP_MAX_LENGTH = 32;
16
+ /**
17
+ * Longest `SocialPaymentSource.ref` the gateway accepts.
18
+ *
19
+ * The bound is the whole defence here, because the value is opaque: Peable
20
+ * never parses, resolves or links a `ref`, so nothing downstream would notice
21
+ * it growing. 128 characters holds any id an app mints (a uuid, a snowflake, a
22
+ * base64 hash) and refuses a payload.
23
+ */
24
+ exports.SOCIAL_SOURCE_REF_MAX_LENGTH = 128;
@@ -0,0 +1,3 @@
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 const BILLING_SUBSCRIPTION_STATUSES = ['incomplete', 'incomplete_expired', 'trialing', 'active', 'past_due', 'canceled', 'unpaid', 'paused'];
@@ -0,0 +1,16 @@
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
+ export {};
package/dist/esm/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // @peable.to/shared-types — public API for the Peable Gateway contract.
2
- export { UNITS_PER_COIN, isBaseUnitString } from './money.js';
3
- export { isValidStatusTransition, PAYMENT_INTENT_STATUSES, } from './paymentIntent.js';
2
+ export { UNITS_PER_COIN, isBaseUnitString, CURRENCY_CODES, CURRENCY_DECIMALS, decimalsFor, isCurrencyCode, } from './money.js';
3
+ export { isValidStatusTransition, canStillBePaid, reachesSettlementFromPayer, POST_SETTLEMENT_STATUSES, PAYMENT_INTENT_STATUSES, PAYMENT_INTENT_RAILS, CHAIN_ONLY_STATUSES, CARD_ONLY_STATUSES, } from './paymentIntent.js';
4
4
  export { signWebhook, verifyWebhook } from './webhookSigner.js';
5
5
  export { MERCHANT_ENVIRONMENTS, } from './merchant.js';
6
+ export { SOCIAL_SOURCE_APP_MAX_LENGTH, SOCIAL_SOURCE_REF_MAX_LENGTH, } from './social.js';
7
+ export { BILLING_SUBSCRIPTION_STATUSES, } from './billing.js';
package/dist/esm/money.js CHANGED
@@ -1,16 +1,54 @@
1
- // Amount helpers over FairCoin base units (m⊜). One coin is UNITS_PER_COIN
2
- // base units. Amounts move through the system as bigint conceptually; the
3
- // PaymentIntent DTO stores them as a base-unit integer string so no float ever
4
- // touches a monetary value.
1
+ // Amount helpers. Every monetary value moves through this gateway as a
2
+ // canonical integer string in the currency's SMALLEST unit — m⊜ for FairCoin,
3
+ // cents for a fiat currency — so no float ever touches one.
4
+ /**
5
+ * The currencies this gateway can denominate a payment in.
6
+ *
7
+ * A closed set, and the database mirrors it as a CHECK
8
+ * (`db/schema/valueSets.ts`). ADR 0001 D4: the scale of an amount is a property
9
+ * of its currency and is NOT implied by the field's type, which is why
10
+ * {@link CURRENCY_DECIMALS} exists and why {@link UNITS_PER_COIN} must never be
11
+ * used to interpret a fiat amount.
12
+ */
13
+ export const CURRENCY_CODES = ['FAIR', 'EUR', 'USD'];
14
+ /** Whether an arbitrary string is a currency this gateway knows. */
15
+ export function isCurrencyCode(value) {
16
+ return CURRENCY_CODES.includes(value);
17
+ }
18
+ /**
19
+ * How many decimal places each currency's smallest unit represents — i.e. an
20
+ * amount of `1` means `10^-decimals` of one whole unit.
21
+ *
22
+ * FairCoin's 8 is a consensus constant and is pinned against
23
+ * {@link UNITS_PER_COIN} by this package's own tests, so the two spellings of
24
+ * one fact cannot drift.
25
+ */
26
+ export const CURRENCY_DECIMALS = Object.freeze({
27
+ FAIR: 8,
28
+ EUR: 2,
29
+ USD: 2,
30
+ });
31
+ /** The decimal places of `currency`'s smallest unit. */
32
+ export function decimalsFor(currency) {
33
+ return CURRENCY_DECIMALS[currency];
34
+ }
5
35
  // Frozen FairCoin consensus constant (smallest units per FAIR). Mirrors
6
36
  // @fairco.in/core's branding.ts; inlined here (rather than imported) to keep
7
37
  // @peable.to/shared-types zero-runtime-dep for a clean CJS+ESM publish — must
8
38
  // stay a bigint, never converted to number.
39
+ //
40
+ // FAIRCOIN ONLY. A fiat amount has its own scale (`CURRENCY_DECIMALS`), and
41
+ // dividing a EUR amount by this constant is off by six orders of magnitude with
42
+ // nothing failing.
9
43
  export const UNITS_PER_COIN = 100000000n;
10
44
  /**
11
- * True only for a canonical non-negative integer string in base units:
12
- * one or more decimal digits, no sign, no decimal point, no leading zeros
13
- * (except the literal "0"). Floats and negatives are rejected.
45
+ * True only for a canonical non-negative integer string in the currency's
46
+ * smallest unit: one or more decimal digits, no sign, no decimal point, no
47
+ * leading zeros (except the literal "0"). Floats and negatives are rejected.
48
+ *
49
+ * Currency-agnostic on purpose — the pattern is about the SPELLING of an
50
+ * amount, not its scale, and the same one is restated as a CHECK in the
51
+ * database (`BASE_UNIT_STRING_PATTERN`), byte-identical.
14
52
  */
15
53
  export function isBaseUnitString(s) {
16
54
  return /^(0|[1-9][0-9]*)$/.test(s);
@@ -0,0 +1 @@
1
+ {"type":"module"}