@aglyn/aglyn 1.0.0-beta.232 → 1.0.0-beta.233

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.
Files changed (48) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/admin-audit-index.d.ts +3 -1
  3. package/src/lib/app-utils/admin-audit-index.js +9 -1
  4. package/src/lib/app-utils/admin-audit-index.js.map +1 -1
  5. package/src/lib/app-utils/analytics-summary.d.ts +95 -0
  6. package/src/lib/app-utils/analytics-summary.js +113 -0
  7. package/src/lib/app-utils/analytics-summary.js.map +1 -0
  8. package/src/lib/app-utils/artifact-list-keys.js +12 -0
  9. package/src/lib/app-utils/artifact-list-keys.js.map +1 -1
  10. package/src/lib/app-utils/artifact-list-queries.d.ts +16 -0
  11. package/src/lib/app-utils/artifact-list-queries.js +122 -4
  12. package/src/lib/app-utils/artifact-list-queries.js.map +1 -1
  13. package/src/lib/app-utils/crm.d.ts +30 -2
  14. package/src/lib/app-utils/crm.js +77 -15
  15. package/src/lib/app-utils/crm.js.map +1 -1
  16. package/src/lib/app-utils/docs-help.generated.d.ts +17 -5
  17. package/src/lib/app-utils/docs-help.generated.js +35 -1
  18. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  19. package/src/lib/app-utils/docs-index.generated.js +139 -6
  20. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  21. package/src/lib/app-utils/lockdown.js +1 -1
  22. package/src/lib/app-utils/lockdown.js.map +1 -1
  23. package/src/lib/app-utils/message-search.js +5 -2
  24. package/src/lib/app-utils/message-search.js.map +1 -1
  25. package/src/lib/app-utils/name-search.js +15 -3
  26. package/src/lib/app-utils/name-search.js.map +1 -1
  27. package/src/lib/app-utils/screen-analytics-aggregate.d.ts +70 -0
  28. package/src/lib/app-utils/screen-analytics-aggregate.js +82 -0
  29. package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -0
  30. package/src/lib/app-utils/screen-kind.d.ts +25 -0
  31. package/src/lib/app-utils/screen-kind.js +25 -0
  32. package/src/lib/app-utils/screen-kind.js.map +1 -0
  33. package/src/lib/app-utils/screen-route.d.ts +1 -5
  34. package/src/lib/app-utils/screen-route.js +2 -4
  35. package/src/lib/app-utils/screen-route.js.map +1 -1
  36. package/src/lib/app-utils/site-journey-steps.d.ts +38 -0
  37. package/src/lib/app-utils/site-journey-steps.js +54 -0
  38. package/src/lib/app-utils/site-journey-steps.js.map +1 -0
  39. package/src/lib/app-utils/site-journey.d.ts +2 -22
  40. package/src/lib/app-utils/site-journey.js +2 -32
  41. package/src/lib/app-utils/site-journey.js.map +1 -1
  42. package/src/lib/plugin-manager/first-party-plugins.generated.js +20 -2
  43. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  44. package/src/lib/plugin-manager/plugin-checkout-credits.d.ts +288 -0
  45. package/src/lib/plugin-manager/plugin-checkout-credits.js +196 -0
  46. package/src/lib/plugin-manager/plugin-checkout-credits.js.map +1 -0
  47. package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +10 -0
  48. package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -1
@@ -0,0 +1,288 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Money another plugin honors at checkout and at the register (AGL-3640).
19
+ *
20
+ * A rewards balance, store credit and a friend's referral credit are all the
21
+ * same thing to a seller: a code the buyer brings that pays part of the sale
22
+ * from an account the SELLER does not keep. The plugin that keeps the account
23
+ * registers a provider here; the seller asks it, by code, how much the account
24
+ * can give, reserves that much while an online buyer pays, and takes it in the
25
+ * same transaction that writes the sale. The seller never reads the provider's
26
+ * documents and the provider never reads the seller's: the sale records which
27
+ * provider paid what, under the provider's own opaque `reference`, and the
28
+ * seller's order events carry that onward.
29
+ *
30
+ * ## Money
31
+ *
32
+ * Integer cents in the sale's currency, always. A provider answers what it
33
+ * CAN give and the seller never takes more than that.
34
+ *
35
+ * - Online, the seller {@link PluginCheckoutCreditProvider.hold | holds}
36
+ * before the payment processor is asked, so a shortfall is a refusal and
37
+ * not an apology. The hold is keyed by the checkout attempt, so a retry
38
+ * re-places the same hold rather than a second one, and it lapses on its
39
+ * own if the checkout is abandoned and nobody releases it.
40
+ * - When the sale is written, the seller {@link PluginCheckoutCreditProvider.stage | stages}
41
+ * the account INSIDE its own transaction: the provider reads (and only
42
+ * reads) what it needs, and the stage's `debit` writes the redemption in
43
+ * the same commit as the sale. A sale that is not written takes nothing;
44
+ * a redemption is never written without its sale.
45
+ * - A refund gives back through {@link PluginCheckoutCreditProvider.restore | restore},
46
+ * idempotent per key, so a retried refund restores once.
47
+ *
48
+ * ## Nobody home
49
+ *
50
+ * With no provider registered, or none offered on a site, the seller shows no
51
+ * field and sells exactly as it did before. Every reader here that a seller
52
+ * calls on a sale path is safe to call with nothing registered.
53
+ *
54
+ * Import this module by its own subpath
55
+ * (`@aglyn/aglyn/plugin-manager/plugin-checkout-credits`); it is not in the
56
+ * barrel.
57
+ */
58
+ /** Where the sale happens. */
59
+ export type CheckoutCreditChannel = 'online' | 'pos';
60
+ /**
61
+ * A server transaction as a provider uses it: the Admin SDK's shape, typed
62
+ * structurally so the platform names no database SDK. Every `get` happens
63
+ * before any write, as the database requires.
64
+ */
65
+ export interface CheckoutCreditTransaction {
66
+ get(ref: any): Promise<any>;
67
+ set(ref: any, data: any, options?: any): unknown;
68
+ update(ref: any, data: any): unknown;
69
+ create(ref: any, data: any): unknown;
70
+ }
71
+ /** A provider's refusal, in words the buyer or the cashier can read. */
72
+ export interface CheckoutCreditRefusal {
73
+ ok: false;
74
+ /** 400 for a bad code, 404 for an unknown one, 409 for an empty or busy one. */
75
+ status: number;
76
+ error: string;
77
+ }
78
+ /** The account a code names. */
79
+ export interface CheckoutCreditAccount {
80
+ ok: true;
81
+ /**
82
+ * The provider's own handle on the account, stored on the sale and handed
83
+ * back to it later. Never shown to a buyer and never a bearer secret.
84
+ * {@link CHECKOUT_CREDIT_REFERENCE} holds its shape.
85
+ */
86
+ reference: string;
87
+ /** What the buyer and the receipt call it: `Rewards`, `Referral credit`. */
88
+ label: string;
89
+ /** The last characters of the code, for the register and the receipt. */
90
+ last4: string;
91
+ /** What the account could give right now, integer cents. */
92
+ availableCents: number;
93
+ /** One line for the cashier: `1,250 points and $5.00 store credit`. */
94
+ detail?: string;
95
+ }
96
+ /**
97
+ * The account as the seller's transaction read it. The provider builds it in
98
+ * {@link PluginCheckoutCreditProvider.stage}, which reads; its methods only
99
+ * WRITE, through the same transaction, so the redemption commits with the sale
100
+ * or not at all.
101
+ */
102
+ export interface CheckoutCreditStage {
103
+ /**
104
+ * What the account may give now, honoring every live hold except the one
105
+ * the stage was opened for (that one is this sale's own).
106
+ */
107
+ availableCents: number;
108
+ /**
109
+ * Takes up to `cents` and returns what was taken: never more than
110
+ * `availableCents` plus this sale's own hold. `key` names this redemption
111
+ * (the attempt, or the register payment) and is what a void reverses.
112
+ */
113
+ debit(input: {
114
+ cents: number;
115
+ key: string;
116
+ orderId: string;
117
+ channel: CheckoutCreditChannel;
118
+ }): number;
119
+ /** Gives back exactly what the redemption named by `key` took: a voided register payment. */
120
+ reverse(input: {
121
+ key: string;
122
+ orderId: string;
123
+ }): number;
124
+ }
125
+ export interface CheckoutCreditHoldRequest {
126
+ hostId: string;
127
+ reference: string;
128
+ /** The checkout attempt: the same key re-places the same hold. */
129
+ holdKey: string;
130
+ /** The most the sale can take: what is left to pay on the goods. */
131
+ maxCents: number;
132
+ /** ISO-4217, lower case. */
133
+ currency: string;
134
+ customerEmail: string | null;
135
+ nowMs: number;
136
+ }
137
+ export interface PluginCheckoutCreditProvider {
138
+ /** Stable within the plugin: lower-case words and dashes. */
139
+ key: string;
140
+ /** What the field and the register's tender button say: `Rewards`. */
141
+ label: string;
142
+ /** Whether a code is one this provider issues. Syntax only: no reads. */
143
+ recognizes(code: string): boolean;
144
+ /** Whether the site offers it at all: plugin on, program on, plan carries it. */
145
+ offered(input: {
146
+ hostId: string;
147
+ channel: CheckoutCreditChannel;
148
+ }): Promise<boolean>;
149
+ /**
150
+ * A buyer's code — or, at the register only, a `reference` a staff
151
+ * {@link lookup} returned — to the account it names. `staff` is true only
152
+ * when the seller has authenticated a staff member for this site; a
153
+ * provider honors a bare `reference` only then.
154
+ */
155
+ resolve(input: {
156
+ hostId: string;
157
+ code?: string;
158
+ reference?: string;
159
+ channel: CheckoutCreditChannel;
160
+ customerEmail: string | null;
161
+ staff: boolean;
162
+ }): Promise<CheckoutCreditAccount | CheckoutCreditRefusal>;
163
+ /** Reserves up to `maxCents` for one online checkout attempt. */
164
+ hold(input: CheckoutCreditHoldRequest): Promise<{
165
+ ok: true;
166
+ cents: number;
167
+ } | CheckoutCreditRefusal>;
168
+ /** Lets a hold go: an abandoned or refused checkout. Never throws. */
169
+ release(input: {
170
+ hostId: string;
171
+ reference: string;
172
+ holdKey: string;
173
+ }): Promise<void>;
174
+ /**
175
+ * Reads the account inside the seller's transaction. `null` when it no
176
+ * longer exists. `holdKey` is this sale's own online hold, when it has one.
177
+ */
178
+ stage(input: {
179
+ transaction: CheckoutCreditTransaction;
180
+ hostId: string;
181
+ reference: string;
182
+ orderId: string;
183
+ nowMs: number;
184
+ holdKey?: string;
185
+ }): Promise<CheckoutCreditStage | null>;
186
+ /**
187
+ * Gives back up to `cents` of what the sale took from the account, for a
188
+ * refund. Idempotent per `key`; returns the cents given back (0 for a key
189
+ * already restored, or an account that is gone).
190
+ */
191
+ restore(input: {
192
+ hostId: string;
193
+ reference: string;
194
+ orderId: string;
195
+ cents: number;
196
+ key: string;
197
+ }): Promise<number>;
198
+ /** Staff search at the register by email or code. Optional. */
199
+ lookup?(input: {
200
+ hostId: string;
201
+ query: string;
202
+ }): Promise<CheckoutCreditAccount[]>;
203
+ }
204
+ /** A provider as the seller resolves it. */
205
+ export interface ResolvedCheckoutCreditProvider {
206
+ /** `{pluginId}.{key}`: what a sale records. */
207
+ providerId: string;
208
+ pluginId: string;
209
+ provider: PluginCheckoutCreditProvider;
210
+ }
211
+ /**
212
+ * What a sale records of one redemption, on the sale and in the events the
213
+ * seller raises about it, so the provider learns what was taken from the
214
+ * seller's own facts. `appliedAs` says how the seller counted it: online it
215
+ * comes off the goods like a discount, at the register it is one of the
216
+ * sale's payments.
217
+ */
218
+ export interface PluginCheckoutCreditSold {
219
+ providerId: string;
220
+ pluginId: string;
221
+ key: string;
222
+ reference: string;
223
+ label: string;
224
+ last4: string;
225
+ amountCents: number;
226
+ appliedAs: 'discount' | 'tender';
227
+ }
228
+ /** The shape of a provider's `reference`: short, and safe in a document path segment. */
229
+ export declare const CHECKOUT_CREDIT_REFERENCE: RegExp;
230
+ /** Joins the providers. A plugin re-registering a key replaces its own. */
231
+ export declare function registerPluginCheckoutCredit(provider: PluginCheckoutCreditProvider, options?: {
232
+ pluginId?: string;
233
+ }): void;
234
+ /** Whether any plugin registered a provider: a seller with none skips the field. */
235
+ export declare function hasPluginCheckoutCredits(): boolean;
236
+ /**
237
+ * A code as typed — any case, spaces and dashes — in the one form providers
238
+ * see: upper case, letters, digits and single dashes, at most 40 characters.
239
+ * Empty when nothing usable was typed.
240
+ */
241
+ export declare function normalizeCheckoutCreditCode(value: unknown): string;
242
+ /** The provider that issues a code, or `null`. The first to recognize it answers. */
243
+ export declare function checkoutCreditProviderForCode(code: string): ResolvedCheckoutCreditProvider | null;
244
+ /** One provider by the id a sale recorded, or `null` when it is gone. */
245
+ export declare function checkoutCreditProvider(providerId: string): ResolvedCheckoutCreditProvider | null;
246
+ /**
247
+ * The providers a site offers on a channel, for the cart's field and the
248
+ * register's tender. Never throws: a provider that fails is not offered.
249
+ */
250
+ export declare function offeredCheckoutCredits(input: {
251
+ hostId: string;
252
+ channel: CheckoutCreditChannel;
253
+ }): Promise<Array<{
254
+ providerId: string;
255
+ label: string;
256
+ lookup: boolean;
257
+ }>>;
258
+ /** An account answer held to the contract, or a refusal in its place. */
259
+ export declare function normalizeCheckoutCreditAccount(answer: CheckoutCreditAccount | CheckoutCreditRefusal | null | undefined): CheckoutCreditAccount | CheckoutCreditRefusal;
260
+ /** Cents a provider says it held or took, as whole non-negative cents no larger than `ceiling`. */
261
+ export declare function boundedCreditCents(value: unknown, ceiling: number): number;
262
+ /** The metadata key an online redemption rides under on the payment processor. */
263
+ export declare const CHECKOUT_CREDIT_METADATA_KEY = "credit0";
264
+ /** What an online checkout carries to its webhook: the hold to settle. */
265
+ export interface CheckoutCreditHeld {
266
+ providerId: string;
267
+ reference: string;
268
+ holdKey: string;
269
+ amountCents: number;
270
+ label: string;
271
+ last4: string;
272
+ }
273
+ /**
274
+ * The hold an online checkout placed, packed for a payment processor's
275
+ * metadata: one key holding `[providerId, reference, holdKey, cents, label, last4]`
276
+ * as JSON, inside a 500-character value. Never the code itself: metadata is
277
+ * readable on the merchant's dashboard, and a code is a bearer secret.
278
+ */
279
+ export declare function encodeCheckoutCreditMetadata(held: CheckoutCreditHeld): Record<string, string>;
280
+ /** Reads {@link encodeCheckoutCreditMetadata} back. Anything unreadable is `null`, never guessed. */
281
+ export declare function decodeCheckoutCreditMetadata(metadata: Record<string, unknown> | null | undefined): CheckoutCreditHeld | null;
282
+ /** The plugin id and key of a provider id. */
283
+ export declare function splitCheckoutCreditProviderId(providerId: string): {
284
+ pluginId: string;
285
+ key: string;
286
+ } | null;
287
+ /** A recorded redemption, as a seller stores it and its events carry it. */
288
+ export declare function checkoutCreditSold(input: Omit<PluginCheckoutCreditSold, 'pluginId' | 'key'>): PluginCheckoutCreditSold | null;
@@ -0,0 +1,196 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
18
+ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
19
+ const KEY = /^[a-z][a-z0-9-]{1,39}$/;
20
+ const PROVIDER_ID = /^([a-z][a-z0-9-]{0,63})\.([a-z][a-z0-9-]{1,39})$/;
21
+ /** The shape of a provider's `reference`: short, and safe in a document path segment. */ export const CHECKOUT_CREDIT_REFERENCE = /^[A-Za-z0-9:_-]{1,128}$/;
22
+ const PLUGIN_CHECKOUT_CREDITS = definePluginServiceContract('core.checkout-credits', {
23
+ multiple: true
24
+ });
25
+ /** Joins the providers. A plugin re-registering a key replaces its own. */ export function registerPluginCheckoutCredit(provider, options) {
26
+ var _ref, _getRegisteringPluginId;
27
+ if (!KEY.test(String((_ref = provider == null ? void 0 : provider.key) != null ? _ref : ''))) {
28
+ throw new Error(`checkout credit provider key "${provider == null ? void 0 : provider.key}" must be lower-case words and dashes`);
29
+ }
30
+ const pluginId = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId;
31
+ registerPluginService(PLUGIN_CHECKOUT_CREDITS, provider, _extends({}, pluginId ? {
32
+ pluginId
33
+ } : {}, {
34
+ key: provider.key
35
+ }));
36
+ }
37
+ function resolved() {
38
+ return resolvePluginServices(PLUGIN_CHECKOUT_CREDITS).map((entry)=>({
39
+ providerId: `${entry.pluginId}.${entry.impl.key}`,
40
+ pluginId: entry.pluginId,
41
+ provider: entry.impl
42
+ }));
43
+ }
44
+ /** Whether any plugin registered a provider: a seller with none skips the field. */ export function hasPluginCheckoutCredits() {
45
+ return resolvePluginServices(PLUGIN_CHECKOUT_CREDITS).length > 0;
46
+ }
47
+ /**
48
+ * A code as typed — any case, spaces and dashes — in the one form providers
49
+ * see: upper case, letters, digits and single dashes, at most 40 characters.
50
+ * Empty when nothing usable was typed.
51
+ */ export function normalizeCheckoutCreditCode(value) {
52
+ return String(value != null ? value : '').toUpperCase().replace(/[^A-Z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
53
+ }
54
+ /** The provider that issues a code, or `null`. The first to recognize it answers. */ export function checkoutCreditProviderForCode(code) {
55
+ const normalized = normalizeCheckoutCreditCode(code);
56
+ if (!normalized) return null;
57
+ for (const entry of resolved()){
58
+ try {
59
+ if (entry.provider.recognizes(normalized)) return entry;
60
+ } catch (error) {
61
+ console.error(`[checkout-credits] "${entry.providerId}" failed to read a code`, error);
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ /** One provider by the id a sale recorded, or `null` when it is gone. */ export function checkoutCreditProvider(providerId) {
67
+ var _resolved_find;
68
+ return (_resolved_find = resolved().find((entry)=>entry.providerId === providerId)) != null ? _resolved_find : null;
69
+ }
70
+ /**
71
+ * The providers a site offers on a channel, for the cart's field and the
72
+ * register's tender. Never throws: a provider that fails is not offered.
73
+ */ export async function offeredCheckoutCredits(input) {
74
+ const answers = await Promise.all(resolved().map(async (entry)=>{
75
+ const offered = await entry.provider.offered(input).catch((error)=>{
76
+ console.error(`[checkout-credits] "${entry.providerId}" offered() failed for ${input.hostId}`, error);
77
+ return false;
78
+ });
79
+ return offered ? {
80
+ providerId: entry.providerId,
81
+ label: entry.provider.label,
82
+ lookup: typeof entry.provider.lookup === 'function'
83
+ } : null;
84
+ }));
85
+ return answers.filter((answer)=>Boolean(answer));
86
+ }
87
+ /** An account answer held to the contract, or a refusal in its place. */ export function normalizeCheckoutCreditAccount(answer) {
88
+ var _answer_reference;
89
+ if (!answer || typeof answer !== 'object') {
90
+ return {
91
+ ok: false,
92
+ status: 404,
93
+ error: 'That code is not valid.'
94
+ };
95
+ }
96
+ if (answer.ok !== true) {
97
+ const status = Number.isInteger(answer.status) && answer.status >= 400 && answer.status < 500 ? answer.status : 409;
98
+ return {
99
+ ok: false,
100
+ status,
101
+ error: cleanText(answer.error, 160) || 'That code cannot be used.'
102
+ };
103
+ }
104
+ if (!CHECKOUT_CREDIT_REFERENCE.test(String((_answer_reference = answer.reference) != null ? _answer_reference : ''))) {
105
+ return {
106
+ ok: false,
107
+ status: 404,
108
+ error: 'That code is not valid.'
109
+ };
110
+ }
111
+ const available = answer.availableCents;
112
+ return _extends({
113
+ ok: true,
114
+ reference: answer.reference,
115
+ label: cleanText(answer.label, 40) || 'Store credit',
116
+ last4: cleanText(answer.last4, 4).toUpperCase(),
117
+ availableCents: Number.isSafeInteger(available) && available > 0 ? available : 0
118
+ }, answer.detail ? {
119
+ detail: cleanText(answer.detail, 120)
120
+ } : {});
121
+ }
122
+ /** Cents a provider says it held or took, as whole non-negative cents no larger than `ceiling`. */ export function boundedCreditCents(value, ceiling) {
123
+ const cents = typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : 0;
124
+ const max = Number.isSafeInteger(ceiling) && ceiling > 0 ? ceiling : 0;
125
+ return Math.min(cents, max);
126
+ }
127
+ function cleanText(value, max) {
128
+ return Array.from(String(value != null ? value : ''), (char)=>char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 ? ' ' : char).join('').replace(/\s+/g, ' ').trim().slice(0, max);
129
+ }
130
+ /** The metadata key an online redemption rides under on the payment processor. */ export const CHECKOUT_CREDIT_METADATA_KEY = 'credit0';
131
+ /**
132
+ * The hold an online checkout placed, packed for a payment processor's
133
+ * metadata: one key holding `[providerId, reference, holdKey, cents, label, last4]`
134
+ * as JSON, inside a 500-character value. Never the code itself: metadata is
135
+ * readable on the merchant's dashboard, and a code is a bearer secret.
136
+ */ export function encodeCheckoutCreditMetadata(held) {
137
+ const entry = [
138
+ held.providerId,
139
+ held.reference,
140
+ held.holdKey.slice(0, 200),
141
+ held.amountCents,
142
+ cleanText(held.label, 40),
143
+ cleanText(held.last4, 4)
144
+ ];
145
+ return {
146
+ [CHECKOUT_CREDIT_METADATA_KEY]: JSON.stringify(entry)
147
+ };
148
+ }
149
+ /** Reads {@link encodeCheckoutCreditMetadata} back. Anything unreadable is `null`, never guessed. */ export function decodeCheckoutCreditMetadata(metadata) {
150
+ const raw = metadata == null ? void 0 : metadata[CHECKOUT_CREDIT_METADATA_KEY];
151
+ if (raw === undefined || raw === null || raw === '') return null;
152
+ let entry;
153
+ try {
154
+ entry = JSON.parse(String(raw));
155
+ } catch (unused) {
156
+ return null;
157
+ }
158
+ if (!Array.isArray(entry)) return null;
159
+ const [providerId, reference, holdKey, cents, label, last4] = entry;
160
+ if (!PROVIDER_ID.test(String(providerId != null ? providerId : ''))) return null;
161
+ if (!CHECKOUT_CREDIT_REFERENCE.test(String(reference != null ? reference : ''))) return null;
162
+ if (typeof holdKey !== 'string' || !holdKey) return null;
163
+ if (typeof cents !== 'number' || !Number.isSafeInteger(cents) || cents <= 0) return null;
164
+ return {
165
+ providerId: String(providerId),
166
+ reference: String(reference),
167
+ holdKey,
168
+ amountCents: cents,
169
+ label: cleanText(label, 40) || 'Store credit',
170
+ last4: cleanText(last4, 4)
171
+ };
172
+ }
173
+ /** The plugin id and key of a provider id. */ export function splitCheckoutCreditProviderId(providerId) {
174
+ const match = PROVIDER_ID.exec(String(providerId != null ? providerId : ''));
175
+ return match ? {
176
+ pluginId: match[1],
177
+ key: match[2]
178
+ } : null;
179
+ }
180
+ /** A recorded redemption, as a seller stores it and its events carry it. */ export function checkoutCreditSold(input) {
181
+ const parts = splitCheckoutCreditProviderId(input.providerId);
182
+ if (!parts || !CHECKOUT_CREDIT_REFERENCE.test(input.reference)) return null;
183
+ if (!Number.isSafeInteger(input.amountCents) || input.amountCents <= 0) return null;
184
+ return {
185
+ providerId: input.providerId,
186
+ pluginId: parts.pluginId,
187
+ key: parts.key,
188
+ reference: input.reference,
189
+ label: cleanText(input.label, 40) || 'Store credit',
190
+ last4: cleanText(input.last4, 4),
191
+ amountCents: input.amountCents,
192
+ appliedAs: input.appliedAs === 'tender' ? 'tender' : 'discount'
193
+ };
194
+ }
195
+
196
+ //# sourceMappingURL=plugin-checkout-credits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-checkout-credits.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Money another plugin honors at checkout and at the register (AGL-3640).\n *\n * A rewards balance, store credit and a friend's referral credit are all the\n * same thing to a seller: a code the buyer brings that pays part of the sale\n * from an account the SELLER does not keep. The plugin that keeps the account\n * registers a provider here; the seller asks it, by code, how much the account\n * can give, reserves that much while an online buyer pays, and takes it in the\n * same transaction that writes the sale. The seller never reads the provider's\n * documents and the provider never reads the seller's: the sale records which\n * provider paid what, under the provider's own opaque `reference`, and the\n * seller's order events carry that onward.\n *\n * ## Money\n *\n * Integer cents in the sale's currency, always. A provider answers what it\n * CAN give and the seller never takes more than that.\n *\n * - Online, the seller {@link PluginCheckoutCreditProvider.hold | holds}\n * before the payment processor is asked, so a shortfall is a refusal and\n * not an apology. The hold is keyed by the checkout attempt, so a retry\n * re-places the same hold rather than a second one, and it lapses on its\n * own if the checkout is abandoned and nobody releases it.\n * - When the sale is written, the seller {@link PluginCheckoutCreditProvider.stage | stages}\n * the account INSIDE its own transaction: the provider reads (and only\n * reads) what it needs, and the stage's `debit` writes the redemption in\n * the same commit as the sale. A sale that is not written takes nothing;\n * a redemption is never written without its sale.\n * - A refund gives back through {@link PluginCheckoutCreditProvider.restore | restore},\n * idempotent per key, so a retried refund restores once.\n *\n * ## Nobody home\n *\n * With no provider registered, or none offered on a site, the seller shows no\n * field and sells exactly as it did before. Every reader here that a seller\n * calls on a sale path is safe to call with nothing registered.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-checkout-credits`); it is not in the\n * barrel.\n */\n\n/** Where the sale happens. */\nexport type CheckoutCreditChannel = 'online' | 'pos'\n\n/**\n * A server transaction as a provider uses it: the Admin SDK's shape, typed\n * structurally so the platform names no database SDK. Every `get` happens\n * before any write, as the database requires.\n */\nexport interface CheckoutCreditTransaction {\n get(ref: any): Promise<any>\n set(ref: any, data: any, options?: any): unknown\n update(ref: any, data: any): unknown\n create(ref: any, data: any): unknown\n}\n\n/** A provider's refusal, in words the buyer or the cashier can read. */\nexport interface CheckoutCreditRefusal {\n ok: false\n /** 400 for a bad code, 404 for an unknown one, 409 for an empty or busy one. */\n status: number\n error: string\n}\n\n/** The account a code names. */\nexport interface CheckoutCreditAccount {\n ok: true\n /**\n * The provider's own handle on the account, stored on the sale and handed\n * back to it later. Never shown to a buyer and never a bearer secret.\n * {@link CHECKOUT_CREDIT_REFERENCE} holds its shape.\n */\n reference: string\n /** What the buyer and the receipt call it: `Rewards`, `Referral credit`. */\n label: string\n /** The last characters of the code, for the register and the receipt. */\n last4: string\n /** What the account could give right now, integer cents. */\n availableCents: number\n /** One line for the cashier: `1,250 points and $5.00 store credit`. */\n detail?: string\n}\n\n/**\n * The account as the seller's transaction read it. The provider builds it in\n * {@link PluginCheckoutCreditProvider.stage}, which reads; its methods only\n * WRITE, through the same transaction, so the redemption commits with the sale\n * or not at all.\n */\nexport interface CheckoutCreditStage {\n /**\n * What the account may give now, honoring every live hold except the one\n * the stage was opened for (that one is this sale's own).\n */\n availableCents: number\n /**\n * Takes up to `cents` and returns what was taken: never more than\n * `availableCents` plus this sale's own hold. `key` names this redemption\n * (the attempt, or the register payment) and is what a void reverses.\n */\n debit(input: { cents: number; key: string; orderId: string; channel: CheckoutCreditChannel }): number\n /** Gives back exactly what the redemption named by `key` took: a voided register payment. */\n reverse(input: { key: string; orderId: string }): number\n}\n\nexport interface CheckoutCreditHoldRequest {\n hostId: string\n reference: string\n /** The checkout attempt: the same key re-places the same hold. */\n holdKey: string\n /** The most the sale can take: what is left to pay on the goods. */\n maxCents: number\n /** ISO-4217, lower case. */\n currency: string\n customerEmail: string | null\n nowMs: number\n}\n\nexport interface PluginCheckoutCreditProvider {\n /** Stable within the plugin: lower-case words and dashes. */\n key: string\n /** What the field and the register's tender button say: `Rewards`. */\n label: string\n /** Whether a code is one this provider issues. Syntax only: no reads. */\n recognizes(code: string): boolean\n /** Whether the site offers it at all: plugin on, program on, plan carries it. */\n offered(input: { hostId: string; channel: CheckoutCreditChannel }): Promise<boolean>\n /**\n * A buyer's code — or, at the register only, a `reference` a staff\n * {@link lookup} returned — to the account it names. `staff` is true only\n * when the seller has authenticated a staff member for this site; a\n * provider honors a bare `reference` only then.\n */\n resolve(input: {\n hostId: string\n code?: string\n reference?: string\n channel: CheckoutCreditChannel\n customerEmail: string | null\n staff: boolean\n }): Promise<CheckoutCreditAccount | CheckoutCreditRefusal>\n /** Reserves up to `maxCents` for one online checkout attempt. */\n hold(input: CheckoutCreditHoldRequest): Promise<{ ok: true; cents: number } | CheckoutCreditRefusal>\n /** Lets a hold go: an abandoned or refused checkout. Never throws. */\n release(input: { hostId: string; reference: string; holdKey: string }): Promise<void>\n /**\n * Reads the account inside the seller's transaction. `null` when it no\n * longer exists. `holdKey` is this sale's own online hold, when it has one.\n */\n stage(input: {\n transaction: CheckoutCreditTransaction\n hostId: string\n reference: string\n orderId: string\n nowMs: number\n holdKey?: string\n }): Promise<CheckoutCreditStage | null>\n /**\n * Gives back up to `cents` of what the sale took from the account, for a\n * refund. Idempotent per `key`; returns the cents given back (0 for a key\n * already restored, or an account that is gone).\n */\n restore(input: { hostId: string; reference: string; orderId: string; cents: number; key: string }): Promise<number>\n /** Staff search at the register by email or code. Optional. */\n lookup?(input: { hostId: string; query: string }): Promise<CheckoutCreditAccount[]>\n}\n\n/** A provider as the seller resolves it. */\nexport interface ResolvedCheckoutCreditProvider {\n /** `{pluginId}.{key}`: what a sale records. */\n providerId: string\n pluginId: string\n provider: PluginCheckoutCreditProvider\n}\n\n/**\n * What a sale records of one redemption, on the sale and in the events the\n * seller raises about it, so the provider learns what was taken from the\n * seller's own facts. `appliedAs` says how the seller counted it: online it\n * comes off the goods like a discount, at the register it is one of the\n * sale's payments.\n */\nexport interface PluginCheckoutCreditSold {\n providerId: string\n pluginId: string\n key: string\n reference: string\n label: string\n last4: string\n amountCents: number\n appliedAs: 'discount' | 'tender'\n}\n\nconst KEY = /^[a-z][a-z0-9-]{1,39}$/\nconst PROVIDER_ID = /^([a-z][a-z0-9-]{0,63})\\.([a-z][a-z0-9-]{1,39})$/\n\n/** The shape of a provider's `reference`: short, and safe in a document path segment. */\nexport const CHECKOUT_CREDIT_REFERENCE = /^[A-Za-z0-9:_-]{1,128}$/\n\nconst PLUGIN_CHECKOUT_CREDITS = definePluginServiceContract<PluginCheckoutCreditProvider>(\n 'core.checkout-credits',\n { multiple: true },\n)\n\n/** Joins the providers. A plugin re-registering a key replaces its own. */\nexport function registerPluginCheckoutCredit(\n provider: PluginCheckoutCreditProvider,\n options?: { pluginId?: string },\n): void {\n if (!KEY.test(String(provider?.key ?? ''))) {\n throw new Error(`checkout credit provider key \"${provider?.key}\" must be lower-case words and dashes`)\n }\n const pluginId = getRegisteringPluginId() ?? options?.pluginId\n registerPluginService(PLUGIN_CHECKOUT_CREDITS, provider, {\n ...(pluginId ? { pluginId } : {}),\n key: provider.key,\n })\n}\n\nfunction resolved(): ResolvedCheckoutCreditProvider[] {\n return resolvePluginServices(PLUGIN_CHECKOUT_CREDITS).map((entry) => ({\n providerId: `${entry.pluginId}.${entry.impl.key}`,\n pluginId: entry.pluginId,\n provider: entry.impl,\n }))\n}\n\n/** Whether any plugin registered a provider: a seller with none skips the field. */\nexport function hasPluginCheckoutCredits(): boolean {\n return resolvePluginServices(PLUGIN_CHECKOUT_CREDITS).length > 0\n}\n\n/**\n * A code as typed — any case, spaces and dashes — in the one form providers\n * see: upper case, letters, digits and single dashes, at most 40 characters.\n * Empty when nothing usable was typed.\n */\nexport function normalizeCheckoutCreditCode(value: unknown): string {\n return String(value ?? '')\n .toUpperCase()\n .replace(/[^A-Z0-9-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 40)\n}\n\n/** The provider that issues a code, or `null`. The first to recognize it answers. */\nexport function checkoutCreditProviderForCode(code: string): ResolvedCheckoutCreditProvider | null {\n const normalized = normalizeCheckoutCreditCode(code)\n if (!normalized) return null\n for (const entry of resolved()) {\n try {\n if (entry.provider.recognizes(normalized)) return entry\n } catch (error) {\n console.error(`[checkout-credits] \"${entry.providerId}\" failed to read a code`, error)\n }\n }\n return null\n}\n\n/** One provider by the id a sale recorded, or `null` when it is gone. */\nexport function checkoutCreditProvider(providerId: string): ResolvedCheckoutCreditProvider | null {\n return resolved().find((entry) => entry.providerId === providerId) ?? null\n}\n\n/**\n * The providers a site offers on a channel, for the cart's field and the\n * register's tender. Never throws: a provider that fails is not offered.\n */\nexport async function offeredCheckoutCredits(input: {\n hostId: string\n channel: CheckoutCreditChannel\n}): Promise<Array<{ providerId: string; label: string; lookup: boolean }>> {\n const answers = await Promise.all(\n resolved().map(async (entry) => {\n const offered = await entry.provider.offered(input).catch((error: unknown) => {\n console.error(`[checkout-credits] \"${entry.providerId}\" offered() failed for ${input.hostId}`, error)\n return false\n })\n return offered\n ? { providerId: entry.providerId, label: entry.provider.label, lookup: typeof entry.provider.lookup === 'function' }\n : null\n }),\n )\n return answers.filter((answer): answer is { providerId: string; label: string; lookup: boolean } => Boolean(answer))\n}\n\n/** An account answer held to the contract, or a refusal in its place. */\nexport function normalizeCheckoutCreditAccount(\n answer: CheckoutCreditAccount | CheckoutCreditRefusal | null | undefined,\n): CheckoutCreditAccount | CheckoutCreditRefusal {\n if (!answer || typeof answer !== 'object') {\n return { ok: false, status: 404, error: 'That code is not valid.' }\n }\n if (answer.ok !== true) {\n const status = Number.isInteger(answer.status) && answer.status >= 400 && answer.status < 500 ? answer.status : 409\n return { ok: false, status, error: cleanText(answer.error, 160) || 'That code cannot be used.' }\n }\n if (!CHECKOUT_CREDIT_REFERENCE.test(String(answer.reference ?? ''))) {\n return { ok: false, status: 404, error: 'That code is not valid.' }\n }\n const available = answer.availableCents\n return {\n ok: true,\n reference: answer.reference,\n label: cleanText(answer.label, 40) || 'Store credit',\n last4: cleanText(answer.last4, 4).toUpperCase(),\n availableCents: Number.isSafeInteger(available) && available > 0 ? available : 0,\n ...(answer.detail ? { detail: cleanText(answer.detail, 120) } : {}),\n }\n}\n\n/** Cents a provider says it held or took, as whole non-negative cents no larger than `ceiling`. */\nexport function boundedCreditCents(value: unknown, ceiling: number): number {\n const cents = typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : 0\n const max = Number.isSafeInteger(ceiling) && ceiling > 0 ? ceiling : 0\n return Math.min(cents, max)\n}\n\nfunction cleanText(value: unknown, max: number): string {\n return Array.from(String(value ?? ''), (char) =>\n char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 ? ' ' : char,\n )\n .join('')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, max)\n}\n\n/** The metadata key an online redemption rides under on the payment processor. */\nexport const CHECKOUT_CREDIT_METADATA_KEY = 'credit0'\n\n/** What an online checkout carries to its webhook: the hold to settle. */\nexport interface CheckoutCreditHeld {\n providerId: string\n reference: string\n holdKey: string\n amountCents: number\n label: string\n last4: string\n}\n\n/**\n * The hold an online checkout placed, packed for a payment processor's\n * metadata: one key holding `[providerId, reference, holdKey, cents, label, last4]`\n * as JSON, inside a 500-character value. Never the code itself: metadata is\n * readable on the merchant's dashboard, and a code is a bearer secret.\n */\nexport function encodeCheckoutCreditMetadata(held: CheckoutCreditHeld): Record<string, string> {\n const entry = [\n held.providerId,\n held.reference,\n held.holdKey.slice(0, 200),\n held.amountCents,\n cleanText(held.label, 40),\n cleanText(held.last4, 4),\n ]\n return { [CHECKOUT_CREDIT_METADATA_KEY]: JSON.stringify(entry) }\n}\n\n/** Reads {@link encodeCheckoutCreditMetadata} back. Anything unreadable is `null`, never guessed. */\nexport function decodeCheckoutCreditMetadata(\n metadata: Record<string, unknown> | null | undefined,\n): CheckoutCreditHeld | null {\n const raw = metadata?.[CHECKOUT_CREDIT_METADATA_KEY]\n if (raw === undefined || raw === null || raw === '') return null\n let entry: unknown\n try {\n entry = JSON.parse(String(raw))\n } catch {\n return null\n }\n if (!Array.isArray(entry)) return null\n const [providerId, reference, holdKey, cents, label, last4] = entry\n if (!PROVIDER_ID.test(String(providerId ?? ''))) return null\n if (!CHECKOUT_CREDIT_REFERENCE.test(String(reference ?? ''))) return null\n if (typeof holdKey !== 'string' || !holdKey) return null\n if (typeof cents !== 'number' || !Number.isSafeInteger(cents) || cents <= 0) return null\n return {\n providerId: String(providerId),\n reference: String(reference),\n holdKey,\n amountCents: cents,\n label: cleanText(label, 40) || 'Store credit',\n last4: cleanText(last4, 4),\n }\n}\n\n/** The plugin id and key of a provider id. */\nexport function splitCheckoutCreditProviderId(providerId: string): { pluginId: string; key: string } | null {\n const match = PROVIDER_ID.exec(String(providerId ?? ''))\n return match ? { pluginId: match[1], key: match[2] } : null\n}\n\n/** A recorded redemption, as a seller stores it and its events carry it. */\nexport function checkoutCreditSold(\n input: Omit<PluginCheckoutCreditSold, 'pluginId' | 'key'>,\n): PluginCheckoutCreditSold | null {\n const parts = splitCheckoutCreditProviderId(input.providerId)\n if (!parts || !CHECKOUT_CREDIT_REFERENCE.test(input.reference)) return null\n if (!Number.isSafeInteger(input.amountCents) || input.amountCents <= 0) return null\n return {\n providerId: input.providerId,\n pluginId: parts.pluginId,\n key: parts.key,\n reference: input.reference,\n label: cleanText(input.label, 40) || 'Store credit',\n last4: cleanText(input.last4, 4),\n amountCents: input.amountCents,\n appliedAs: input.appliedAs === 'tender' ? 'tender' : 'discount',\n }\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","KEY","PROVIDER_ID","CHECKOUT_CREDIT_REFERENCE","PLUGIN_CHECKOUT_CREDITS","multiple","registerPluginCheckoutCredit","provider","options","test","String","key","Error","pluginId","resolved","map","entry","providerId","impl","hasPluginCheckoutCredits","length","normalizeCheckoutCreditCode","value","toUpperCase","replace","slice","checkoutCreditProviderForCode","code","normalized","recognizes","error","console","checkoutCreditProvider","find","offeredCheckoutCredits","input","answers","Promise","all","offered","catch","hostId","label","lookup","filter","answer","Boolean","normalizeCheckoutCreditAccount","ok","status","Number","isInteger","cleanText","reference","available","availableCents","last4","isSafeInteger","detail","boundedCreditCents","ceiling","cents","max","Math","min","Array","from","char","charCodeAt","join","trim","CHECKOUT_CREDIT_METADATA_KEY","encodeCheckoutCreditMetadata","held","holdKey","amountCents","JSON","stringify","decodeCheckoutCreditMetadata","metadata","raw","undefined","parse","isArray","splitCheckoutCreditProviderId","match","exec","checkoutCreditSold","parts","appliedAs"],"mappings":"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"}
@@ -119,6 +119,16 @@ export interface PluginShippingAddressCheck {
119
119
  suggested?: PluginShippingAddress;
120
120
  /** Why it is not deliverable, in the carrier's words. */
121
121
  messages: string[];
122
+ /**
123
+ * Where the address is on a map, when the provider placed it. Optional:
124
+ * only some providers return it, and only for addresses they verified. A
125
+ * caller measuring distance (local delivery radius zones) treats its
126
+ * absence as "cannot measure", never as zero.
127
+ */
128
+ coordinates?: {
129
+ lat: number;
130
+ lng: number;
131
+ };
122
132
  }
123
133
  export interface PluginShippingRateQuoter {
124
134
  /** Whether quotes can be asked for this site now: configured and switched on. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-shipping-rates.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Live parcel rates, answered by the one plugin that talks to carriers\n * (AGL-3612).\n *\n * A plugin that sells goods prices delivery from its own table until the\n * merchant asks for live rates; then it needs a carrier's quote for an\n * address and a parcel, and it must not learn which carrier network, which\n * account or which vendor answers. A plugin that buys labels registers here;\n * a plugin that sells asks here, and neither imports the other.\n *\n * ## The shape is a parcel and an address, nothing more\n *\n * Every type below is postal: where it goes, what it weighs, how big it is,\n * and what it is worth for insurance and customs. Nothing names a catalog, an\n * order or a store, so a booking plugin shipping a rental kit asks the same\n * question in the same words.\n *\n * ## Nobody home is `null`, never a refusal\n *\n * Unlike the tax profile, an absent quoter is a perfectly good answer: the\n * seller falls back to its own rates, which is what it did before anyone\n * registered. {@link pluginShippingRateQuoter} answers `null`, and a quoter\n * whose deployment is not configured answers `available() === false`, which\n * a caller must read the same way.\n *\n * ## A quote is advisory until a label is bought\n *\n * `amountCents` is what the carrier quoted the platform for that parcel at\n * that moment, BEFORE any markup or handling the seller adds. The seller\n * decides what the shopper pays; the quoter never does.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-shipping-rates`); it is not in the\n * barrel.\n */\n\n/** A postal address. `country` is ISO-3166 alpha-2; everything else is optional. */\nexport interface PluginShippingAddress {\n name?: string\n company?: string\n line1?: string\n line2?: string\n city?: string\n /** State, province or region code, as the destination writes it. */\n state?: string\n postalCode?: string\n country: string\n phone?: string\n email?: string\n /** Whether the address is a home, when the caller knows; carriers price it. */\n residential?: boolean\n}\n\n/** One parcel. Metric throughout; an adapter converts to its vendor's units. */\nexport interface PluginShippingParcel {\n weightGrams: number\n lengthCm?: number\n widthCm?: number\n heightCm?: number\n}\n\n/** What a seller asks: one shipment's rates to one address. */\nexport interface PluginShippingQuoteRequest {\n /** The site the shipment leaves from; the quoter resolves its ship-from address. */\n hostId: string\n to: PluginShippingAddress\n parcels: PluginShippingParcel[]\n /** ISO-4217, lower case, of the amounts the caller will charge in. */\n currency: string\n /** The goods' value, for insurance and customs; 0 when unknown. */\n valueCents: number\n /**\n * Service keys the seller offers (`'usps:priority'`), as\n * {@link PluginShippingRateQuoter.listServices} names them. Empty or\n * absent means every service the quoter can price.\n */\n services?: string[]\n /** Aborted when the caller stops waiting; a quoter passes it to its fetches. */\n signal?: AbortSignal\n}\n\n/** One carrier service's price for the shipment. */\nexport interface PluginShippingQuote {\n /** Stable across quotes: `carrier:service`, the key a seller stores. */\n serviceKey: string\n carrier: string\n service: string\n /** What a shopper reads: `'USPS Priority Mail'`. */\n label: string\n /** The carrier's price to the platform, before any markup or handling. */\n amountCents: number\n currency: string\n /** Transit estimate in business days, when the carrier gives one. */\n estimatedDays?: number\n}\n\n/** A service a seller may choose to offer. */\nexport interface PluginShippingService {\n serviceKey: string\n carrier: string\n label: string\n}\n\n/** What address validation answers. */\nexport interface PluginShippingAddressCheck {\n /** `valid` deliverable as written; `corrected` deliverable as `suggested`; `invalid` not deliverable. */\n verdict: 'valid' | 'corrected' | 'invalid' | 'unknown'\n /** The address the carrier would deliver to, when it differs. */\n suggested?: PluginShippingAddress\n /** Why it is not deliverable, in the carrier's words. */\n messages: string[]\n}\n\nexport interface PluginShippingRateQuoter {\n /** Whether quotes can be asked for this site now: configured and switched on. */\n available(hostId: string): Promise<boolean>\n /**\n * The rates. Throws on a provider failure; a caller that cannot wait\n * aborts `signal` and falls back to its own rates.\n */\n quote(request: PluginShippingQuoteRequest): Promise<PluginShippingQuote[]>\n /** The services a seller can pick from for this site. */\n listServices(hostId: string): Promise<PluginShippingService[]>\n /** Whether an address is deliverable, and the carrier's correction. */\n validateAddress?(\n hostId: string,\n address: PluginShippingAddress,\n ): Promise<PluginShippingAddressCheck>\n}\n\nexport const PLUGIN_SHIPPING_RATE_QUOTER =\n definePluginServiceContract<PluginShippingRateQuoter>('core.shipping-rate-quoter', {\n multiple: false,\n })\n\n/** Registers the plugin that quotes carrier rates. A second plugin is refused. */\nexport function registerPluginShippingRateQuoter(\n quoter: PluginShippingRateQuoter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_SHIPPING_RATE_QUOTER, quoter, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The quoter, or `null` when no plugin registered one. */\nexport function pluginShippingRateQuoter(): PluginShippingRateQuoter | null {\n return resolvePluginServices(PLUGIN_SHIPPING_RATE_QUOTER)[0]?.impl ?? null\n}\n\n/**\n * Asks the quoter and gives up after `timeoutMs`: `null` when nobody is\n * registered, the site is not available, the provider failed, or the wait\n * ran out. Never throws. The caller's fallback is its own rates, so every\n * failure reads the same.\n */\nexport async function quotePluginShippingRates(\n request: Omit<PluginShippingQuoteRequest, 'signal'>,\n options: { timeoutMs: number },\n): Promise<PluginShippingQuote[] | null> {\n const quoter = pluginShippingRateQuoter()\n if (!quoter) return null\n const controller = new AbortController()\n let timer: ReturnType<typeof setTimeout> | undefined\n const deadline = new Promise<null>((resolve) => {\n timer = setTimeout(() => {\n controller.abort()\n resolve(null)\n }, Math.max(0, options.timeoutMs))\n })\n try {\n return await Promise.race([\n (async (): Promise<PluginShippingQuote[] | null> => {\n if (!(await quoter.available(request.hostId))) return null\n return quoter.quote({ ...request, signal: controller.signal })\n })().catch((): null => null),\n deadline,\n ])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_SHIPPING_RATE_QUOTER","multiple","registerPluginShippingRateQuoter","quoter","options","pluginId","pluginShippingRateQuoter","impl","quotePluginShippingRates","request","controller","AbortController","timer","deadline","Promise","resolve","setTimeout","abort","Math","max","timeoutMs","race","available","hostId","quote","signal","catch","clearTimeout"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAoI1B,OAAO,MAAMC,8BACXH,4BAAsD,6BAA6B;IACjFI,UAAU;AACZ,GAAE;AAEJ,gFAAgF,GAChF,OAAO,SAASC,iCACdC,MAAgC,EAChCC,OAA+B;IAE/BN,sBAAsBE,6BAA6BG,QAAQ,aACrDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,yDAAyD,GACzD,OAAO,SAASC;;QACPP;IAAP,gBAAOA,0BAAAA,sBAAsBC,4BAA4B,CAAC,EAAE,qBAArDD,wBAAuDQ,IAAI,mBAAI;AACxE;AAEA;;;;;CAKC,GACD,OAAO,eAAeC,yBACpBC,OAAmD,EACnDL,OAA8B;IAE9B,MAAMD,SAASG;IACf,IAAI,CAACH,QAAQ,OAAO;IACpB,MAAMO,aAAa,IAAIC;IACvB,IAAIC;IACJ,MAAMC,WAAW,IAAIC,QAAc,CAACC;QAClCH,QAAQI,WAAW;YACjBN,WAAWO,KAAK;YAChBF,QAAQ;QACV,GAAGG,KAAKC,GAAG,CAAC,GAAGf,QAAQgB,SAAS;IAClC;IACA,IAAI;QACF,OAAO,MAAMN,QAAQO,IAAI,CAAC;YACvB,CAAA;gBACC,IAAI,CAAE,MAAMlB,OAAOmB,SAAS,CAACb,QAAQc,MAAM,GAAI,OAAO;gBACtD,OAAOpB,OAAOqB,KAAK,CAAC,aAAKf;oBAASgB,QAAQf,WAAWe,MAAM;;YAC7D,CAAA,IAAKC,KAAK,CAAC,IAAY;YACvBb;SACD;IACH,SAAU;QACR,IAAID,OAAOe,aAAaf;IAC1B;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-shipping-rates.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Live parcel rates, answered by the one plugin that talks to carriers\n * (AGL-3612).\n *\n * A plugin that sells goods prices delivery from its own table until the\n * merchant asks for live rates; then it needs a carrier's quote for an\n * address and a parcel, and it must not learn which carrier network, which\n * account or which vendor answers. A plugin that buys labels registers here;\n * a plugin that sells asks here, and neither imports the other.\n *\n * ## The shape is a parcel and an address, nothing more\n *\n * Every type below is postal: where it goes, what it weighs, how big it is,\n * and what it is worth for insurance and customs. Nothing names a catalog, an\n * order or a store, so a booking plugin shipping a rental kit asks the same\n * question in the same words.\n *\n * ## Nobody home is `null`, never a refusal\n *\n * Unlike the tax profile, an absent quoter is a perfectly good answer: the\n * seller falls back to its own rates, which is what it did before anyone\n * registered. {@link pluginShippingRateQuoter} answers `null`, and a quoter\n * whose deployment is not configured answers `available() === false`, which\n * a caller must read the same way.\n *\n * ## A quote is advisory until a label is bought\n *\n * `amountCents` is what the carrier quoted the platform for that parcel at\n * that moment, BEFORE any markup or handling the seller adds. The seller\n * decides what the shopper pays; the quoter never does.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-shipping-rates`); it is not in the\n * barrel.\n */\n\n/** A postal address. `country` is ISO-3166 alpha-2; everything else is optional. */\nexport interface PluginShippingAddress {\n name?: string\n company?: string\n line1?: string\n line2?: string\n city?: string\n /** State, province or region code, as the destination writes it. */\n state?: string\n postalCode?: string\n country: string\n phone?: string\n email?: string\n /** Whether the address is a home, when the caller knows; carriers price it. */\n residential?: boolean\n}\n\n/** One parcel. Metric throughout; an adapter converts to its vendor's units. */\nexport interface PluginShippingParcel {\n weightGrams: number\n lengthCm?: number\n widthCm?: number\n heightCm?: number\n}\n\n/** What a seller asks: one shipment's rates to one address. */\nexport interface PluginShippingQuoteRequest {\n /** The site the shipment leaves from; the quoter resolves its ship-from address. */\n hostId: string\n to: PluginShippingAddress\n parcels: PluginShippingParcel[]\n /** ISO-4217, lower case, of the amounts the caller will charge in. */\n currency: string\n /** The goods' value, for insurance and customs; 0 when unknown. */\n valueCents: number\n /**\n * Service keys the seller offers (`'usps:priority'`), as\n * {@link PluginShippingRateQuoter.listServices} names them. Empty or\n * absent means every service the quoter can price.\n */\n services?: string[]\n /** Aborted when the caller stops waiting; a quoter passes it to its fetches. */\n signal?: AbortSignal\n}\n\n/** One carrier service's price for the shipment. */\nexport interface PluginShippingQuote {\n /** Stable across quotes: `carrier:service`, the key a seller stores. */\n serviceKey: string\n carrier: string\n service: string\n /** What a shopper reads: `'USPS Priority Mail'`. */\n label: string\n /** The carrier's price to the platform, before any markup or handling. */\n amountCents: number\n currency: string\n /** Transit estimate in business days, when the carrier gives one. */\n estimatedDays?: number\n}\n\n/** A service a seller may choose to offer. */\nexport interface PluginShippingService {\n serviceKey: string\n carrier: string\n label: string\n}\n\n/** What address validation answers. */\nexport interface PluginShippingAddressCheck {\n /** `valid` deliverable as written; `corrected` deliverable as `suggested`; `invalid` not deliverable. */\n verdict: 'valid' | 'corrected' | 'invalid' | 'unknown'\n /** The address the carrier would deliver to, when it differs. */\n suggested?: PluginShippingAddress\n /** Why it is not deliverable, in the carrier's words. */\n messages: string[]\n /**\n * Where the address is on a map, when the provider placed it. Optional:\n * only some providers return it, and only for addresses they verified. A\n * caller measuring distance (local delivery radius zones) treats its\n * absence as \"cannot measure\", never as zero.\n */\n coordinates?: { lat: number; lng: number }\n}\n\nexport interface PluginShippingRateQuoter {\n /** Whether quotes can be asked for this site now: configured and switched on. */\n available(hostId: string): Promise<boolean>\n /**\n * The rates. Throws on a provider failure; a caller that cannot wait\n * aborts `signal` and falls back to its own rates.\n */\n quote(request: PluginShippingQuoteRequest): Promise<PluginShippingQuote[]>\n /** The services a seller can pick from for this site. */\n listServices(hostId: string): Promise<PluginShippingService[]>\n /** Whether an address is deliverable, and the carrier's correction. */\n validateAddress?(\n hostId: string,\n address: PluginShippingAddress,\n ): Promise<PluginShippingAddressCheck>\n}\n\nexport const PLUGIN_SHIPPING_RATE_QUOTER =\n definePluginServiceContract<PluginShippingRateQuoter>('core.shipping-rate-quoter', {\n multiple: false,\n })\n\n/** Registers the plugin that quotes carrier rates. A second plugin is refused. */\nexport function registerPluginShippingRateQuoter(\n quoter: PluginShippingRateQuoter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_SHIPPING_RATE_QUOTER, quoter, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The quoter, or `null` when no plugin registered one. */\nexport function pluginShippingRateQuoter(): PluginShippingRateQuoter | null {\n return resolvePluginServices(PLUGIN_SHIPPING_RATE_QUOTER)[0]?.impl ?? null\n}\n\n/**\n * Asks the quoter and gives up after `timeoutMs`: `null` when nobody is\n * registered, the site is not available, the provider failed, or the wait\n * ran out. Never throws. The caller's fallback is its own rates, so every\n * failure reads the same.\n */\nexport async function quotePluginShippingRates(\n request: Omit<PluginShippingQuoteRequest, 'signal'>,\n options: { timeoutMs: number },\n): Promise<PluginShippingQuote[] | null> {\n const quoter = pluginShippingRateQuoter()\n if (!quoter) return null\n const controller = new AbortController()\n let timer: ReturnType<typeof setTimeout> | undefined\n const deadline = new Promise<null>((resolve) => {\n timer = setTimeout(() => {\n controller.abort()\n resolve(null)\n }, Math.max(0, options.timeoutMs))\n })\n try {\n return await Promise.race([\n (async (): Promise<PluginShippingQuote[] | null> => {\n if (!(await quoter.available(request.hostId))) return null\n return quoter.quote({ ...request, signal: controller.signal })\n })().catch((): null => null),\n deadline,\n ])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_SHIPPING_RATE_QUOTER","multiple","registerPluginShippingRateQuoter","quoter","options","pluginId","pluginShippingRateQuoter","impl","quotePluginShippingRates","request","controller","AbortController","timer","deadline","Promise","resolve","setTimeout","abort","Math","max","timeoutMs","race","available","hostId","quote","signal","catch","clearTimeout"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA2I1B,OAAO,MAAMC,8BACXH,4BAAsD,6BAA6B;IACjFI,UAAU;AACZ,GAAE;AAEJ,gFAAgF,GAChF,OAAO,SAASC,iCACdC,MAAgC,EAChCC,OAA+B;IAE/BN,sBAAsBE,6BAA6BG,QAAQ,aACrDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,yDAAyD,GACzD,OAAO,SAASC;;QACPP;IAAP,gBAAOA,0BAAAA,sBAAsBC,4BAA4B,CAAC,EAAE,qBAArDD,wBAAuDQ,IAAI,mBAAI;AACxE;AAEA;;;;;CAKC,GACD,OAAO,eAAeC,yBACpBC,OAAmD,EACnDL,OAA8B;IAE9B,MAAMD,SAASG;IACf,IAAI,CAACH,QAAQ,OAAO;IACpB,MAAMO,aAAa,IAAIC;IACvB,IAAIC;IACJ,MAAMC,WAAW,IAAIC,QAAc,CAACC;QAClCH,QAAQI,WAAW;YACjBN,WAAWO,KAAK;YAChBF,QAAQ;QACV,GAAGG,KAAKC,GAAG,CAAC,GAAGf,QAAQgB,SAAS;IAClC;IACA,IAAI;QACF,OAAO,MAAMN,QAAQO,IAAI,CAAC;YACvB,CAAA;gBACC,IAAI,CAAE,MAAMlB,OAAOmB,SAAS,CAACb,QAAQc,MAAM,GAAI,OAAO;gBACtD,OAAOpB,OAAOqB,KAAK,CAAC,aAAKf;oBAASgB,QAAQf,WAAWe,MAAM;;YAC7D,CAAA,IAAKC,KAAK,CAAC,IAAY;YACvBb;SACD;IACH,SAAU;QACR,IAAID,OAAOe,aAAaf;IAC1B;AACF"}