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

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 (150) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-acquisition.d.ts +15 -2
  3. package/src/lib/app-utils/account-acquisition.js +20 -1
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/artifact-list-queries.d.ts +35 -0
  6. package/src/lib/app-utils/artifact-list-queries.js +203 -0
  7. package/src/lib/app-utils/artifact-list-queries.js.map +1 -0
  8. package/src/lib/app-utils/business-profile.d.ts +171 -0
  9. package/src/lib/app-utils/business-profile.js +277 -0
  10. package/src/lib/app-utils/business-profile.js.map +1 -0
  11. package/src/lib/app-utils/child-contract.d.ts +4 -1
  12. package/src/lib/app-utils/child-contract.js +5 -2
  13. package/src/lib/app-utils/child-contract.js.map +1 -1
  14. package/src/lib/app-utils/console-routes.d.ts +5 -0
  15. package/src/lib/app-utils/console-routes.js +1 -0
  16. package/src/lib/app-utils/console-routes.js.map +1 -1
  17. package/src/lib/app-utils/content-schema-type.d.ts +9 -0
  18. package/src/lib/app-utils/content-schema-type.js +4 -0
  19. package/src/lib/app-utils/content-schema-type.js.map +1 -1
  20. package/src/lib/app-utils/crm.d.ts +14 -1
  21. package/src/lib/app-utils/crm.js +19 -2
  22. package/src/lib/app-utils/crm.js.map +1 -1
  23. package/src/lib/app-utils/docs-help.generated.d.ts +119 -5
  24. package/src/lib/app-utils/docs-help.generated.js +290 -2
  25. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  26. package/src/lib/app-utils/docs-index.generated.js +697 -32
  27. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  28. package/src/lib/app-utils/entry-list-declaration.d.ts +33 -0
  29. package/src/lib/app-utils/entry-list-declaration.js +172 -0
  30. package/src/lib/app-utils/entry-list-declaration.js.map +1 -0
  31. package/src/lib/app-utils/first-version-seed.d.ts +19 -0
  32. package/src/lib/app-utils/first-version-seed.js +53 -0
  33. package/src/lib/app-utils/first-version-seed.js.map +1 -0
  34. package/src/lib/app-utils/host-status.d.ts +85 -0
  35. package/src/lib/app-utils/host-status.js +115 -0
  36. package/src/lib/app-utils/host-status.js.map +1 -0
  37. package/src/lib/app-utils/local-business.d.ts +8 -0
  38. package/src/lib/app-utils/local-business.js +4 -0
  39. package/src/lib/app-utils/local-business.js.map +1 -1
  40. package/src/lib/app-utils/lockdown.js +1 -1
  41. package/src/lib/app-utils/lockdown.js.map +1 -1
  42. package/src/lib/app-utils/media-filter.d.ts +136 -0
  43. package/src/lib/app-utils/media-filter.js +400 -0
  44. package/src/lib/app-utils/media-filter.js.map +1 -0
  45. package/src/lib/app-utils/mobile-push.d.ts +98 -0
  46. package/src/lib/app-utils/mobile-push.js +97 -0
  47. package/src/lib/app-utils/mobile-push.js.map +1 -0
  48. package/src/lib/app-utils/notification-push.d.ts +38 -0
  49. package/src/lib/app-utils/notification-push.js +54 -0
  50. package/src/lib/app-utils/notification-push.js.map +1 -0
  51. package/src/lib/app-utils/notifications.d.ts +7 -0
  52. package/src/lib/app-utils/notifications.js.map +1 -1
  53. package/src/lib/app-utils/organizations.js +5 -2
  54. package/src/lib/app-utils/organizations.js.map +1 -1
  55. package/src/lib/app-utils/page-markdown.js +3 -0
  56. package/src/lib/app-utils/page-markdown.js.map +1 -1
  57. package/src/lib/app-utils/plugin-host-events.generated.d.ts +1 -1
  58. package/src/lib/app-utils/plugin-host-events.generated.js +149 -0
  59. package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
  60. package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
  61. package/src/lib/app-utils/plugin-release-flags.generated.js +7 -0
  62. package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
  63. package/src/lib/app-utils/realm-host-surface.generated.js +3 -0
  64. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  65. package/src/lib/app-utils/release-flags.js +6 -2
  66. package/src/lib/app-utils/release-flags.js.map +1 -1
  67. package/src/lib/app-utils/scope-tokens.d.ts +16 -1
  68. package/src/lib/app-utils/scope-tokens.js +15 -1
  69. package/src/lib/app-utils/scope-tokens.js.map +1 -1
  70. package/src/lib/app-utils/site-list-query.d.ts +47 -0
  71. package/src/lib/app-utils/site-list-query.js +142 -0
  72. package/src/lib/app-utils/site-list-query.js.map +1 -0
  73. package/src/lib/app-utils/site-wide-outbox.d.ts +95 -0
  74. package/src/lib/app-utils/site-wide-outbox.js +117 -0
  75. package/src/lib/app-utils/site-wide-outbox.js.map +1 -0
  76. package/src/lib/app-utils/transfer-launcher-context.d.ts +6 -0
  77. package/src/lib/app-utils/transfer-launcher-context.js.map +1 -1
  78. package/src/lib/app-utils/upload-inspection.js +7 -0
  79. package/src/lib/app-utils/upload-inspection.js.map +1 -1
  80. package/src/lib/app-utils/webhook-delivery.js +4 -1
  81. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  82. package/src/lib/foundation/definitions/org-billing.types.d.ts +8 -0
  83. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  84. package/src/lib/foundation/definitions/organization.types.d.ts +18 -7
  85. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  86. package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +4 -1
  87. package/src/lib/foundation/definitions/write-deny-coverage.util.js +12 -2
  88. package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
  89. package/src/lib/plugin-manager/enabled-plugins.js +4 -2
  90. package/src/lib/plugin-manager/enabled-plugins.js.map +1 -1
  91. package/src/lib/plugin-manager/feature-plugins.d.ts +107 -0
  92. package/src/lib/plugin-manager/feature-plugins.js +52 -0
  93. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  94. package/src/lib/plugin-manager/first-party-plugins.generated.js +326 -2
  95. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  96. package/src/lib/plugin-manager/plugin-channel-orders.d.ts +262 -0
  97. package/src/lib/plugin-manager/plugin-channel-orders.js +32 -0
  98. package/src/lib/plugin-manager/plugin-channel-orders.js.map +1 -0
  99. package/src/lib/plugin-manager/plugin-checkout-extras.d.ts +168 -0
  100. package/src/lib/plugin-manager/plugin-checkout-extras.js +172 -0
  101. package/src/lib/plugin-manager/plugin-checkout-extras.js.map +1 -0
  102. package/src/lib/plugin-manager/plugin-contributions.d.ts +7 -0
  103. package/src/lib/plugin-manager/plugin-contributions.js +1 -1
  104. package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
  105. package/src/lib/plugin-manager/plugin-domain-events.d.ts +138 -0
  106. package/src/lib/plugin-manager/plugin-domain-events.js +148 -0
  107. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -0
  108. package/src/lib/plugin-manager/plugin-events.d.ts +30 -0
  109. package/src/lib/plugin-manager/plugin-events.js +4 -0
  110. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  111. package/src/lib/plugin-manager/plugin-fulfillment-providers.d.ts +101 -0
  112. package/src/lib/plugin-manager/plugin-fulfillment-providers.js +83 -0
  113. package/src/lib/plugin-manager/plugin-fulfillment-providers.js.map +1 -0
  114. package/src/lib/plugin-manager/plugin-permissions.js +21 -5
  115. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  116. package/src/lib/plugin-manager/plugin-person-records.d.ts +90 -0
  117. package/src/lib/plugin-manager/plugin-person-records.js +26 -0
  118. package/src/lib/plugin-manager/plugin-person-records.js.map +1 -1
  119. package/src/lib/plugin-manager/plugin-product-catalog.d.ts +209 -0
  120. package/src/lib/plugin-manager/plugin-product-catalog.js +43 -0
  121. package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -0
  122. package/src/lib/plugin-manager/plugin-product-writer.d.ts +166 -0
  123. package/src/lib/plugin-manager/plugin-product-writer.js +31 -0
  124. package/src/lib/plugin-manager/plugin-product-writer.js.map +1 -0
  125. package/src/lib/plugin-manager/plugin-shipment-records.d.ts +210 -0
  126. package/src/lib/plugin-manager/plugin-shipment-records.js +63 -0
  127. package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -0
  128. package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +151 -0
  129. package/src/lib/plugin-manager/plugin-shipping-rates.js +62 -0
  130. package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -0
  131. package/src/lib/plugin-manager/plugin-sms-messaging.d.ts +103 -0
  132. package/src/lib/plugin-manager/plugin-sms-messaging.js +39 -0
  133. package/src/lib/plugin-manager/plugin-sms-messaging.js.map +1 -0
  134. package/src/lib/plugin-manager/plugin-stock-levels.d.ts +81 -0
  135. package/src/lib/plugin-manager/plugin-stock-levels.js +32 -0
  136. package/src/lib/plugin-manager/plugin-stock-levels.js.map +1 -0
  137. package/src/lib/plugin-manager/plugin-tax-profile.d.ts +154 -0
  138. package/src/lib/plugin-manager/plugin-tax-profile.js +56 -0
  139. package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
  140. package/src/lib/plugin-manager/plugin-theme-font-catalog.d.ts +59 -0
  141. package/src/lib/plugin-manager/plugin-theme-font-catalog.js +40 -0
  142. package/src/lib/plugin-manager/plugin-theme-font-catalog.js.map +1 -0
  143. package/src/lib/plugin-manager/plugin-theme-presets.d.ts +40 -0
  144. package/src/lib/plugin-manager/plugin-theme-presets.js +44 -0
  145. package/src/lib/plugin-manager/plugin-theme-presets.js.map +1 -0
  146. package/src/lib/plugin-manager/plugin-tracking-pages.d.ts +55 -0
  147. package/src/lib/plugin-manager/plugin-tracking-pages.js +76 -0
  148. package/src/lib/plugin-manager/plugin-tracking-pages.js.map +1 -0
  149. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +3 -0
  150. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
@@ -0,0 +1,262 @@
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
+ * Orders a store took somewhere else (AGL-3638): a marketplace, a social
19
+ * shop, any outside channel that sells the store's goods and is paid there.
20
+ *
21
+ * Such an order must become a REAL order of the store — numbered with the
22
+ * rest, shown in the same list, shipped from the same dialog, announced to
23
+ * the same subscribers — and its units must leave the same shelf every other
24
+ * channel sells from, in the same write, or two channels sell the last unit
25
+ * twice. The plugin that brings the order in must not write the seller's
26
+ * orders or products itself, so the seller registers
27
+ * {@link PluginChannelOrders} and records each order under its own rules: the
28
+ * order number, the stock decrement and its ledger row, the oversell alert,
29
+ * the order events.
30
+ *
31
+ * ## Once per outside order
32
+ *
33
+ * An import is keyed by `channel.id` and `externalOrderId`. The same pair a
34
+ * second time — a retried run, a page read twice — answers `already` with
35
+ * the record the first import made, and nothing moves.
36
+ *
37
+ * ## The channel was paid, not the store
38
+ *
39
+ * The buyer paid the channel, and the channel pays the merchant out. No
40
+ * money moves through the seller for the order; its refunds and returns
41
+ * happen on the channel. What the channel charged the merchant for the sale
42
+ * (`fees`) is RECORDED on the order for the merchant's books and never
43
+ * charged or collected by anyone here.
44
+ *
45
+ * Money is integer minor units (cents for USD) in `currency`. A seller may
46
+ * refuse an order in a currency its store does not sell in.
47
+ *
48
+ * Import this module by its own subpath
49
+ * (`@aglyn/aglyn/plugin-manager/plugin-channel-orders`); it is not in the
50
+ * barrel.
51
+ */
52
+ /** One line of an outside order. */
53
+ export interface PluginChannelOrderLine {
54
+ /** The channel's id for the line, which a shipment back to it names. */
55
+ externalLineId: string;
56
+ /** The seller SKU the channel sold it under; `null` when it gave none. */
57
+ sku: string | null;
58
+ /**
59
+ * The product and configuration, when the importer already knows them
60
+ * (it listed them from `core.product-catalog`). Otherwise the seller
61
+ * matches `sku`.
62
+ */
63
+ productId?: string | null;
64
+ variantId?: string | null;
65
+ /** The channel's name for the item, kept on the order. */
66
+ name: string;
67
+ quantity: number;
68
+ /** Per unit, before tax. */
69
+ unitPriceCents: number;
70
+ }
71
+ /** The address the order ships to. */
72
+ export interface PluginChannelOrderAddress {
73
+ name?: string | null;
74
+ line1?: string | null;
75
+ line2?: string | null;
76
+ city?: string | null;
77
+ state?: string | null;
78
+ postalCode?: string | null;
79
+ /** ISO 3166-1 alpha-2, upper case. */
80
+ country?: string | null;
81
+ phone?: string | null;
82
+ }
83
+ /** A channel's charge on a sale, recorded for the merchant and never collected here. */
84
+ export interface PluginChannelOrderFee {
85
+ label: string;
86
+ amountCents: number;
87
+ }
88
+ export interface PluginChannelOrder {
89
+ hostId: string;
90
+ /** Stable id and display name, as the importer names its channel. */
91
+ channel: {
92
+ id: string;
93
+ label: string;
94
+ };
95
+ externalOrderId: string;
96
+ /** What the merchant sees on the channel. */
97
+ externalRef: string;
98
+ placedAtMs: number;
99
+ /** ISO 4217. */
100
+ currency: string;
101
+ lines: PluginChannelOrderLine[];
102
+ shippingCents: number;
103
+ /** Tax the channel collected; the channel, not the store, remits it. */
104
+ taxCents: number;
105
+ discountCents: number;
106
+ /** What the buyer paid the channel in all. */
107
+ totalCents: number;
108
+ /** The channel's charges on the sale; `null` while the channel has not said. */
109
+ fees: PluginChannelOrderFee[] | null;
110
+ customerName: string | null;
111
+ shippingAddress: PluginChannelOrderAddress | null;
112
+ /** Placed in the channel's sandbox: recorded, but never counted as revenue. */
113
+ testMode: boolean;
114
+ /**
115
+ * How the goods leave the store (AGL-3644): `ship`, the default, by a
116
+ * carrier to `shippingAddress`; `courier`, handed over the counter to the
117
+ * channel's own courier (a delivery app), so nothing is shipped from here
118
+ * and the seller says so to the merchant.
119
+ */
120
+ handoff?: 'ship' | 'courier';
121
+ }
122
+ /** A line the store's stock could not cover in full: the channel sold more than the shelf held. */
123
+ export interface PluginChannelOrderShortfall {
124
+ lineIndex: number;
125
+ sku: string | null;
126
+ /** Units sold beyond what the shelf held. */
127
+ short: number;
128
+ }
129
+ export type PluginChannelOrderOutcome = {
130
+ outcome: 'created';
131
+ recordId: string;
132
+ /** What a person calls it: `#1042`. */
133
+ displayRef: string;
134
+ /** Which order line each channel line became. */
135
+ lines: Array<{
136
+ lineIndex: number;
137
+ externalLineId: string;
138
+ }>;
139
+ /** Lines whose units the shelf could not cover. The order is recorded all the same. */
140
+ shortfalls: PluginChannelOrderShortfall[];
141
+ /** Lines matching no product, recorded by name with no stock moved. */
142
+ unmatched: number[];
143
+ } | {
144
+ outcome: 'already';
145
+ recordId: string;
146
+ displayRef: string;
147
+ lines: Array<{
148
+ lineIndex: number;
149
+ externalLineId: string;
150
+ }>;
151
+ }
152
+ /** The seller would not record it; `reason` says why, for the merchant. */
153
+ | {
154
+ outcome: 'refused';
155
+ reason: string;
156
+ };
157
+ export interface PluginChannelOrderCancel {
158
+ hostId: string;
159
+ recordId: string;
160
+ /** Shown on the order's timeline. */
161
+ reason: string;
162
+ }
163
+ export type PluginChannelOrderCancelOutcome =
164
+ /** Cancelled, with this many units back on the shelf. */
165
+ {
166
+ outcome: 'cancelled';
167
+ restockedUnits: number;
168
+ } | {
169
+ outcome: 'already';
170
+ }
171
+ /** The seller's rules refused (it has shipped, say); `status` is the seller's word for where it stands. */
172
+ | {
173
+ outcome: 'not_cancellable';
174
+ status: string;
175
+ } | {
176
+ outcome: 'no_such_record';
177
+ };
178
+ /** The channel's courier took the whole order (AGL-3644): it is fulfilled, with nothing shipped from here. */
179
+ export interface PluginChannelOrderHandoff {
180
+ hostId: string;
181
+ recordId: string;
182
+ /** Shown on the order's timeline, e.g. `Picked up by the DoorDash courier`. */
183
+ note: string;
184
+ }
185
+ export type PluginChannelOrderHandoffOutcome = {
186
+ outcome: 'completed';
187
+ } | {
188
+ outcome: 'already';
189
+ }
190
+ /** The seller's rules refused (it was cancelled, say); `status` is the seller's word for where it stands. */
191
+ | {
192
+ outcome: 'not_completable';
193
+ status: string;
194
+ } | {
195
+ outcome: 'no_such_record';
196
+ };
197
+ /**
198
+ * Money the channel gave back to the buyer of an order it sent (AGL-3644):
199
+ * a refund, or the channel's adjustment of the order — an item removed, a
200
+ * quantity lowered. No money moves here: the channel refunded its buyer and
201
+ * takes it from the merchant's payout, so the seller RECORDS it on the order.
202
+ *
203
+ * `restock` names the units that never left the store (an item taken off the
204
+ * order before it was handed over); the seller puts back at most what the
205
+ * order's own sale took. Empty for a refund of goods already gone.
206
+ */
207
+ export interface PluginChannelOrderRefund {
208
+ hostId: string;
209
+ recordId: string;
210
+ /** The channel's id for this refund or adjustment: the same id a second time records nothing. */
211
+ refundId: string;
212
+ /** Minor units in the order's currency; `0` for an adjustment that only moves stock. */
213
+ amountCents: number;
214
+ /** Shown on the order's timeline. */
215
+ reason: string;
216
+ restock: Array<{
217
+ lineIndex: number;
218
+ quantity: number;
219
+ }>;
220
+ }
221
+ export type PluginChannelOrderRefundOutcome =
222
+ /** Recorded: `refundedCents` is the order's refunded total now, `restockedUnits` what went back on the shelf. */
223
+ {
224
+ outcome: 'recorded';
225
+ refundedCents: number;
226
+ restockedUnits: number;
227
+ } | {
228
+ outcome: 'already';
229
+ }
230
+ /** The seller would not record it; `reason` says why, for the merchant. */
231
+ | {
232
+ outcome: 'refused';
233
+ reason: string;
234
+ } | {
235
+ outcome: 'no_such_record';
236
+ };
237
+ export interface PluginChannelOrders {
238
+ /** Records an outside order once, with its units off the shelf in the same write. */
239
+ importOrder(order: PluginChannelOrder): Promise<PluginChannelOrderOutcome>;
240
+ /** The channel canceled an order it had sent: cancel it here and put its units back. */
241
+ cancelOrder(request: PluginChannelOrderCancel): Promise<PluginChannelOrderCancelOutcome>;
242
+ /** The channel's charges on a sale, once it says what they were. */
243
+ recordFees(request: {
244
+ hostId: string;
245
+ recordId: string;
246
+ fees: PluginChannelOrderFee[];
247
+ }): Promise<'recorded' | 'no_such_record'>;
248
+ /**
249
+ * The channel's courier took the order (AGL-3644). Optional: a seller that
250
+ * cannot record a hand-off leaves it out, and the importer leaves the order
251
+ * for the merchant to fulfill.
252
+ */
253
+ completeOrder?(request: PluginChannelOrderHandoff): Promise<PluginChannelOrderHandoffOutcome>;
254
+ /** The channel refunded or adjusted an order it sent (AGL-3644). Optional, as `completeOrder`. */
255
+ recordRefund?(request: PluginChannelOrderRefund): Promise<PluginChannelOrderRefundOutcome>;
256
+ }
257
+ /** Registers the seller. A second plugin is refused naming both. */
258
+ export declare function registerPluginChannelOrders(orders: PluginChannelOrders, options?: {
259
+ pluginId?: string;
260
+ }): void;
261
+ /** The seller, or `null` when no plugin registered one. */
262
+ export declare function pluginChannelOrders(): PluginChannelOrders | null;
@@ -0,0 +1,32 @@
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 { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
18
+ const PLUGIN_CHANNEL_ORDERS = definePluginServiceContract('core.channel-orders', {
19
+ multiple: false
20
+ });
21
+ /** Registers the seller. A second plugin is refused naming both. */ export function registerPluginChannelOrders(orders, options) {
22
+ registerPluginService(PLUGIN_CHANNEL_ORDERS, orders, _extends({}, (options == null ? void 0 : options.pluginId) ? {
23
+ pluginId: options.pluginId
24
+ } : {}));
25
+ }
26
+ /** The seller, or `null` when no plugin registered one. */ export function pluginChannelOrders() {
27
+ var _ref;
28
+ var _resolvePluginServices_;
29
+ return (_ref = (_resolvePluginServices_ = resolvePluginServices(PLUGIN_CHANNEL_ORDERS)[0]) == null ? void 0 : _resolvePluginServices_.impl) != null ? _ref : null;
30
+ }
31
+
32
+ //# sourceMappingURL=plugin-channel-orders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-channel-orders.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 * Orders a store took somewhere else (AGL-3638): a marketplace, a social\n * shop, any outside channel that sells the store's goods and is paid there.\n *\n * Such an order must become a REAL order of the store — numbered with the\n * rest, shown in the same list, shipped from the same dialog, announced to\n * the same subscribers — and its units must leave the same shelf every other\n * channel sells from, in the same write, or two channels sell the last unit\n * twice. The plugin that brings the order in must not write the seller's\n * orders or products itself, so the seller registers\n * {@link PluginChannelOrders} and records each order under its own rules: the\n * order number, the stock decrement and its ledger row, the oversell alert,\n * the order events.\n *\n * ## Once per outside order\n *\n * An import is keyed by `channel.id` and `externalOrderId`. The same pair a\n * second time — a retried run, a page read twice — answers `already` with\n * the record the first import made, and nothing moves.\n *\n * ## The channel was paid, not the store\n *\n * The buyer paid the channel, and the channel pays the merchant out. No\n * money moves through the seller for the order; its refunds and returns\n * happen on the channel. What the channel charged the merchant for the sale\n * (`fees`) is RECORDED on the order for the merchant's books and never\n * charged or collected by anyone here.\n *\n * Money is integer minor units (cents for USD) in `currency`. A seller may\n * refuse an order in a currency its store does not sell in.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-channel-orders`); it is not in the\n * barrel.\n */\n\n/** One line of an outside order. */\nexport interface PluginChannelOrderLine {\n /** The channel's id for the line, which a shipment back to it names. */\n externalLineId: string\n /** The seller SKU the channel sold it under; `null` when it gave none. */\n sku: string | null\n /**\n * The product and configuration, when the importer already knows them\n * (it listed them from `core.product-catalog`). Otherwise the seller\n * matches `sku`.\n */\n productId?: string | null\n variantId?: string | null\n /** The channel's name for the item, kept on the order. */\n name: string\n quantity: number\n /** Per unit, before tax. */\n unitPriceCents: number\n}\n\n/** The address the order ships to. */\nexport interface PluginChannelOrderAddress {\n name?: string | null\n line1?: string | null\n line2?: string | null\n city?: string | null\n state?: string | null\n postalCode?: string | null\n /** ISO 3166-1 alpha-2, upper case. */\n country?: string | null\n phone?: string | null\n}\n\n/** A channel's charge on a sale, recorded for the merchant and never collected here. */\nexport interface PluginChannelOrderFee {\n label: string\n amountCents: number\n}\n\nexport interface PluginChannelOrder {\n hostId: string\n /** Stable id and display name, as the importer names its channel. */\n channel: { id: string; label: string }\n externalOrderId: string\n /** What the merchant sees on the channel. */\n externalRef: string\n placedAtMs: number\n /** ISO 4217. */\n currency: string\n lines: PluginChannelOrderLine[]\n shippingCents: number\n /** Tax the channel collected; the channel, not the store, remits it. */\n taxCents: number\n discountCents: number\n /** What the buyer paid the channel in all. */\n totalCents: number\n /** The channel's charges on the sale; `null` while the channel has not said. */\n fees: PluginChannelOrderFee[] | null\n customerName: string | null\n shippingAddress: PluginChannelOrderAddress | null\n /** Placed in the channel's sandbox: recorded, but never counted as revenue. */\n testMode: boolean\n /**\n * How the goods leave the store (AGL-3644): `ship`, the default, by a\n * carrier to `shippingAddress`; `courier`, handed over the counter to the\n * channel's own courier (a delivery app), so nothing is shipped from here\n * and the seller says so to the merchant.\n */\n handoff?: 'ship' | 'courier'\n}\n\n/** A line the store's stock could not cover in full: the channel sold more than the shelf held. */\nexport interface PluginChannelOrderShortfall {\n lineIndex: number\n sku: string | null\n /** Units sold beyond what the shelf held. */\n short: number\n}\n\nexport type PluginChannelOrderOutcome =\n | {\n outcome: 'created'\n recordId: string\n /** What a person calls it: `#1042`. */\n displayRef: string\n /** Which order line each channel line became. */\n lines: Array<{ lineIndex: number; externalLineId: string }>\n /** Lines whose units the shelf could not cover. The order is recorded all the same. */\n shortfalls: PluginChannelOrderShortfall[]\n /** Lines matching no product, recorded by name with no stock moved. */\n unmatched: number[]\n }\n | {\n outcome: 'already'\n recordId: string\n displayRef: string\n lines: Array<{ lineIndex: number; externalLineId: string }>\n }\n /** The seller would not record it; `reason` says why, for the merchant. */\n | { outcome: 'refused'; reason: string }\n\nexport interface PluginChannelOrderCancel {\n hostId: string\n recordId: string\n /** Shown on the order's timeline. */\n reason: string\n}\n\nexport type PluginChannelOrderCancelOutcome =\n /** Cancelled, with this many units back on the shelf. */\n | { outcome: 'cancelled'; restockedUnits: number }\n | { outcome: 'already' }\n /** The seller's rules refused (it has shipped, say); `status` is the seller's word for where it stands. */\n | { outcome: 'not_cancellable'; status: string }\n | { outcome: 'no_such_record' }\n\n/** The channel's courier took the whole order (AGL-3644): it is fulfilled, with nothing shipped from here. */\nexport interface PluginChannelOrderHandoff {\n hostId: string\n recordId: string\n /** Shown on the order's timeline, e.g. `Picked up by the DoorDash courier`. */\n note: string\n}\n\nexport type PluginChannelOrderHandoffOutcome =\n | { outcome: 'completed' }\n | { outcome: 'already' }\n /** The seller's rules refused (it was cancelled, say); `status` is the seller's word for where it stands. */\n | { outcome: 'not_completable'; status: string }\n | { outcome: 'no_such_record' }\n\n/**\n * Money the channel gave back to the buyer of an order it sent (AGL-3644):\n * a refund, or the channel's adjustment of the order — an item removed, a\n * quantity lowered. No money moves here: the channel refunded its buyer and\n * takes it from the merchant's payout, so the seller RECORDS it on the order.\n *\n * `restock` names the units that never left the store (an item taken off the\n * order before it was handed over); the seller puts back at most what the\n * order's own sale took. Empty for a refund of goods already gone.\n */\nexport interface PluginChannelOrderRefund {\n hostId: string\n recordId: string\n /** The channel's id for this refund or adjustment: the same id a second time records nothing. */\n refundId: string\n /** Minor units in the order's currency; `0` for an adjustment that only moves stock. */\n amountCents: number\n /** Shown on the order's timeline. */\n reason: string\n restock: Array<{ lineIndex: number; quantity: number }>\n}\n\nexport type PluginChannelOrderRefundOutcome =\n /** Recorded: `refundedCents` is the order's refunded total now, `restockedUnits` what went back on the shelf. */\n | { outcome: 'recorded'; refundedCents: number; restockedUnits: number }\n | { outcome: 'already' }\n /** The seller would not record it; `reason` says why, for the merchant. */\n | { outcome: 'refused'; reason: string }\n | { outcome: 'no_such_record' }\n\nexport interface PluginChannelOrders {\n /** Records an outside order once, with its units off the shelf in the same write. */\n importOrder(order: PluginChannelOrder): Promise<PluginChannelOrderOutcome>\n /** The channel canceled an order it had sent: cancel it here and put its units back. */\n cancelOrder(request: PluginChannelOrderCancel): Promise<PluginChannelOrderCancelOutcome>\n /** The channel's charges on a sale, once it says what they were. */\n recordFees(request: {\n hostId: string\n recordId: string\n fees: PluginChannelOrderFee[]\n }): Promise<'recorded' | 'no_such_record'>\n /**\n * The channel's courier took the order (AGL-3644). Optional: a seller that\n * cannot record a hand-off leaves it out, and the importer leaves the order\n * for the merchant to fulfill.\n */\n completeOrder?(request: PluginChannelOrderHandoff): Promise<PluginChannelOrderHandoffOutcome>\n /** The channel refunded or adjusted an order it sent (AGL-3644). Optional, as `completeOrder`. */\n recordRefund?(request: PluginChannelOrderRefund): Promise<PluginChannelOrderRefundOutcome>\n}\n\nconst PLUGIN_CHANNEL_ORDERS = definePluginServiceContract<PluginChannelOrders>('core.channel-orders', {\n multiple: false,\n})\n\n/** Registers the seller. A second plugin is refused naming both. */\nexport function registerPluginChannelOrders(orders: PluginChannelOrders, options?: { pluginId?: string }): void {\n registerPluginService(PLUGIN_CHANNEL_ORDERS, orders, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The seller, or `null` when no plugin registered one. */\nexport function pluginChannelOrders(): PluginChannelOrders | null {\n return resolvePluginServices(PLUGIN_CHANNEL_ORDERS)[0]?.impl ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_CHANNEL_ORDERS","multiple","registerPluginChannelOrders","orders","options","pluginId","pluginChannelOrders","impl"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA4N1B,MAAMC,wBAAwBH,4BAAiD,uBAAuB;IACpGI,UAAU;AACZ;AAEA,kEAAkE,GAClE,OAAO,SAASC,4BAA4BC,MAA2B,EAAEC,OAA+B;IACtGN,sBAAsBE,uBAAuBG,QAAQ,aAC/CC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,yDAAyD,GACzD,OAAO,SAASC;;QACPP;IAAP,gBAAOA,0BAAAA,sBAAsBC,sBAAsB,CAAC,EAAE,qBAA/CD,wBAAiDQ,IAAI,mBAAI;AAClE"}
@@ -0,0 +1,168 @@
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
+ * Optional lines a buyer may add at checkout, offered by another plugin
19
+ * (AGL-3635).
20
+ *
21
+ * Package protection is the first: a plugin that insures parcels quotes a
22
+ * premium for the basket, the seller shows it as a box the buyer ticks, and
23
+ * the seller charges it as one more line. The seller never learns which
24
+ * insurer answered and the insurer never reads the seller's documents: the
25
+ * offer crosses here, and what was bought is recorded on the sale, which the
26
+ * seller's own events then carry.
27
+ *
28
+ * ## Money
29
+ *
30
+ * Every amount is integer cents in the request's currency. An offer is
31
+ * advisory until the sale: the seller asks again when the buyer pays and
32
+ * charges THAT answer, so a stale price in a drawer is never what is
33
+ * charged. An offer whose amount is not a positive integer, or whose
34
+ * currency differs from the request's, is dropped rather than rounded.
35
+ *
36
+ * ## Nobody home is an empty list
37
+ *
38
+ * A provider that is not configured, not switched on for the site, slow or
39
+ * failing answers nothing, and the seller sells exactly as it did before.
40
+ * {@link quotePluginCheckoutExtras} never throws.
41
+ *
42
+ * Import this module by its own subpath
43
+ * (`@aglyn/aglyn/plugin-manager/plugin-checkout-extras`); it is not in the
44
+ * barrel.
45
+ */
46
+ /** One line of the basket, as a provider prices it. */
47
+ export interface PluginCheckoutExtraLine {
48
+ /** The seller's id for the item, when it has one. */
49
+ itemId?: string;
50
+ name: string;
51
+ sku?: string;
52
+ quantity: number;
53
+ /** Per unit, integer cents, before discounts. */
54
+ unitCents: number;
55
+ /** Whether the line travels in a parcel. */
56
+ ships: boolean;
57
+ }
58
+ /** What a seller asks: the basket a buyer is about to pay for. */
59
+ export interface PluginCheckoutExtraRequest {
60
+ hostId: string;
61
+ /** ISO-4217, lower case. */
62
+ currency: string;
63
+ /** The goods' value, integer cents, before discounts and shipping. */
64
+ itemsCents: number;
65
+ lines: PluginCheckoutExtraLine[];
66
+ /** Where the buyer said it goes, when they said. */
67
+ destination?: {
68
+ country?: string;
69
+ postalCode?: string;
70
+ };
71
+ /** Aborted when the seller stops waiting; a provider passes it to its fetches. */
72
+ signal?: AbortSignal;
73
+ }
74
+ /** What a provider offers for that basket. */
75
+ export interface PluginCheckoutExtraOffer {
76
+ /** Stable within the provider: `package-protection`. Lower-case words and dashes. */
77
+ key: string;
78
+ /** What the buyer reads beside the box: `Package protection`. */
79
+ label: string;
80
+ /** One sentence under it. */
81
+ description?: string;
82
+ /** Integer cents, above zero. */
83
+ amountCents: number;
84
+ /** ISO-4217, lower case; must be the request's. */
85
+ currency: string;
86
+ /** Whether the box starts ticked. The merchant decides; default unticked. */
87
+ defaultSelected?: boolean;
88
+ /** The provider's id for this quote, carried onto the sale (64 characters at most). */
89
+ quoteRef?: string;
90
+ /** A page the buyer can read about it, `https:` only. */
91
+ termsUrl?: string;
92
+ }
93
+ export interface PluginCheckoutExtraProvider {
94
+ /** The offer for this basket, or `null`. Throwing reads as `null`. */
95
+ offer(request: PluginCheckoutExtraRequest): Promise<PluginCheckoutExtraOffer | null>;
96
+ }
97
+ /** An offer as the seller sees it: whose it is, and the id it is chosen by. */
98
+ export interface QuotedPluginCheckoutExtra extends Required<Pick<PluginCheckoutExtraOffer, 'defaultSelected'>> {
99
+ /** `{pluginId}.{key}`: what a buyer's choice names. */
100
+ id: string;
101
+ pluginId: string;
102
+ key: string;
103
+ label: string;
104
+ description?: string;
105
+ amountCents: number;
106
+ currency: string;
107
+ quoteRef?: string;
108
+ termsUrl?: string;
109
+ }
110
+ /**
111
+ * What a sale recorded of one extra the buyer took. The seller stores this
112
+ * on the sale and puts it in the events it raises about the sale, so the
113
+ * provider learns what was bought from the seller's own facts.
114
+ */
115
+ export interface PluginCheckoutExtraSold {
116
+ id: string;
117
+ pluginId: string;
118
+ key: string;
119
+ label: string;
120
+ amountCents: number;
121
+ quoteRef?: string;
122
+ }
123
+ /** The most extras one checkout carries. */
124
+ export declare const MAX_CHECKOUT_EXTRAS = 3;
125
+ /** Joins the providers. Re-registering under the same plugin replaces its own. */
126
+ export declare function registerPluginCheckoutExtra(provider: PluginCheckoutExtraProvider, options?: {
127
+ pluginId?: string;
128
+ }): void;
129
+ /** Whether any plugin offers extras: a seller with none skips the question. */
130
+ export declare function hasPluginCheckoutExtras(): boolean;
131
+ /**
132
+ * An offer held to the contract, or `null`: a positive integer amount in the
133
+ * request's currency, a key and a label, and nothing else a page would render
134
+ * that it should not.
135
+ */
136
+ export declare function normalizePluginCheckoutExtra(pluginId: string, offer: PluginCheckoutExtraOffer | null | undefined, currency: string): QuotedPluginCheckoutExtra | null;
137
+ /**
138
+ * Asks every provider at once and keeps what answered within `timeoutMs`,
139
+ * each one held to the contract. Never throws; a provider that throws, is
140
+ * late or answers nonsense is simply absent. At most
141
+ * {@link MAX_CHECKOUT_EXTRAS}, in the providers' resolve order.
142
+ */
143
+ export declare function quotePluginCheckoutExtras(request: Omit<PluginCheckoutExtraRequest, 'signal'>, options: {
144
+ timeoutMs: number;
145
+ }): Promise<QuotedPluginCheckoutExtra[]>;
146
+ /**
147
+ * The buyer's choice, read from a request body: the ids of offers they took,
148
+ * de-duplicated and bounded. Ids only; the amounts are always asked again.
149
+ */
150
+ export declare function readChosenCheckoutExtras(value: unknown): string[];
151
+ /** The metadata keys the extras ride under: `extra0`, `extra1`, `extra2`. */
152
+ export declare const CHECKOUT_EXTRA_METADATA_PREFIX = "extra";
153
+ /**
154
+ * The extras a sale carries, packed for a payment processor's metadata: one
155
+ * key per extra (`extra0` …), each `[id, cents, label, quoteRef?]` as JSON,
156
+ * well inside a 500-character value. One key each, so a long quote id can
157
+ * never truncate another extra out of the record.
158
+ */
159
+ export declare function encodeCheckoutExtrasMetadata(extras: readonly QuotedPluginCheckoutExtra[]): Record<string, string>;
160
+ /**
161
+ * Reads {@link encodeCheckoutExtrasMetadata} back off the metadata object.
162
+ * Anything unreadable is dropped, never guessed.
163
+ */
164
+ export declare function decodeCheckoutExtrasMetadata(metadata: Record<string, unknown> | null | undefined): PluginCheckoutExtraSold[];
165
+ /** The cents a sale's extras add up to. */
166
+ export declare function checkoutExtrasCents(extras: ReadonlyArray<{
167
+ amountCents: number;
168
+ }> | null | undefined): number;
@@ -0,0 +1,172 @@
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
+ /** The most extras one checkout carries. */ export const MAX_CHECKOUT_EXTRAS = 3;
20
+ const KEY = /^[a-z][a-z0-9-]{1,39}$/;
21
+ const PLUGIN_ID = /^[a-z][a-z0-9-]{0,63}$/;
22
+ const PLUGIN_CHECKOUT_EXTRAS = definePluginServiceContract('core.checkout-extras', {
23
+ multiple: true
24
+ });
25
+ /** Joins the providers. Re-registering under the same plugin replaces its own. */ export function registerPluginCheckoutExtra(provider, options) {
26
+ var _getRegisteringPluginId;
27
+ const pluginId = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId;
28
+ registerPluginService(PLUGIN_CHECKOUT_EXTRAS, provider, _extends({}, pluginId ? {
29
+ pluginId
30
+ } : {}));
31
+ }
32
+ /** Whether any plugin offers extras: a seller with none skips the question. */ export function hasPluginCheckoutExtras() {
33
+ return resolvePluginServices(PLUGIN_CHECKOUT_EXTRAS).length > 0;
34
+ }
35
+ function cleanText(value, max) {
36
+ // Control characters become spaces: an offer's words are drawn on a page.
37
+ 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);
38
+ }
39
+ /**
40
+ * An offer held to the contract, or `null`: a positive integer amount in the
41
+ * request's currency, a key and a label, and nothing else a page would render
42
+ * that it should not.
43
+ */ export function normalizePluginCheckoutExtra(pluginId, offer, currency) {
44
+ var _offer_key, _offer_currency, _offer_termsUrl;
45
+ if (!offer || !PLUGIN_ID.test(pluginId)) return null;
46
+ const key = String((_offer_key = offer.key) != null ? _offer_key : '');
47
+ const label = cleanText(offer.label, 60);
48
+ const amountCents = offer.amountCents;
49
+ if (!KEY.test(key) || !label) return null;
50
+ if (typeof amountCents !== 'number' || !Number.isSafeInteger(amountCents) || amountCents <= 0) return null;
51
+ if (String((_offer_currency = offer.currency) != null ? _offer_currency : '').toLowerCase() !== String(currency != null ? currency : '').toLowerCase()) return null;
52
+ const description = cleanText(offer.description, 240);
53
+ const quoteRef = cleanText(offer.quoteRef, 64);
54
+ const termsUrl = /^https:\/\/[^\s"'<>]+$/i.test(String((_offer_termsUrl = offer.termsUrl) != null ? _offer_termsUrl : '')) ? String(offer.termsUrl) : '';
55
+ return _extends({
56
+ id: `${pluginId}.${key}`,
57
+ pluginId,
58
+ key,
59
+ label
60
+ }, description ? {
61
+ description
62
+ } : {}, {
63
+ amountCents,
64
+ currency: currency.toLowerCase(),
65
+ defaultSelected: offer.defaultSelected === true
66
+ }, quoteRef ? {
67
+ quoteRef
68
+ } : {}, termsUrl ? {
69
+ termsUrl
70
+ } : {});
71
+ }
72
+ /**
73
+ * Asks every provider at once and keeps what answered within `timeoutMs`,
74
+ * each one held to the contract. Never throws; a provider that throws, is
75
+ * late or answers nonsense is simply absent. At most
76
+ * {@link MAX_CHECKOUT_EXTRAS}, in the providers' resolve order.
77
+ */ export async function quotePluginCheckoutExtras(request, options) {
78
+ const providers = resolvePluginServices(PLUGIN_CHECKOUT_EXTRAS);
79
+ if (!providers.length) return [];
80
+ const controller = new AbortController();
81
+ let timer;
82
+ const deadline = new Promise((resolve)=>{
83
+ timer = setTimeout(()=>{
84
+ controller.abort();
85
+ resolve('late');
86
+ }, Math.max(0, options.timeoutMs));
87
+ });
88
+ try {
89
+ const answers = await Promise.all(providers.map(async (entry)=>{
90
+ const answer = await Promise.race([
91
+ entry.impl.offer(_extends({}, request, {
92
+ signal: controller.signal
93
+ })).catch((error)=>{
94
+ console.error(`[checkout-extras] provider "${entry.pluginId}" failed for ${request.hostId}`, error);
95
+ return null;
96
+ }),
97
+ deadline
98
+ ]);
99
+ return answer === 'late' ? null : normalizePluginCheckoutExtra(entry.pluginId, answer, request.currency);
100
+ }));
101
+ const seen = new Set();
102
+ return answers.filter((answer)=>Boolean(answer)).filter((answer)=>seen.has(answer.id) ? false : (seen.add(answer.id), true)).slice(0, MAX_CHECKOUT_EXTRAS);
103
+ } finally{
104
+ if (timer) clearTimeout(timer);
105
+ }
106
+ }
107
+ /**
108
+ * The buyer's choice, read from a request body: the ids of offers they took,
109
+ * de-duplicated and bounded. Ids only; the amounts are always asked again.
110
+ */ export function readChosenCheckoutExtras(value) {
111
+ if (!Array.isArray(value)) return [];
112
+ const ids = value.map((entry)=>String(entry != null ? entry : '').trim()).filter((entry)=>/^[a-z][a-z0-9-]{0,63}\.[a-z][a-z0-9-]{1,39}$/.test(entry));
113
+ return [
114
+ ...new Set(ids)
115
+ ].slice(0, MAX_CHECKOUT_EXTRAS);
116
+ }
117
+ /** The metadata keys the extras ride under: `extra0`, `extra1`, `extra2`. */ export const CHECKOUT_EXTRA_METADATA_PREFIX = 'extra';
118
+ /**
119
+ * The extras a sale carries, packed for a payment processor's metadata: one
120
+ * key per extra (`extra0` …), each `[id, cents, label, quoteRef?]` as JSON,
121
+ * well inside a 500-character value. One key each, so a long quote id can
122
+ * never truncate another extra out of the record.
123
+ */ export function encodeCheckoutExtrasMetadata(extras) {
124
+ const packed = {};
125
+ extras.slice(0, MAX_CHECKOUT_EXTRAS).forEach((extra, index)=>{
126
+ const entry = [
127
+ extra.id,
128
+ extra.amountCents,
129
+ extra.label.slice(0, 60)
130
+ ];
131
+ if (extra.quoteRef) entry.push(extra.quoteRef.slice(0, 64));
132
+ packed[`${CHECKOUT_EXTRA_METADATA_PREFIX}${index}`] = JSON.stringify(entry);
133
+ });
134
+ return packed;
135
+ }
136
+ /**
137
+ * Reads {@link encodeCheckoutExtrasMetadata} back off the metadata object.
138
+ * Anything unreadable is dropped, never guessed.
139
+ */ export function decodeCheckoutExtrasMetadata(metadata) {
140
+ const sold = [];
141
+ for(let index = 0; index < MAX_CHECKOUT_EXTRAS; index += 1){
142
+ const raw = metadata == null ? void 0 : metadata[`${CHECKOUT_EXTRA_METADATA_PREFIX}${index}`];
143
+ if (raw === undefined || raw === null || raw === '') continue;
144
+ let entry;
145
+ try {
146
+ entry = JSON.parse(String(raw));
147
+ } catch (unused) {
148
+ continue;
149
+ }
150
+ if (!Array.isArray(entry)) continue;
151
+ const [id, cents, label, quoteRef] = entry;
152
+ const match = /^([a-z][a-z0-9-]{0,63})\.([a-z][a-z0-9-]{1,39})$/.exec(String(id != null ? id : ''));
153
+ if (!match || typeof cents !== 'number' || !Number.isSafeInteger(cents) || cents <= 0) continue;
154
+ if (sold.some((extra)=>extra.id === id)) continue;
155
+ const ref = cleanText(quoteRef, 64);
156
+ sold.push(_extends({
157
+ id: String(id),
158
+ pluginId: match[1],
159
+ key: match[2],
160
+ label: cleanText(label, 60) || 'Extra',
161
+ amountCents: cents
162
+ }, ref ? {
163
+ quoteRef: ref
164
+ } : {}));
165
+ }
166
+ return sold;
167
+ }
168
+ /** The cents a sale's extras add up to. */ export function checkoutExtrasCents(extras) {
169
+ return (extras != null ? extras : []).reduce((sum, extra)=>sum + (Number.isSafeInteger(extra == null ? void 0 : extra.amountCents) && extra.amountCents > 0 ? extra.amountCents : 0), 0);
170
+ }
171
+
172
+ //# sourceMappingURL=plugin-checkout-extras.js.map