@jskit-ai/payments-core 0.2.0 → 0.2.2

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  `contracts/conformance.json` is ordinary JSON, also exported as
4
4
  `@jskit-ai/payments-core/conformance.json`. Read the file as data; no Node process,
5
- JSKIT import, editor service or Genesis command is necessary to interpret it.
5
+ JSKIT import, editor service or project tool is necessary to interpret it.
6
6
  It accompanies the draft-07 configuration schema and the semantic contract.
7
7
  These are test examples, not a production workflow interpreter.
8
8
 
@@ -72,9 +72,9 @@ runtime. The application selects its billable model and schema; the fixture does
72
72
  not prescribe table names or copy JSKIT's storage internals into another framework.
73
73
 
74
74
  A Node CLI consumer can run the same semantic calls against its installed JSKIT
75
- services. Genesis may describe the project's test command, but these fixtures do
76
- not require Genesis to execute it. Vibe64 configuration must preserve these same
77
- values rather than inventing a different hosted payment format.
75
+ services. Project tooling may describe the project's test command, but these
76
+ fixtures do not require it to execute. A hosted editor's configuration must
77
+ preserve these same values rather than inventing a different hosted payment format.
78
78
 
79
79
  ## Evidence boundaries
80
80
 
@@ -249,7 +249,7 @@ An optional app-owned `inspectApplication` callback may supply `webhook`,
249
249
  `passed`, `failed`, `unknown` or `manual` and a safe explanation of at most 500
250
250
  characters. Record only observed checks. Never turn an unavailable check into
251
251
  success or claim provider approval from a fixture. This callback is a normal app
252
- function, not a Vibe64 API. Authorize any route/CLI wrapper exposing the service.
252
+ function, not a hosted API. Authorize any route/CLI wrapper exposing the service.
253
253
 
254
254
  Catalogue recovery requires a fresh `catalogue.preview()` and its `reviewId`:
255
255
  `catalogue.recover({reviewId, providerId})`. The adapter reads the candidate object;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/payments-core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Application-owned payment configuration, subscription access and transactional usage credits.",
6
6
  "exports": {
@@ -16,10 +16,11 @@
16
16
  "./server/paddle": "./src/server/paddle.js",
17
17
  "./server/catalogue": "./src/server/catalogue.js",
18
18
  "./server/readiness": "./src/server/readiness.js",
19
+ "./server/types": "./src/server/types.js",
19
20
  "./locales/en.json": "./locales/en.json"
20
21
  },
21
22
  "scripts": {
22
- "test": "node --test --test-concurrency=1"
23
+ "test": "jskit test"
23
24
  },
24
25
  "dependencies": {
25
26
  "@paddle/paddle-node-sdk": "3.10.0",
@@ -4,8 +4,15 @@ import { PaymentError, fingerprint } from "./errors.js";
4
4
  const priceFields = (plan) => ({ amount: plan.amount, currency: plan.currency, interval: plan.interval });
5
5
  const samePrice = (a, b) => fingerprint(priceFields(a)) === fingerprint(priceFields(b));
6
6
 
7
- // Trusted application service. Hosts must authorize catalogue management before
8
- // invoking it. No provider mutation happens while merely reading configuration.
7
+ /**
8
+ * Trusted application service. Hosts must authorize catalogue management before
9
+ * invoking it. No provider mutation happens while merely reading configuration.
10
+ * @param {object} dependencies
11
+ * @param {import("./types.js").PaymentStore} dependencies.store
12
+ * @param {import("./types.js").PaymentProviderAdapter} dependencies.adapter
13
+ * @param {import("./types.js").PaymentMerchantScope} dependencies.scope The merchant scope the catalogue belongs to.
14
+ * @param {import("./types.js").PaymentConfiguration} dependencies.configuration
15
+ */
9
16
  function createPaymentCatalogue({ store, adapter, scope, configuration }) {
10
17
  const plans = structuredClone(configuration.plans);
11
18
  const merchant = structuredClone(scope);
@@ -112,7 +119,11 @@ function createPaymentCatalogue({ store, adapter, scope, configuration }) {
112
119
  await finish(pending, { id: providerId });
113
120
  return { pending: false };
114
121
  }
115
- return Object.freeze({ preview, publish, recover });
122
+ return Object.freeze({
123
+ preview,
124
+ publish,
125
+ recover
126
+ });
116
127
  }
117
128
 
118
129
  export { createPaymentCatalogue };
@@ -1,9 +1,19 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { PaymentError, fingerprint } from "./errors.js";
3
3
 
4
- // Application composition boundary: authorization is mandatory and supplied by
5
- // the app's framework. Actor and subject must come from its authenticated route.
4
+ /**
5
+ * Application composition boundary: authorization is mandatory and supplied by
6
+ * the app's framework. Actor and subject must come from its authenticated route.
7
+ * @param {object} dependencies
8
+ * @param {import("./types.js").PaymentProviderAdapter} dependencies.adapter
9
+ * @param {import("./types.js").PaymentStore} dependencies.store
10
+ * @param {import("./types.js").PaymentService} dependencies.payments
11
+ * @param {import("./types.js").PaymentMerchantScope} dependencies.merchantScope
12
+ * @param {string} dependencies.returnUrl
13
+ * @param {(actor: unknown, request: { subjectId: string, action: string }) => boolean | Promise<boolean>} dependencies.authorize
14
+ */
6
15
  function createPaymentCheckoutService({ adapter, store, payments, merchantScope, returnUrl, authorize }) {
16
+ // readable-check: allow method-probe - input validation: the application passes its billable-subject authorization
7
17
  if (typeof authorize !== "function") throw new TypeError("Supply the application's billable-subject authorization function.");
8
18
  const target = new URL(returnUrl);
9
19
  if ((target.protocol !== "https:" && !(target.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(target.hostname))) || target.username || target.password || target.hash) {
@@ -94,6 +104,7 @@ function createPaymentCheckoutService({ adapter, store, payments, merchantScope,
94
104
  // provider dashboard/API and must return evidence, never browser-supplied data.
95
105
  async function reconcilePending({ actor, subjectId, inspectProvider }) {
96
106
  const scope = await allowed(actor, subjectId, "reconcile");
107
+ // readable-check: allow method-probe - input validation: the caller passes its server-side provider inspection
97
108
  if (typeof inspectProvider !== "function") throw new TypeError("Supply the server's provider inspection function.");
98
109
  return store.withAccount(scope, async (tx) => {
99
110
  const pending = tx.state.pendingPaymentOperation;
@@ -125,7 +136,14 @@ function createPaymentCheckoutService({ adapter, store, payments, merchantScope,
125
136
  return facts;
126
137
  } });
127
138
  }
128
- return Object.freeze({ account, history, checkout, portal, webhook, reconcilePending });
139
+ return Object.freeze({
140
+ account,
141
+ history,
142
+ checkout,
143
+ portal,
144
+ webhook,
145
+ reconcilePending
146
+ });
129
147
  }
130
148
 
131
149
  export { createPaymentCheckoutService };
@@ -16,8 +16,12 @@ function identifier(value) {
16
16
  }
17
17
  const accountKey = (scope) => fingerprint([...merchant(scope), identifier(scope.subjectId)]);
18
18
 
19
+ /** @param {{ knex: import("@jskit-ai/database-runtime/server/types").RuntimeKnex }} dependencies */
19
20
  function createKnexPaymentStore({ knex }) {
20
- if (typeof knex !== "function" || typeof knex.transaction !== "function") throw new TypeError("Supply the application's transactional Knex client.");
21
+ // readable-check: allow method-probe - input validation: the application passes its own Knex instance
22
+ if (typeof knex !== "function" || typeof knex.transaction !== "function") {
23
+ throw new TypeError("createKnexPaymentStore requires the application's Knex instance.");
24
+ }
21
25
 
22
26
  async function inspect(scope) {
23
27
  const row = await knex("payment_accounts").where({ account_key: accountKey(scope) }).first();
@@ -75,7 +79,14 @@ function createKnexPaymentStore({ knex }) {
75
79
  return result;
76
80
  });
77
81
  }
78
- return Object.freeze({ inspect, withAccount, bindCustomer, resolveCustomer, inspectCatalogue, withCatalogue });
82
+ return Object.freeze({
83
+ inspect,
84
+ withAccount,
85
+ bindCustomer,
86
+ resolveCustomer,
87
+ inspectCatalogue,
88
+ withCatalogue
89
+ });
79
90
  }
80
91
 
81
92
  export { createKnexPaymentStore };
@@ -8,6 +8,17 @@ const invalid = () => {
8
8
  });
9
9
  };
10
10
 
11
+ /**
12
+ * @param {object} options
13
+ * @param {string} [options.apiKey]
14
+ * @param {string} options.webhookSecret
15
+ * @param {"sandbox" | "live"} options.environment
16
+ * @param {Record<string, string>} options.priceBindings Each plan's current price id.
17
+ * @param {Record<string, string>} [options.historicalPriceBindings] The plan of each retired price id.
18
+ * @param {import("@paddle/paddle-node-sdk").TaxCategory} [options.taxCategory] The tax category of the products it publishes.
19
+ * @param {import("@paddle/paddle-node-sdk").Paddle} [options.client]
20
+ */
21
+ // readable-check: allow dependency-default - data: historicalPriceBindings maps retired price ids to plans, not a collaborator
11
22
  function createPaddlePaymentAdapter({ apiKey, webhookSecret, environment, priceBindings, taxCategory, historicalPriceBindings = {}, client = new Paddle(apiKey, { environment: environment === "live" ? Environment.production : Environment.sandbox }) }) {
12
23
  if (!["sandbox", "live"].includes(environment) || typeof webhookSecret !== "string" || !webhookSecret) throw new TypeError("Supply Paddle credentials and environment.");
13
24
  const { validatePlan, priceFor, planFor } = createPriceBindings({ priceBindings, historicalPriceBindings, invalid });
@@ -111,7 +122,22 @@ function createPaddlePaymentAdapter({ apiKey, webhookSecret, environment, priceB
111
122
  async function createPrice({ productId, amount, currency, interval }) {
112
123
  return client.prices.create({ productId, description: `${currency} ${amount} per ${interval}`, unitPrice: { amount: String(amount), currencyCode: currency }, billingCycle: { interval, frequency: 1 } });
113
124
  }
114
- return Object.freeze({ validatePlan, verifyAccount, verifyEvent, eventIdentity, loadEvent, createCustomer, createCheckout, createPortal, readHistory, readProduct, createProduct, renameProduct, readPrice, createPrice });
125
+ return Object.freeze({
126
+ validatePlan,
127
+ verifyAccount,
128
+ verifyEvent,
129
+ eventIdentity,
130
+ loadEvent,
131
+ createCustomer,
132
+ createCheckout,
133
+ createPortal,
134
+ readHistory,
135
+ readProduct,
136
+ createProduct,
137
+ renameProduct,
138
+ readPrice,
139
+ createPrice
140
+ });
115
141
  }
116
142
 
117
143
  export { createPaddlePaymentAdapter };
@@ -1,7 +1,13 @@
1
1
  import { PaymentError } from "./errors.js";
2
2
 
3
- // Plan-to-price bindings shared by the provider adapters. `invalid` throws the
4
- // adapter's own provider-result error for an unknown or ambiguous price.
3
+ /**
4
+ * Plan-to-price bindings shared by the provider adapters. `invalid` throws the
5
+ * adapter's own provider-result error for an unknown or ambiguous price.
6
+ * @param {object} bindings
7
+ * @param {Record<string, string>} bindings.priceBindings Each plan's current price id.
8
+ * @param {Record<string, string>} [bindings.historicalPriceBindings] The plan of each retired price id.
9
+ * @param {() => never} bindings.invalid
10
+ */
5
11
  function createPriceBindings({ priceBindings, historicalPriceBindings = {}, invalid }) {
6
12
  const prices = structuredClone(priceBindings);
7
13
  function validatePlan(planId) {
@@ -17,7 +23,11 @@ function createPriceBindings({ priceBindings, historicalPriceBindings = {}, inva
17
23
  if (matches.length !== 1) invalid();
18
24
  return matches[0][0];
19
25
  }
20
- return { validatePlan, priceFor, planFor };
26
+ return Object.freeze({
27
+ validatePlan,
28
+ priceFor,
29
+ planFor
30
+ });
21
31
  }
22
32
 
23
33
  export { createPriceBindings };
@@ -13,10 +13,19 @@ const DEFAULT_CHECKS = Object.freeze([
13
13
  ]);
14
14
  // Only these checks take their result from the application's own evidence.
15
15
  const APPLICATION_CHECKS = ["webhook", "checkout", "site", "deployment"];
16
+ // No application evidence: every application check keeps its default.
17
+ const inspectNoApplicationEvidence = async () => ({});
16
18
 
17
- // App-server inspection, invoked only after the host authorizes management.
18
- // Missing app evidence stays unknown; provider errors never become display text.
19
- function createPaymentReadiness({ adapter, catalogue, scope, inspectApplication }) {
19
+ /**
20
+ * App-server inspection, invoked only after the host authorizes management.
21
+ * Missing app evidence stays unknown; provider errors never become display text.
22
+ * @param {object} dependencies
23
+ * @param {import("./types.js").PaymentProviderAdapter} dependencies.adapter
24
+ * @param {import("./types.js").PaymentCatalogue} dependencies.catalogue
25
+ * @param {import("./types.js").PaymentMerchantScope} dependencies.scope The merchant scope the readiness checks belong to.
26
+ * @param {() => unknown} [dependencies.inspectApplication] The application's own evidence, by check id.
27
+ */
28
+ function createPaymentReadiness({ adapter, catalogue, scope, inspectApplication = inspectNoApplicationEvidence }) {
20
29
  const merchant = structuredClone(scope);
21
30
  if (!["sandbox", "live"].includes(merchant?.environment) || !merchant.providerAccountId) throw new TypeError("Supply the configured payment merchant scope.");
22
31
  async function inspect() {
@@ -50,18 +59,16 @@ function createPaymentReadiness({ adapter, catalogue, scope, inspectApplication
50
59
  unresolved ? "Catalogue changes, drift, removed plans or an unresolved write need review." : "Saved plans match their published provider products and prices.");
51
60
  } catch { set("catalogue", "failed", "Catalogue inspection failed. Review the application's catalogue command and provider access."); }
52
61
  }
53
- if (inspectApplication) {
54
- let evidence;
55
- try { evidence = await inspectApplication(); } catch { evidence = {}; }
56
- for (const id of APPLICATION_CHECKS) {
57
- const value = evidence?.[id];
58
- if (value === undefined) continue;
59
- if (!value || !["passed", "failed", "unknown", "manual"].includes(value.status) || typeof value.detail !== "string" ||
60
- !value.detail.trim() || value.detail.length > 500 || /[\p{Cc}\p{Cf}]/u.test(value.detail)) {
61
- throw new PaymentError("payment_readiness_invalid", "App readiness checks must return bounded status and safe explanatory text.");
62
- }
63
- set(id, value.status, value.detail);
62
+ let evidence;
63
+ try { evidence = await inspectApplication(); } catch { evidence = {}; }
64
+ for (const id of APPLICATION_CHECKS) {
65
+ const value = evidence?.[id];
66
+ if (value === undefined) continue;
67
+ if (!value || !["passed", "failed", "unknown", "manual"].includes(value.status) || typeof value.detail !== "string" ||
68
+ !value.detail.trim() || value.detail.length > 500 || /[\p{Cc}\p{Cf}]/u.test(value.detail)) {
69
+ throw new PaymentError("payment_readiness_invalid", "App readiness checks must return bounded status and safe explanatory text.");
64
70
  }
71
+ set(id, value.status, value.detail);
65
72
  }
66
73
  return { paymentEnvironment: merchant.environment, providerAccountId: merchant.providerAccountId, checks: [...checks.values()] };
67
74
  }
@@ -39,10 +39,19 @@ async function grant(tx, { reference: id, units: quantity, expiresAt }, now) {
39
39
  });
40
40
  }
41
41
 
42
- // Server-side domain API. The application authorizes the billable subject before
43
- // invoking it; none of these methods is an HTTP endpoint or a browser authority.
42
+ /**
43
+ * Server-side domain API. The application authorizes the billable subject before
44
+ * invoking it; none of these methods is an HTTP endpoint or a browser authority.
45
+ * @param {object} dependencies
46
+ * @param {import("./types.js").PaymentStore} dependencies.store
47
+ * @param {import("./types.js").PaymentConfiguration} dependencies.configuration
48
+ * @param {() => number} [dependencies.clock]
49
+ */
44
50
  function createPaymentService({ store, configuration, clock = Date.now }) {
45
- if (!store?.withAccount || !store?.inspect || !configuration?.plans) throw new TypeError("Supply payment storage and validated payment configuration.");
51
+ // readable-check: allow method-probe - input validation: the application passes its own payment store and validated configuration
52
+ if (typeof store?.withAccount !== "function" || typeof store?.inspect !== "function" || !configuration?.plans) {
53
+ throw new TypeError("Supply payment storage and validated payment configuration.");
54
+ }
46
55
  const plans = structuredClone(configuration.plans);
47
56
  const environments = structuredClone(configuration.environments);
48
57
  function checkScope(scope) {
@@ -122,6 +131,7 @@ function createPaymentService({ store, configuration, clock = Date.now }) {
122
131
  async function reconcileEvent(merchantScope, { eventId, customerId, load }) {
123
132
  checkScope(merchantScope);
124
133
  reference(eventId); reference(customerId);
134
+ // readable-check: allow method-probe - input validation: the caller passes its authenticated provider reconciliation
125
135
  if (typeof load !== "function") throw new TypeError("Supply an authenticated provider reconciliation function.");
126
136
  const subjectId = await store.resolveCustomer(merchantScope, customerId);
127
137
  if (!subjectId) fail("payment_customer_unbound", "Bind this provider customer before processing its events.", 409);
@@ -148,7 +158,14 @@ function createPaymentService({ store, configuration, clock = Date.now }) {
148
158
  }));
149
159
  }
150
160
 
151
- return Object.freeze({ inspect, requireFeature, grantCredits, debitCredits, refundDebit, reconcileEvent });
161
+ return Object.freeze({
162
+ inspect,
163
+ requireFeature,
164
+ grantCredits,
165
+ debitCredits,
166
+ refundDebit,
167
+ reconcileEvent
168
+ });
152
169
  }
153
170
 
154
171
  export { createPaymentService, PaymentError };
@@ -9,6 +9,17 @@ const invalid = () => {
9
9
  });
10
10
  };
11
11
 
12
+ /**
13
+ * @param {object} options
14
+ * @param {string} [options.apiKey]
15
+ * @param {string} options.webhookSecret
16
+ * @param {"sandbox" | "live"} options.environment
17
+ * @param {string} options.providerAccountId
18
+ * @param {Record<string, string>} options.priceBindings Each plan's current price id.
19
+ * @param {Record<string, string>} [options.historicalPriceBindings] The plan of each retired price id.
20
+ * @param {import("stripe").Stripe} [options.client]
21
+ */
22
+ // readable-check: allow dependency-default - data: historicalPriceBindings maps retired price ids to plans, not a collaborator
12
23
  function createStripePaymentAdapter({ apiKey, webhookSecret, environment, providerAccountId, priceBindings, historicalPriceBindings = {}, client = new Stripe(apiKey, { maxNetworkRetries: 0 }) }) {
13
24
  if (!["sandbox", "live"].includes(environment) || typeof webhookSecret !== "string" || !webhookSecret || !providerAccountId) throw new TypeError("Supply Stripe credentials, environment and account identity.");
14
25
  const live = environment === "live";
@@ -126,11 +137,27 @@ function createStripePaymentAdapter({ apiKey, webhookSecret, environment, provid
126
137
  if (value.recurring?.interval_count !== 1 || value.billing_scheme !== "per_unit") invalid();
127
138
  return { id: value.id, productId: id(value.product), amount: value.unit_amount, currency: value.currency.toUpperCase(), interval: value.recurring.interval, active: value.active };
128
139
  }
140
+ /** @param {{ productId: string, amount: number, currency: string, interval: string, requestId: string }} price */
129
141
  async function createPrice({ productId, amount, currency, interval, requestId }) {
130
142
  await verifyAccount();
131
143
  return client.prices.create({ product: productId, unit_amount: amount, currency: currency.toLowerCase(), recurring: { interval } }, { idempotencyKey: requestId });
132
144
  }
133
- return Object.freeze({ validatePlan, verifyAccount, verifyEvent, eventIdentity, loadEvent, createCustomer, createCheckout, createPortal, readHistory, readProduct, createProduct, renameProduct, readPrice, createPrice });
145
+ return Object.freeze({
146
+ validatePlan,
147
+ verifyAccount,
148
+ verifyEvent,
149
+ eventIdentity,
150
+ loadEvent,
151
+ createCustomer,
152
+ createCheckout,
153
+ createPortal,
154
+ readHistory,
155
+ readProduct,
156
+ createProduct,
157
+ renameProduct,
158
+ readPrice,
159
+ createPrice
160
+ });
134
161
  }
135
162
 
136
163
  export { createStripePaymentAdapter };
@@ -0,0 +1,105 @@
1
+ // The types of the payment objects an application composes, for code that receives them,
2
+ // and the contracts of the store and provider adapter it chooses.
3
+
4
+ /** @typedef {ReturnType<typeof import("./catalogue.js").createPaymentCatalogue>} PaymentCatalogue */
5
+ /** @typedef {ReturnType<typeof import("./checkout.js").createPaymentCheckoutService>} PaymentCheckoutService */
6
+ /** @typedef {ReturnType<typeof import("./readiness.js").createPaymentReadiness>} PaymentReadiness */
7
+ /** @typedef {ReturnType<typeof import("./service.js").createPaymentService>} PaymentService */
8
+ /** @typedef {ReturnType<typeof import("../shared/configuration.js").validatePaymentConfiguration>} PaymentConfiguration */
9
+
10
+ /**
11
+ * The merchant a payment record belongs to: one application's provider account
12
+ * in one environment.
13
+ * @typedef {object} PaymentMerchantScope
14
+ * @property {string} applicationId
15
+ * @property {string} integrationId
16
+ * @property {string} providerAccountId
17
+ * @property {"sandbox" | "live"} environment
18
+ */
19
+
20
+ /**
21
+ * One billable subject of a merchant.
22
+ * @typedef {PaymentMerchantScope & { subjectId: string }} PaymentAccountScope
23
+ */
24
+
25
+ /**
26
+ * One billable account's stored state.
27
+ * @typedef {object} PaymentAccountState
28
+ * @property {Array<{ id: string, remaining: number, expiresAt: number | null }>} lots The credit grants and what remains of each.
29
+ * @property {Record<string, { id: string, status: string, planId: string, periodEnd: number }>} subscriptions
30
+ * @property {string} [customerId] The provider customer bound to the account.
31
+ * @property {Record<string, any>} [pendingPaymentOperation] A provider request whose outcome is not recorded yet.
32
+ */
33
+
34
+ /**
35
+ * An account held under the store's lock while work runs: its state, which the
36
+ * work changes in place, and the entries recorded under business references.
37
+ * @typedef {object} PaymentAccountTransaction
38
+ * @property {PaymentAccountState} state
39
+ * @property {(reference: string) => Promise<any>} find The entry recorded under a reference, or null.
40
+ * @property {(reference: string, entry: any) => Promise<void>} record Records an entry under a new reference.
41
+ */
42
+
43
+ /**
44
+ * One merchant's published catalogue: each plan's provider product and price.
45
+ * @typedef {object} PaymentCatalogueState
46
+ * @property {Record<string, Record<string, any>>} plans
47
+ * @property {Array<Record<string, any>>} history The price bindings each plan had before its current one.
48
+ * @property {number} [revision]
49
+ * @property {Record<string, any>} [pending] A provider write whose outcome is not recorded yet.
50
+ */
51
+
52
+ /**
53
+ * Where payment accounts and catalogues are kept: createKnexPaymentStore() or
54
+ * the application's own store. Each `with*` call runs its work under the
55
+ * record's lock and saves the state the work leaves.
56
+ * @typedef {object} PaymentStore
57
+ * @property {(scope: PaymentAccountScope) => Promise<PaymentAccountState>} inspect
58
+ * @property {<T>(scope: PaymentAccountScope, work: (account: PaymentAccountTransaction) => Promise<T>) => Promise<T>} withAccount
59
+ * @property {(scope: PaymentMerchantScope, customerId: string, subjectId: string) => Promise<void>} bindCustomer
60
+ * Binds a provider customer to one billable subject, refusing a customer bound to another.
61
+ * @property {(scope: PaymentMerchantScope, customerId: string) => Promise<string | null>} resolveCustomer The subject a provider customer is bound to.
62
+ * @property {(scope: PaymentMerchantScope) => Promise<PaymentCatalogueState>} inspectCatalogue
63
+ * @property {<T>(scope: PaymentMerchantScope, work: (state: PaymentCatalogueState) => Promise<T>) => Promise<T>} withCatalogue
64
+ */
65
+
66
+ /**
67
+ * A provider's billing page: its id and the URL the customer opens.
68
+ * @typedef {{ id: string, url: string }} PaymentProviderPage
69
+ */
70
+
71
+ /**
72
+ * A price as the provider holds it.
73
+ * @typedef {object} PaymentProviderPrice
74
+ * @property {string} id
75
+ * @property {string} productId
76
+ * @property {number} amount In the currency's minor unit.
77
+ * @property {string} currency
78
+ * @property {string} interval
79
+ * @property {boolean} active
80
+ */
81
+
82
+ /**
83
+ * A payment provider's operations: createStripePaymentAdapter() or
84
+ * createPaddlePaymentAdapter().
85
+ * @typedef {object} PaymentProviderAdapter
86
+ * @property {(planId: string) => void} validatePlan Throws for a plan with no current price binding.
87
+ * @property {() => Promise<{ environment: string, accountId?: string, credentialsVerified?: boolean, chargesEnabled?: boolean, payoutsEnabled?: boolean }>} verifyAccount
88
+ * What the provider confirms about the configured account.
89
+ * @property {(rawBody: Buffer, signature: string) => Promise<any>} verifyEvent The webhook event, once its signature is verified.
90
+ * @property {(event: any) => { eventId: string, customerId: string } | null} eventIdentity The event and customer of a billing event, or null for an event to ignore.
91
+ * @property {(event: any) => Promise<{ customerId: string, subscription: Record<string, any>, renewal?: Record<string, any> } | null>} loadEvent
92
+ * The provider's current subscription facts for the event.
93
+ * @property {(request: { email: string, requestId: string }) => Promise<string>} createCustomer The new customer's id.
94
+ * @property {(request: { customerId: string, planId: string, returnUrl: string, requestId: string }) => Promise<PaymentProviderPage>} createCheckout
95
+ * @property {(request: { customerId: string, returnUrl: string }) => Promise<PaymentProviderPage>} createPortal
96
+ * @property {(request: { customerId: string, collection: string, after?: string | null }) => Promise<{ collection: string, items: Array<Record<string, any>>, nextCursor: string | null }>} readHistory
97
+ * One page of the customer's subscriptions or transactions.
98
+ * @property {(productId: string) => Promise<{ id: string, name: string }>} readProduct
99
+ * @property {(request: { name: string, requestId: string }) => Promise<{ id: string }>} createProduct
100
+ * @property {(request: { id: string, name: string, requestId: string }) => Promise<{ id: string }>} renameProduct
101
+ * @property {(priceId: string) => Promise<PaymentProviderPrice>} readPrice
102
+ * @property {(request: { productId: string, amount: number, currency: string, interval: string, requestId: string }) => Promise<{ id: string }>} createPrice
103
+ */
104
+
105
+ export {};
@@ -3,17 +3,31 @@ import schema from "../../contracts/configuration.schema.json" with { type: "jso
3
3
 
4
4
  const validate = new Ajv({ allErrors: true, strict: true }).compile(schema);
5
5
 
6
- // Each JSKIT field error also has a code and params; the error's messageCode names its sentence.
7
- function configurationError(message, fieldErrors, { messageCode, messageParams = {} }) {
8
- return Object.assign(new Error(message), {
9
- code: "payment_configuration_invalid", statusCode: 422, fieldErrors, messageCode, messageParams
10
- });
6
+ /**
7
+ * A refused payment configuration: every field error has its path, English
8
+ * message, code and params, and the error's messageCode names its own sentence.
9
+ */
10
+ class PaymentConfigurationError extends Error {
11
+ /**
12
+ * @param {string} message
13
+ * @param {Array<{ path: string, message: string, code?: string, params?: Record<string, unknown> }>} fieldErrors
14
+ * @param {{ messageCode: string, messageParams?: Record<string, unknown> }} translation
15
+ */
16
+ constructor(message, fieldErrors, { messageCode, messageParams = {} }) {
17
+ super(message);
18
+ this.name = "PaymentConfigurationError";
19
+ this.code = "payment_configuration_invalid";
20
+ this.statusCode = 422;
21
+ this.fieldErrors = fieldErrors;
22
+ this.messageCode = messageCode;
23
+ this.messageParams = messageParams;
24
+ }
11
25
  }
12
26
 
13
27
  function validatePaymentConfiguration(document) {
14
28
  const value = document?.extensions?.payments;
15
29
  if (!validate(value)) {
16
- throw configurationError("Check the application's payment configuration.",
30
+ throw new PaymentConfigurationError("Check the application's payment configuration.",
17
31
  validate.errors.map(({ instancePath, message }) => ({ path: `extensions.payments${instancePath}`, message })),
18
32
  { messageCode: "payments.configuration.invalid" });
19
33
  }
@@ -42,7 +56,7 @@ function validatePaymentConfiguration(document) {
42
56
  }
43
57
  }
44
58
  }
45
- if (fieldErrors.length) throw configurationError(fieldErrors[0].message, fieldErrors, {
59
+ if (fieldErrors.length) throw new PaymentConfigurationError(fieldErrors[0].message, fieldErrors, {
46
60
  messageCode: fieldErrors[0].code, messageParams: fieldErrors[0].params
47
61
  });
48
62
  // The validated declaration is JSON data; callers may supply reactive proxies.
@@ -9,7 +9,7 @@ import { createPaymentService } from '@jskit-ai/payments-core/server';
9
9
  import { createKnexPaymentStore } from '@jskit-ai/payments-core/server/storage';
10
10
  import migration from '../migrations/payments_core_initial.cjs';
11
11
 
12
- // No editor, Genesis project, network or generated application is involved.
12
+ // No editor, project tooling, network or generated application is involved.
13
13
  test('a hand-written app reloads payment policy from disk while retaining its own billing state', async () => {
14
14
  const directory = await mkdtemp(join(tmpdir(), 'standalone-payments-'));
15
15
  const path = join(directory, 'integrations.json');