@reactionary/acp 0.0.0-stage → 0.10.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,815 @@
1
+ import {
2
+ createInitialRequestContext,
3
+ MemoryCache,
4
+ SessionSchema
5
+ } from "@reactionary/core";
6
+ import {
7
+ ReactionaryFeedGenerator,
8
+ acpProductFeedTransformer
9
+ } from "@reactionary/feeds";
10
+ import {
11
+ ACPCheckoutSessionStateSchema,
12
+ ACPCompleteCheckoutSessionRequestSchema,
13
+ ACPCreateCheckoutSessionRequestSchema,
14
+ ACPUpdateCheckoutSessionRequestSchema
15
+ } from "./acp-schemas.js";
16
+ const ACP_SESSION_ID_HEADER = "acp-session-id";
17
+ const SESSION_CACHE_KEY_PREFIX = "reactionary:acp:session";
18
+ const CHECKOUT_SESSION_CACHE_KEY_PREFIX = "reactionary:acp:checkout-session";
19
+ class ReactionaryACPServer {
20
+ constructor(clientFactory, options = {}) {
21
+ this.clientFactory = clientFactory;
22
+ this.options = options;
23
+ this.sessionStore = new ReactionaryACPSessionStore(
24
+ this.options.sessionCache ?? new MemoryCache(),
25
+ this.options.sessionTtlSeconds ?? 60 * 60 * 24
26
+ );
27
+ this.checkoutSessionStore = new ReactionaryACPCheckoutSessionStore(
28
+ this.options.sessionCache ?? new MemoryCache(),
29
+ this.options.checkoutSessionTtlSeconds ?? 60 * 60 * 24
30
+ );
31
+ assertACPClient(this.clientFactory(createInitialRequestContext()));
32
+ }
33
+ clientFactory;
34
+ options;
35
+ sessionStore;
36
+ checkoutSessionStore;
37
+ async fetch(request) {
38
+ const sessionId = getOrCreateSessionId(request);
39
+ const requestContext = await this.createRequestContext(sessionId);
40
+ const requestedFeed = this.getRequestedProductFeed(request);
41
+ if (requestedFeed) {
42
+ requestContext.languageContext = requestedFeed.feed.languageContext;
43
+ }
44
+ const client = this.clientFactory(requestContext);
45
+ assertACPClient(client);
46
+ const response = await this.handleRequest(request, client, requestContext).catch((error) => toACPErrorResponse(error));
47
+ await this.sessionStore.put(sessionId, requestContext.session);
48
+ response.headers.set(ACP_SESSION_ID_HEADER, sessionId);
49
+ return response;
50
+ }
51
+ getHandler() {
52
+ return {
53
+ fetch: (request) => this.fetch(request),
54
+ close: () => this.close()
55
+ };
56
+ }
57
+ toNodeHandler() {
58
+ return async (request, response) => {
59
+ const webResponse = await this.fetch(await toWebRequest(request));
60
+ await sendWebResponse(response, webResponse);
61
+ };
62
+ }
63
+ close() {
64
+ return Promise.resolve();
65
+ }
66
+ async createRequestContext(sessionId) {
67
+ const restoredSession = await this.sessionStore.get(sessionId);
68
+ const requestContext = createInitialRequestContext();
69
+ if (restoredSession) {
70
+ requestContext.session = restoredSession;
71
+ }
72
+ return requestContext;
73
+ }
74
+ async handleRequest(request, client, requestContext) {
75
+ if (request.method === "OPTIONS") {
76
+ return new Response(null, {
77
+ status: 204,
78
+ headers: {
79
+ allow: "GET, HEAD, OPTIONS, POST"
80
+ }
81
+ });
82
+ }
83
+ if (request.method === "GET" || request.method === "HEAD") {
84
+ const productFeedId = getProductFeedId(request, this.options.basePath);
85
+ if (productFeedId) {
86
+ const feed = this.getProductFeedDefinition(productFeedId);
87
+ if (!feed) {
88
+ return acpErrorResponse(404, {
89
+ type: "invalid_request",
90
+ code: "missing",
91
+ message: `Product feed not found: ${productFeedId}`
92
+ });
93
+ }
94
+ return this.getProductFeed(
95
+ request,
96
+ productFeedId,
97
+ feed,
98
+ client,
99
+ requestContext,
100
+ request.method === "HEAD"
101
+ );
102
+ }
103
+ const checkoutSessionId = getCheckoutSessionId(
104
+ request,
105
+ this.options.basePath
106
+ );
107
+ if (checkoutSessionId) {
108
+ return this.getCheckoutSession(
109
+ checkoutSessionId,
110
+ client,
111
+ request.method === "HEAD"
112
+ );
113
+ }
114
+ return jsonResponse(this.getReadinessDocument(), {
115
+ omitBody: request.method === "HEAD"
116
+ });
117
+ }
118
+ if (request.method === "POST") {
119
+ return this.handlePost(request, client, requestContext);
120
+ }
121
+ return jsonResponse({
122
+ error: {
123
+ code: "METHOD_NOT_ALLOWED",
124
+ message: `Unsupported method: ${request.method}`
125
+ }
126
+ }, {
127
+ status: 405,
128
+ headers: {
129
+ allow: "GET, HEAD, OPTIONS, POST"
130
+ }
131
+ });
132
+ }
133
+ getReadinessDocument() {
134
+ return {
135
+ name: this.options.name ?? "@reactionary/acp",
136
+ version: this.options.version ?? "0.0.1",
137
+ protocol: "acp",
138
+ status: "ready",
139
+ actions: [
140
+ "POST /checkout_sessions",
141
+ "POST /checkout_sessions/{checkout_session_id}",
142
+ "GET /checkout_sessions/{checkout_session_id}",
143
+ "POST /checkout_sessions/{checkout_session_id}/complete",
144
+ "POST /checkout_sessions/{checkout_session_id}/cancel",
145
+ "GET /product_feeds/{id}/products"
146
+ ]
147
+ };
148
+ }
149
+ async getProductFeed(request, feedId, feed, client, requestContext, omitBody) {
150
+ const format = new URL(request.url).searchParams.get("format") ?? "json";
151
+ if (format !== "json" && format !== "jsonl") {
152
+ return acpErrorResponse(400, {
153
+ type: "invalid_request",
154
+ code: "invalid",
155
+ message: `Unsupported product feed format: ${format}`,
156
+ param: "$.format"
157
+ });
158
+ }
159
+ const generator = new ReactionaryFeedGenerator(client, {
160
+ defaultFulfillmentCenterKeys: this.options.productFeed?.defaultFulfillmentCenterKeys,
161
+ productConcurrency: this.options.productFeed?.productConcurrency
162
+ });
163
+ const output = acpProductFeedTransformer.transform(
164
+ generator.products(feed, requestContext),
165
+ {
166
+ feedId,
167
+ feed,
168
+ options: { format }
169
+ }
170
+ );
171
+ const extension = format === "jsonl" ? "jsonl" : "json";
172
+ return new Response(
173
+ omitBody ? null : createFeedStream(output),
174
+ {
175
+ headers: {
176
+ "content-type": format === "jsonl" ? "application/x-ndjson; charset=utf-8" : "application/json; charset=utf-8",
177
+ "content-disposition": `attachment; filename="${feedId}.products.${extension}"`
178
+ }
179
+ }
180
+ );
181
+ }
182
+ getRequestedProductFeed(request) {
183
+ const id = getProductFeedId(request, this.options.basePath);
184
+ const feed = id ? this.getProductFeedDefinition(id) : void 0;
185
+ return id && feed ? { id, feed } : void 0;
186
+ }
187
+ getProductFeedDefinition(feedId) {
188
+ return this.options.productFeed?.feeds[feedId];
189
+ }
190
+ async handlePost(request, client, requestContext) {
191
+ const checkoutSessionId = getCheckoutSessionId(
192
+ request,
193
+ this.options.basePath
194
+ );
195
+ if (!checkoutSessionId) {
196
+ return this.createCheckoutSession(
197
+ await parseJsonBody(request, ACPCreateCheckoutSessionRequestSchema),
198
+ client,
199
+ requestContext
200
+ );
201
+ }
202
+ if (isCheckoutSessionCompleteRequest(request, this.options.basePath)) {
203
+ return this.completeCheckoutSession(
204
+ checkoutSessionId,
205
+ await parseJsonBody(request, ACPCompleteCheckoutSessionRequestSchema),
206
+ client
207
+ );
208
+ }
209
+ if (isCheckoutSessionCancelRequest(request, this.options.basePath)) {
210
+ return this.cancelCheckoutSession(checkoutSessionId, client);
211
+ }
212
+ return this.updateCheckoutSession(
213
+ checkoutSessionId,
214
+ await parseJsonBody(request, ACPUpdateCheckoutSessionRequestSchema),
215
+ client,
216
+ requestContext
217
+ );
218
+ }
219
+ async createCheckoutSession(input, client, requestContext) {
220
+ const cart = await this.createCartForItems(input.items, client);
221
+ const checkout = await unwrapACPResult(
222
+ client.checkout.initiateCheckoutForCart({
223
+ cart,
224
+ billingAddress: input.fulfillment_address ? toReactionaryAddress(input.fulfillment_address) : void 0,
225
+ notificationEmail: input.buyer?.email,
226
+ notificationPhone: input.buyer?.phone_number
227
+ })
228
+ );
229
+ const state = {
230
+ id: `checkout_session_${crypto.randomUUID()}`,
231
+ cartId: cart.identifier.key,
232
+ checkoutId: checkout.identifier.key,
233
+ status: getCheckoutStatus(checkout),
234
+ buyer: input.buyer,
235
+ fulfillmentAddress: input.fulfillment_address
236
+ };
237
+ await this.checkoutSessionStore.put(state.id, state);
238
+ return jsonResponse(
239
+ await this.toACPCheckoutSession(state, checkout, client, requestContext),
240
+ { status: 201 }
241
+ );
242
+ }
243
+ async updateCheckoutSession(checkoutSessionId, input, client, requestContext) {
244
+ const state = await this.getRequiredCheckoutSessionState(checkoutSessionId);
245
+ let checkout = await this.getReactionaryCheckout(state, client);
246
+ let cartId = state.cartId;
247
+ if (input.items) {
248
+ const cart = await this.createCartForItems(input.items, client);
249
+ checkout = await unwrapACPResult(
250
+ client.checkout.initiateCheckoutForCart({
251
+ cart,
252
+ billingAddress: input.fulfillment_address ? toReactionaryAddress(input.fulfillment_address) : state.fulfillmentAddress ? toReactionaryAddress(state.fulfillmentAddress) : void 0,
253
+ notificationEmail: input.buyer?.email ?? state.buyer?.email,
254
+ notificationPhone: input.buyer?.phone_number ?? state.buyer?.phone_number
255
+ })
256
+ );
257
+ cartId = cart.identifier.key;
258
+ }
259
+ if (input.fulfillment_address) {
260
+ checkout = await unwrapACPResult(
261
+ client.checkout.setShippingAddress({
262
+ checkout: checkout.identifier,
263
+ shippingAddress: toReactionaryAddress(input.fulfillment_address)
264
+ })
265
+ );
266
+ }
267
+ if (input.fulfillment_option_id) {
268
+ checkout = await unwrapACPResult(
269
+ client.checkout.setShippingInstruction({
270
+ checkout: checkout.identifier,
271
+ shippingInstruction: {
272
+ shippingMethod: { key: input.fulfillment_option_id },
273
+ pickupPoint: "",
274
+ instructions: "",
275
+ consentForUnattendedDelivery: false
276
+ }
277
+ })
278
+ );
279
+ }
280
+ const updatedState = {
281
+ ...state,
282
+ cartId,
283
+ checkoutId: checkout.identifier.key,
284
+ status: getCheckoutStatus(checkout),
285
+ buyer: input.buyer ?? state.buyer,
286
+ fulfillmentAddress: input.fulfillment_address ?? state.fulfillmentAddress,
287
+ fulfillmentOptionId: input.fulfillment_option_id ?? state.fulfillmentOptionId
288
+ };
289
+ await this.checkoutSessionStore.put(checkoutSessionId, updatedState);
290
+ return jsonResponse(
291
+ await this.toACPCheckoutSession(
292
+ updatedState,
293
+ checkout,
294
+ client,
295
+ requestContext
296
+ )
297
+ );
298
+ }
299
+ async getCheckoutSession(checkoutSessionId, client, omitBody) {
300
+ const state = await this.checkoutSessionStore.get(checkoutSessionId);
301
+ if (!state) {
302
+ return acpErrorResponse(404, {
303
+ type: "invalid_request",
304
+ code: "missing",
305
+ message: `Checkout session not found: ${checkoutSessionId}`
306
+ });
307
+ }
308
+ const checkout = await this.getReactionaryCheckout(state, client);
309
+ return jsonResponse(
310
+ await this.toACPCheckoutSession(state, checkout, client),
311
+ { omitBody }
312
+ );
313
+ }
314
+ async completeCheckoutSession(checkoutSessionId, input, client) {
315
+ const state = await this.getRequiredCheckoutSessionState(checkoutSessionId);
316
+ if (state.status === "canceled") {
317
+ return acpErrorResponse(405, {
318
+ type: "invalid_request",
319
+ code: "invalid",
320
+ message: "Canceled checkout sessions cannot be completed."
321
+ });
322
+ }
323
+ let checkout = await this.getReactionaryCheckout(state, client);
324
+ checkout = await unwrapACPResult(
325
+ client.checkout.addPaymentInstruction({
326
+ checkout: checkout.identifier,
327
+ paymentInstruction: {
328
+ amount: checkout.price.grandTotal,
329
+ paymentMethod: toPaymentMethodIdentifier(input.payment_data.provider),
330
+ protocolData: [
331
+ {
332
+ key: "delegated_payment_token",
333
+ value: input.payment_data.token
334
+ }
335
+ ]
336
+ }
337
+ })
338
+ );
339
+ checkout = await unwrapACPResult(
340
+ client.checkout.finalizeCheckout({
341
+ checkout: checkout.identifier
342
+ })
343
+ );
344
+ const completedState = {
345
+ ...state,
346
+ status: "completed",
347
+ buyer: input.buyer ?? state.buyer,
348
+ orderId: checkout.resultingOrder?.key
349
+ };
350
+ await this.checkoutSessionStore.put(checkoutSessionId, completedState);
351
+ return jsonResponse(
352
+ await this.toACPCheckoutSession(completedState, checkout, client)
353
+ );
354
+ }
355
+ async cancelCheckoutSession(checkoutSessionId, client) {
356
+ const state = await this.getRequiredCheckoutSessionState(checkoutSessionId);
357
+ if (state.status === "completed" || state.status === "canceled") {
358
+ return acpErrorResponse(405, {
359
+ type: "invalid_request",
360
+ code: "invalid",
361
+ message: "Completed or canceled checkout sessions cannot be canceled."
362
+ });
363
+ }
364
+ const canceledState = {
365
+ ...state,
366
+ status: "canceled"
367
+ };
368
+ const checkout = await this.getReactionaryCheckout(canceledState, client);
369
+ await this.checkoutSessionStore.put(checkoutSessionId, canceledState);
370
+ return jsonResponse(
371
+ await this.toACPCheckoutSession(canceledState, checkout, client)
372
+ );
373
+ }
374
+ async createCartForItems(items, client) {
375
+ let cart = await unwrapACPResult(client.cart.createCart({}));
376
+ for (const item of items) {
377
+ cart = await unwrapACPResult(
378
+ client.cart.add({
379
+ cart: cart.identifier,
380
+ variant: { sku: item.id },
381
+ quantity: item.quantity
382
+ })
383
+ );
384
+ }
385
+ return cart;
386
+ }
387
+ async getRequiredCheckoutSessionState(checkoutSessionId) {
388
+ const state = await this.checkoutSessionStore.get(checkoutSessionId);
389
+ if (!state) {
390
+ throw new ACPHttpError(404, {
391
+ type: "invalid_request",
392
+ code: "missing",
393
+ message: `Checkout session not found: ${checkoutSessionId}`
394
+ });
395
+ }
396
+ return state;
397
+ }
398
+ async getReactionaryCheckout(state, client) {
399
+ return unwrapACPResult(
400
+ client.checkout.getById({
401
+ identifier: { key: state.checkoutId }
402
+ })
403
+ );
404
+ }
405
+ async toACPCheckoutSession(state, checkout, client, requestContext = createInitialRequestContext()) {
406
+ const fulfillmentOptions = await this.getFulfillmentOptions(
407
+ checkout,
408
+ client
409
+ );
410
+ return {
411
+ id: state.id,
412
+ ...state.buyer ? { buyer: state.buyer } : {},
413
+ payment_provider: await this.getPaymentProvider(checkout, client),
414
+ status: state.status === "completed" || state.status === "canceled" ? state.status : getCheckoutStatus(checkout),
415
+ currency: getCurrency(checkout, requestContext),
416
+ line_items: checkout.items.map(toACPLineItem),
417
+ ...state.fulfillmentAddress ? { fulfillment_address: state.fulfillmentAddress } : {},
418
+ fulfillment_options: fulfillmentOptions,
419
+ ...state.fulfillmentOptionId ? { fulfillment_option_id: state.fulfillmentOptionId } : checkout.shippingInstruction?.shippingMethod.key ? {
420
+ fulfillment_option_id: checkout.shippingInstruction.shippingMethod.key
421
+ } : {},
422
+ totals: toACPTotals(checkout),
423
+ ...state.orderId ? {
424
+ order: {
425
+ id: state.orderId,
426
+ checkout_session_id: state.id,
427
+ permalink_url: ""
428
+ }
429
+ } : {},
430
+ messages: [],
431
+ links: this.options.links ?? []
432
+ };
433
+ }
434
+ async getFulfillmentOptions(checkout, client) {
435
+ const result = await client.checkout.getAvailableShippingMethods({
436
+ checkout: checkout.identifier
437
+ });
438
+ if (!result.success) {
439
+ return [];
440
+ }
441
+ return result.value.map(toACPFulfillmentOption);
442
+ }
443
+ async getPaymentProvider(checkout, client) {
444
+ if (this.options.paymentProvider) {
445
+ return this.options.paymentProvider;
446
+ }
447
+ const result = await client.checkout.getAvailablePaymentMethods({
448
+ checkout: checkout.identifier
449
+ });
450
+ if (result.success) {
451
+ const supported = result.value.find(
452
+ (method) => ["stripe", "adyen", "braintree"].includes(
453
+ method.identifier.paymentProcessor
454
+ )
455
+ );
456
+ if (supported) {
457
+ return {
458
+ provider: supported.identifier.paymentProcessor,
459
+ supported_payment_methods: ["card"]
460
+ };
461
+ }
462
+ }
463
+ return {
464
+ provider: "stripe",
465
+ supported_payment_methods: ["card"]
466
+ };
467
+ }
468
+ }
469
+ class ACPHttpError extends Error {
470
+ constructor(status, body) {
471
+ super(body.message);
472
+ this.status = status;
473
+ this.body = body;
474
+ }
475
+ status;
476
+ body;
477
+ }
478
+ class ReactionaryACPSessionStore {
479
+ constructor(cache, ttlSeconds) {
480
+ this.cache = cache;
481
+ this.ttlSeconds = ttlSeconds;
482
+ }
483
+ cache;
484
+ ttlSeconds;
485
+ async get(sessionId) {
486
+ return await this.cache.get(
487
+ this.getCacheKey(sessionId),
488
+ SessionSchema
489
+ ) ?? void 0;
490
+ }
491
+ async put(sessionId, session) {
492
+ await this.cache.invalidate([this.getDependencyId(sessionId)]);
493
+ await this.cache.put(this.getCacheKey(sessionId), session, {
494
+ ttlSeconds: this.ttlSeconds,
495
+ dependencyIds: [this.getDependencyId(sessionId)]
496
+ });
497
+ }
498
+ getCacheKey(sessionId) {
499
+ return `${SESSION_CACHE_KEY_PREFIX}:${sessionId}`;
500
+ }
501
+ getDependencyId(sessionId) {
502
+ return `${SESSION_CACHE_KEY_PREFIX}:${sessionId}`;
503
+ }
504
+ }
505
+ class ReactionaryACPCheckoutSessionStore {
506
+ constructor(cache, ttlSeconds) {
507
+ this.cache = cache;
508
+ this.ttlSeconds = ttlSeconds;
509
+ }
510
+ cache;
511
+ ttlSeconds;
512
+ async get(checkoutSessionId) {
513
+ return await this.cache.get(
514
+ this.getCacheKey(checkoutSessionId),
515
+ ACPCheckoutSessionStateSchema
516
+ ) ?? void 0;
517
+ }
518
+ async put(checkoutSessionId, session) {
519
+ await this.cache.invalidate([this.getDependencyId(checkoutSessionId)]);
520
+ await this.cache.put(this.getCacheKey(checkoutSessionId), session, {
521
+ ttlSeconds: this.ttlSeconds,
522
+ dependencyIds: [this.getDependencyId(checkoutSessionId)]
523
+ });
524
+ }
525
+ getCacheKey(checkoutSessionId) {
526
+ return `${CHECKOUT_SESSION_CACHE_KEY_PREFIX}:${checkoutSessionId}`;
527
+ }
528
+ getDependencyId(checkoutSessionId) {
529
+ return `${CHECKOUT_SESSION_CACHE_KEY_PREFIX}:${checkoutSessionId}`;
530
+ }
531
+ }
532
+ function getSessionId(request) {
533
+ return request.headers.get(ACP_SESSION_ID_HEADER) ?? void 0;
534
+ }
535
+ function getOrCreateSessionId(request) {
536
+ return getSessionId(request) ?? crypto.randomUUID();
537
+ }
538
+ function getCheckoutSessionId(request, basePath) {
539
+ const pathname = getProtocolPathname(request, basePath);
540
+ const match = /^\/checkout_sessions\/([^/]+)(?:\/(?:complete|cancel))?$/.exec(
541
+ pathname
542
+ );
543
+ return match?.[1];
544
+ }
545
+ function getProductFeedId(request, basePath) {
546
+ const pathname = getProtocolPathname(request, basePath);
547
+ const match = /^\/product_feeds\/([^/]+)\/products$/.exec(pathname);
548
+ return match?.[1];
549
+ }
550
+ function isCheckoutSessionCompleteRequest(request, basePath) {
551
+ return getProtocolPathname(request, basePath).endsWith("/complete");
552
+ }
553
+ function isCheckoutSessionCancelRequest(request, basePath) {
554
+ return getProtocolPathname(request, basePath).endsWith("/cancel");
555
+ }
556
+ function getProtocolPathname(request, basePath = "/acp") {
557
+ const pathname = new URL(request.url).pathname;
558
+ if (pathname === basePath) {
559
+ return "/";
560
+ }
561
+ if (pathname.startsWith(`${basePath}/`)) {
562
+ return pathname.slice(basePath.length);
563
+ }
564
+ return pathname;
565
+ }
566
+ async function parseJsonBody(request, schema) {
567
+ try {
568
+ return schema.parse(await request.json());
569
+ } catch (error) {
570
+ throw new ACPHttpError(400, {
571
+ type: "invalid_request",
572
+ code: "invalid",
573
+ message: error instanceof Error ? error.message : "Invalid request body"
574
+ });
575
+ }
576
+ }
577
+ function jsonResponse(body, options = {}) {
578
+ const headers = new Headers(options.headers);
579
+ headers.set("content-type", "application/json; charset=utf-8");
580
+ return new Response(
581
+ options.omitBody ? null : JSON.stringify(body),
582
+ {
583
+ status: options.status ?? 200,
584
+ headers
585
+ }
586
+ );
587
+ }
588
+ function acpErrorResponse(status, body) {
589
+ return jsonResponse(body, { status });
590
+ }
591
+ function assertACPClient(client) {
592
+ const missing = getMissingACPClientOperations(client);
593
+ if (missing.length > 0) {
594
+ throw new Error(
595
+ `Reactionary ACP server cannot initialize because the client is missing required operations: ${missing.join(", ")}`
596
+ );
597
+ }
598
+ }
599
+ function toACPErrorResponse(error) {
600
+ if (error instanceof ACPHttpError) {
601
+ return acpErrorResponse(error.status, error.body);
602
+ }
603
+ return acpErrorResponse(500, {
604
+ type: "processing_error",
605
+ code: "internal_error",
606
+ message: error instanceof Error ? error.message : String(error)
607
+ });
608
+ }
609
+ function getMissingACPClientOperations(client) {
610
+ return [
611
+ ["cart.createCart", client.cart?.createCart],
612
+ ["cart.add", client.cart?.add],
613
+ ["cart.getById", client.cart?.getById],
614
+ ["checkout.initiateCheckoutForCart", client.checkout?.initiateCheckoutForCart],
615
+ ["checkout.getById", client.checkout?.getById],
616
+ ["checkout.setShippingAddress", client.checkout?.setShippingAddress],
617
+ ["checkout.getAvailableShippingMethods", client.checkout?.getAvailableShippingMethods],
618
+ ["checkout.getAvailablePaymentMethods", client.checkout?.getAvailablePaymentMethods],
619
+ ["checkout.setShippingInstruction", client.checkout?.setShippingInstruction],
620
+ ["checkout.addPaymentInstruction", client.checkout?.addPaymentInstruction],
621
+ ["checkout.finalizeCheckout", client.checkout?.finalizeCheckout],
622
+ ["productSearch.queryByTerm", client.productSearch?.queryByTerm],
623
+ ["product.getBySKU", client.product?.getBySKU],
624
+ ["price.getListPrice", client.price?.getListPrice],
625
+ ["price.getCustomerPrice", client.price?.getCustomerPrice],
626
+ ["inventory.getBySKU", client.inventory?.getBySKU]
627
+ ].filter(([, operation]) => typeof operation !== "function").map(([name]) => name);
628
+ }
629
+ async function unwrapACPResult(resultPromise) {
630
+ const result = await resultPromise;
631
+ if (!result.success) {
632
+ throw new ACPHttpError(400, {
633
+ type: "processing_error",
634
+ code: "reactionary_error",
635
+ message: JSON.stringify(result.error)
636
+ });
637
+ }
638
+ return result.value;
639
+ }
640
+ function getCheckoutStatus(checkout) {
641
+ return checkout.readyForFinalization ? "ready_for_payment" : "not_ready_for_payment";
642
+ }
643
+ function getCurrency(checkout, requestContext) {
644
+ return (checkout.price.grandTotal.currency ?? requestContext.languageContext.currencyCode).toLowerCase();
645
+ }
646
+ function toACPLineItem(item) {
647
+ const baseAmount = toMinorUnits(item.price.unitPrice.value * item.quantity);
648
+ const discount = toMinorUnits(item.price.totalDiscount.value);
649
+ const total = toMinorUnits(item.price.totalPrice.value);
650
+ return {
651
+ id: item.identifier.key,
652
+ item: {
653
+ id: item.variant.sku,
654
+ quantity: item.quantity
655
+ },
656
+ base_amount: baseAmount,
657
+ discount,
658
+ subtotal: Math.max(baseAmount - discount, 0),
659
+ tax: 0,
660
+ total
661
+ };
662
+ }
663
+ function toACPTotals(checkout) {
664
+ const base = toMinorUnits(checkout.price.totalProductPrice.value);
665
+ const discount = toMinorUnits(checkout.price.totalDiscount.value);
666
+ const subtotal = Math.max(base - discount, 0);
667
+ const fulfillment = toMinorUnits(checkout.price.totalShipping.value);
668
+ const tax = toMinorUnits(checkout.price.totalTax.value);
669
+ const total = toMinorUnits(checkout.price.grandTotal.value);
670
+ return [
671
+ {
672
+ type: "items_base_amount",
673
+ display_text: "Item(s) total",
674
+ amount: base
675
+ },
676
+ {
677
+ type: "items_discount",
678
+ display_text: "Item discount",
679
+ amount: discount
680
+ },
681
+ {
682
+ type: "subtotal",
683
+ display_text: "Subtotal",
684
+ amount: subtotal
685
+ },
686
+ {
687
+ type: "fulfillment",
688
+ display_text: "Fulfillment",
689
+ amount: fulfillment
690
+ },
691
+ {
692
+ type: "tax",
693
+ display_text: "Tax",
694
+ amount: tax
695
+ },
696
+ {
697
+ type: "total",
698
+ display_text: "Total",
699
+ amount: total
700
+ }
701
+ ];
702
+ }
703
+ function toACPFulfillmentOption(method) {
704
+ const amount = toMinorUnits(method.price.value);
705
+ return {
706
+ type: "shipping",
707
+ id: method.identifier.key,
708
+ title: method.name,
709
+ subtitle: method.deliveryTime,
710
+ carrier: method.carrier ?? "",
711
+ earliest_delivery_time: (/* @__PURE__ */ new Date()).toISOString(),
712
+ latest_delivery_time: (/* @__PURE__ */ new Date()).toISOString(),
713
+ subtotal: amount,
714
+ tax: 0,
715
+ total: amount
716
+ };
717
+ }
718
+ function toReactionaryAddress(address) {
719
+ const [firstName, ...lastNameParts] = address.name.split(" ");
720
+ return {
721
+ firstName: firstName ?? address.name,
722
+ lastName: lastNameParts.join(" "),
723
+ streetAddress: address.line_one,
724
+ streetNumber: "",
725
+ city: address.city,
726
+ region: address.state,
727
+ postalCode: address.postal_code,
728
+ countryCode: address.country
729
+ };
730
+ }
731
+ function toPaymentMethodIdentifier(provider) {
732
+ return {
733
+ method: "card",
734
+ name: provider,
735
+ paymentProcessor: provider
736
+ };
737
+ }
738
+ function toMinorUnits(value) {
739
+ return Math.max(Math.round(value * 100), 0);
740
+ }
741
+ function createFeedStream(chunks) {
742
+ const encoder = new TextEncoder();
743
+ const iterator = chunks[Symbol.asyncIterator]();
744
+ return new ReadableStream({
745
+ async pull(controller) {
746
+ const next = await iterator.next();
747
+ if (next.done) {
748
+ controller.close();
749
+ return;
750
+ }
751
+ controller.enqueue(
752
+ typeof next.value === "string" ? encoder.encode(next.value) : next.value
753
+ );
754
+ },
755
+ async cancel() {
756
+ await iterator.return?.();
757
+ }
758
+ });
759
+ }
760
+ async function toWebRequest(request) {
761
+ const headers = toWebHeaders(request.headers);
762
+ const url = new URL(
763
+ request.url ?? "/",
764
+ `http://${request.headers.host ?? "localhost"}`
765
+ );
766
+ const body = await readNodeRequestBody(request);
767
+ return new Request(url, {
768
+ method: request.method,
769
+ headers,
770
+ body
771
+ });
772
+ }
773
+ function toWebHeaders(headers) {
774
+ const webHeaders = new Headers();
775
+ for (const [key, value] of Object.entries(headers)) {
776
+ if (value === void 0) {
777
+ continue;
778
+ }
779
+ if (Array.isArray(value)) {
780
+ for (const entry of value) {
781
+ webHeaders.append(key, entry);
782
+ }
783
+ continue;
784
+ }
785
+ webHeaders.set(key, value);
786
+ }
787
+ return webHeaders;
788
+ }
789
+ async function readNodeRequestBody(request) {
790
+ if (request.method === "GET" || request.method === "HEAD") {
791
+ return null;
792
+ }
793
+ const chunks = [];
794
+ for await (const chunk of request) {
795
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
796
+ }
797
+ if (chunks.length === 0) {
798
+ return null;
799
+ }
800
+ return Buffer.concat(chunks);
801
+ }
802
+ async function sendWebResponse(response, webResponse) {
803
+ response.writeHead(
804
+ webResponse.status,
805
+ Object.fromEntries(webResponse.headers.entries())
806
+ );
807
+ if (webResponse.body) {
808
+ response.end(Buffer.from(await webResponse.arrayBuffer()));
809
+ return;
810
+ }
811
+ response.end();
812
+ }
813
+ export {
814
+ ReactionaryACPServer
815
+ };