@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Solteq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,157 @@
1
- # Temporary Holding Version
1
+ # @reactionary/acp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ HTTP adapter for exposing Reactionary checkout capabilities through the Agentic Commerce Protocol (ACP) checkout surface.
4
+
5
+ The package keeps Reactionary core protocol-neutral: host applications mount this adapter and provide a request-scoped Reactionary client factory.
6
+
7
+ ## Implemented surface
8
+
9
+ The adapter implements the merchant-hosted ACP checkout endpoints:
10
+
11
+ - `GET /product_feeds/{id}/products`
12
+ - `POST /checkout_sessions`
13
+ - `POST /checkout_sessions/{checkout_session_id}`
14
+ - `GET /checkout_sessions/{checkout_session_id}`
15
+ - `POST /checkout_sessions/{checkout_session_id}/complete`
16
+ - `POST /checkout_sessions/{checkout_session_id}/cancel`
17
+
18
+ It also serves a readiness document from `GET` / `HEAD`.
19
+
20
+ If the adapter is mounted under `/acp`, both `/checkout_sessions/...` and `/acp/checkout_sessions/...` paths are understood. Set `basePath` when mounting somewhere else.
21
+
22
+ ## Product feed generation
23
+
24
+ `GET /product_feeds/{id}/products` generates an ACP-compatible product feed from the configured feed definition whose key matches `{id}`.
25
+
26
+ The feed enumeration and serialization pipeline is shared with `@reactionary/feeds`; this adapter keeps the ACP-compatible route shape while delegating normalization and ACP product feed output to the reusable feed package.
27
+
28
+ Each feed definition provides:
29
+
30
+ - `languageContext` — applied to the `RequestContext` before the Reactionary client is created.
31
+ - `search` — a `ProductSearchIdentifierSchema`-compatible search object passed to `productSearch.queryByTerm`.
32
+ - `productUrlBase` — either a base URL that the product slug is resolved against, or a URL template supporting `{lang}` and `{slug}` placeholders.
33
+
34
+ By default it returns the ACP JSON shape:
35
+
36
+ ```json
37
+ {
38
+ "target_country": "FI",
39
+ "products": []
40
+ }
41
+ ```
42
+
43
+ `target_country` is derived from `languageContext.locale` when the locale includes a region, e.g. `fi-FI` -> `FI`.
44
+
45
+ For file generation, request JSONL:
46
+
47
+ ```bash
48
+ curl "https://example.com/acp/product_feeds/default/products?format=jsonl" > products.jsonl
49
+ ```
50
+
51
+ The first implementation streams the response directly instead of generating a download URL. That keeps the adapter stateless and lets callers pipe the result to a file, object storage upload, or feed-ingestion job. A signed download URL can be layered on later by a host application or a storage-backed feed job.
52
+
53
+ ## Required Reactionary capabilities
54
+
55
+ The server validates the supplied client during construction and on every request. If the client does not expose the operations required by ACP checkout, the server does not initialize.
56
+
57
+ Required operations:
58
+
59
+ - `cart.createCart`
60
+ - `cart.add`
61
+ - `cart.getById`
62
+ - `checkout.initiateCheckoutForCart`
63
+ - `checkout.getById`
64
+ - `checkout.setShippingAddress`
65
+ - `checkout.getAvailableShippingMethods`
66
+ - `checkout.getAvailablePaymentMethods`
67
+ - `checkout.setShippingInstruction`
68
+ - `checkout.addPaymentInstruction`
69
+ - `checkout.finalizeCheckout`
70
+ - `productSearch.queryByTerm`
71
+ - `product.getBySKU`
72
+ - `price.getListPrice`
73
+ - `price.getCustomerPrice`
74
+ - `inventory.getBySKU`
75
+
76
+ This is deliberate because Reactionary clients can be built with different capability sets. A partially capable client should fail fast instead of advertising ACP checkout.
77
+
78
+ ## Usage
79
+
80
+ ```ts
81
+ import { ReactionaryACPServer } from '@reactionary/acp';
82
+
83
+ const acp = new ReactionaryACPServer((requestContext) =>
84
+ createReactionaryClient({ contextOverrides: requestContext }),
85
+ );
86
+
87
+ export const POST = (request: Request) => acp.fetch(request);
88
+ export const GET = (request: Request) => acp.fetch(request);
89
+ ```
90
+
91
+ For Node HTTP:
92
+
93
+ ```ts
94
+ import { createServer } from 'node:http';
95
+ import { ReactionaryACPServer } from '@reactionary/acp';
96
+
97
+ const acp = new ReactionaryACPServer(createClient);
98
+ const handler = acp.toNodeHandler();
99
+
100
+ createServer((request, response) => {
101
+ void handler(request, response);
102
+ }).listen(3000);
103
+ ```
104
+
105
+ ## Session handling
106
+
107
+ The server uses two pieces of state:
108
+
109
+ - `RequestContext.session`, keyed by the `acp-session-id` header.
110
+ - ACP checkout-session state, keyed by `checkout_session_id`.
111
+
112
+ If the request does not include `acp-session-id`, the server creates a new one and returns it in the response.
113
+
114
+ State is stored through the Reactionary `Cache` interface. The default is `MemoryCache`, which is useful for local development only.
115
+
116
+ ```ts
117
+ new ReactionaryACPServer(createClient, {
118
+ sessionCache: redisCache,
119
+ sessionTtlSeconds: 60 * 60,
120
+ checkoutSessionTtlSeconds: 60 * 60,
121
+ productFeed: {
122
+ feeds: {
123
+ finnish: {
124
+ languageContext: {
125
+ locale: 'fi-FI',
126
+ currencyCode: 'EUR',
127
+ },
128
+ search: {
129
+ term: '',
130
+ facets: [],
131
+ filters: ['market:fi'],
132
+ paginationOptions: {
133
+ pageNumber: 1,
134
+ pageSize: 50,
135
+ },
136
+ },
137
+ productUrlBase: 'https://shop.example/{lang}/products/{slug}',
138
+ },
139
+ },
140
+ },
141
+ });
142
+ ```
143
+
144
+ ## Mapping notes
145
+
146
+ - ACP item `id` maps to Reactionary `ProductVariantIdentifier.sku`.
147
+ - ACP amounts are returned as integer minor units.
148
+ - ACP product feed variant `price` comes from `price.getCustomerPrice`, which includes active customer/global campaign prices and can fall back to list prices in providers.
149
+ - ACP product feed variant `list_price` comes from `price.getListPrice`.
150
+ - ACP product feed availability comes from `inventory.getBySKU`.
151
+ - ACP payment data is passed as checkout payment-instruction protocol data with key `delegated_payment_token`.
152
+ - Fulfillment options are sourced from `checkout.getAvailableShippingMethods`.
153
+ - Payment provider information is sourced from `checkout.getAvailablePaymentMethods`, with a configurable fallback through `paymentProvider`.
154
+
155
+ Use `@reactionary/feeds` directly when you want Google Merchant, sitemap XML, or PriceRunner outputs from the same feed definitions.
156
+
157
+ Product-feed upsert/push APIs and order webhooks are separate ACP surfaces and are not implemented yet.
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./lib/acp-schemas.js";
2
+ export * from "./lib/reactionary-acp-server.js";
@@ -0,0 +1,123 @@
1
+ import * as z from "zod";
2
+ const ACPItemSchema = z.object({
3
+ id: z.string().min(1),
4
+ quantity: z.int().min(1)
5
+ });
6
+ const ACPBuyerSchema = z.object({
7
+ name: z.string().min(1),
8
+ email: z.email(),
9
+ phone_number: z.string().optional()
10
+ });
11
+ const ACPAddressSchema = z.object({
12
+ name: z.string().min(1),
13
+ line_one: z.string().min(1),
14
+ line_two: z.string().optional(),
15
+ city: z.string().min(1),
16
+ state: z.string().min(1),
17
+ country: z.string().min(1),
18
+ postal_code: z.string().min(1),
19
+ phone_number: z.string().optional()
20
+ });
21
+ const ACPCreateCheckoutSessionRequestSchema = z.object({
22
+ buyer: ACPBuyerSchema.optional(),
23
+ items: z.array(ACPItemSchema).min(1),
24
+ fulfillment_address: ACPAddressSchema.optional()
25
+ });
26
+ const ACPUpdateCheckoutSessionRequestSchema = z.object({
27
+ buyer: ACPBuyerSchema.optional(),
28
+ items: z.array(ACPItemSchema).min(1).optional(),
29
+ fulfillment_address: ACPAddressSchema.optional(),
30
+ fulfillment_option_id: z.string().optional()
31
+ });
32
+ const ACPPaymentDataSchema = z.object({
33
+ token: z.string().min(1),
34
+ provider: z.enum(["stripe", "adyen", "braintree"]),
35
+ billing_address: ACPAddressSchema.optional()
36
+ });
37
+ const ACPCompleteCheckoutSessionRequestSchema = z.object({
38
+ buyer: ACPBuyerSchema.optional(),
39
+ payment_data: ACPPaymentDataSchema
40
+ });
41
+ const ACPCheckoutSessionStateSchema = z.looseObject({
42
+ id: z.string(),
43
+ cartId: z.string(),
44
+ checkoutId: z.string(),
45
+ status: z.enum([
46
+ "not_ready_for_payment",
47
+ "ready_for_payment",
48
+ "completed",
49
+ "canceled"
50
+ ]),
51
+ buyer: ACPBuyerSchema.optional(),
52
+ fulfillmentAddress: ACPAddressSchema.optional(),
53
+ fulfillmentOptionId: z.string().optional(),
54
+ orderId: z.string().optional()
55
+ });
56
+ const ACPDescriptionSchema = z.object({
57
+ plain: z.string().optional(),
58
+ html: z.string().optional(),
59
+ markdown: z.string().optional()
60
+ });
61
+ const ACPMediaSchema = z.object({
62
+ url: z.string(),
63
+ alt_text: z.string().optional()
64
+ });
65
+ const ACPFeedPriceSchema = z.object({
66
+ amount: z.int(),
67
+ currency: z.string().length(3)
68
+ });
69
+ const ACPAvailabilitySchema = z.object({
70
+ available: z.boolean().optional(),
71
+ status: z.string().optional()
72
+ });
73
+ const ACPBarcodeSchema = z.object({
74
+ type: z.string(),
75
+ value: z.string()
76
+ });
77
+ const ACPVariantOptionSchema = z.object({
78
+ name: z.string(),
79
+ value: z.string()
80
+ });
81
+ const ACPFeedVariantSchema = z.object({
82
+ id: z.string(),
83
+ title: z.string(),
84
+ description: ACPDescriptionSchema.optional(),
85
+ url: z.string().optional(),
86
+ barcodes: z.array(ACPBarcodeSchema).optional(),
87
+ price: ACPFeedPriceSchema.optional(),
88
+ list_price: ACPFeedPriceSchema.optional(),
89
+ availability: ACPAvailabilitySchema.optional(),
90
+ variant_options: z.array(ACPVariantOptionSchema).optional(),
91
+ media: z.array(ACPMediaSchema).optional()
92
+ });
93
+ const ACPFeedProductSchema = z.object({
94
+ id: z.string(),
95
+ title: z.string().optional(),
96
+ description: ACPDescriptionSchema.optional(),
97
+ url: z.string().optional(),
98
+ media: z.array(ACPMediaSchema).optional(),
99
+ variants: z.array(ACPFeedVariantSchema).min(1)
100
+ });
101
+ const ACPProductFeedResponseSchema = z.object({
102
+ target_country: z.string().length(2).optional(),
103
+ products: z.array(ACPFeedProductSchema)
104
+ });
105
+ export {
106
+ ACPAddressSchema,
107
+ ACPAvailabilitySchema,
108
+ ACPBarcodeSchema,
109
+ ACPBuyerSchema,
110
+ ACPCheckoutSessionStateSchema,
111
+ ACPCompleteCheckoutSessionRequestSchema,
112
+ ACPCreateCheckoutSessionRequestSchema,
113
+ ACPDescriptionSchema,
114
+ ACPFeedPriceSchema,
115
+ ACPFeedProductSchema,
116
+ ACPFeedVariantSchema,
117
+ ACPItemSchema,
118
+ ACPMediaSchema,
119
+ ACPPaymentDataSchema,
120
+ ACPProductFeedResponseSchema,
121
+ ACPUpdateCheckoutSessionRequestSchema,
122
+ ACPVariantOptionSchema
123
+ };