@curless/shopify-adapter 0.5.0 → 0.6.0
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/dist/checkout.d.ts +21 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +29 -0
- package/dist/checkout.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/order-lookup.d.ts +33 -0
- package/dist/order-lookup.d.ts.map +1 -0
- package/dist/order-lookup.js +83 -0
- package/dist/order-lookup.js.map +1 -0
- package/dist/products.d.ts +54 -0
- package/dist/products.d.ts.map +1 -0
- package/dist/products.js +213 -0
- package/dist/products.js.map +1 -0
- package/package.json +11 -13
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ShopifyConfig } from './types.js';
|
|
2
|
+
export type CartLine = {
|
|
3
|
+
/** Shopify variant NUMERIC id (CatalogItem.variantId), not the gid://. */
|
|
4
|
+
variantId: string;
|
|
5
|
+
quantity: number;
|
|
6
|
+
};
|
|
7
|
+
export declare const cartPermalink: (config: Pick<ShopifyConfig, "shopDomain">, lines: CartLine[], opts?: {
|
|
8
|
+
discountCode?: string;
|
|
9
|
+
note?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Cart attributes — `attributes[key]=value`, which Shopify records on the
|
|
12
|
+
* order and shows the merchant under its additional details.
|
|
13
|
+
*
|
|
14
|
+
* This is the ONLY place an order can carry where it came from. The order's
|
|
15
|
+
* `Channel` says "Online Store" because that is literally the channel that
|
|
16
|
+
* created it, and changing that would mean being a registered sales-channel
|
|
17
|
+
* app rather than a link.
|
|
18
|
+
*/
|
|
19
|
+
attributes?: Record<string, string>;
|
|
20
|
+
}) => string;
|
|
21
|
+
//# sourceMappingURL=checkout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG;IACrB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAQF,eAAO,MAAM,aAAa,GACxB,QAAQ,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,EACzC,OAAO,QAAQ,EAAE,EACjB,OAAM;IACJ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,KACL,MAoBF,CAAC"}
|
package/dist/checkout.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// `https://shop.myshopify.com/cart/{variantId}:{qty},{variantId}:{qty}?…`
|
|
2
|
+
//
|
|
3
|
+
// The permalink is a documented Shopify entry point, and it is the reason this
|
|
4
|
+
// path needs no app permissions at all — anyone holding a variant id can build
|
|
5
|
+
// one. That also means it must never carry anything private: no buyer email, no
|
|
6
|
+
// internal ids, nothing but what the buyer is buying.
|
|
7
|
+
export const cartPermalink = (config, lines, opts = {}) => {
|
|
8
|
+
const usable = lines.filter((l) => l.variantId && Number.isInteger(l.quantity) && l.quantity > 0);
|
|
9
|
+
if (usable.length === 0) {
|
|
10
|
+
throw new Error('cartPermalink: need at least one line with a variantId and a positive quantity');
|
|
11
|
+
}
|
|
12
|
+
// A gid:// slipped in here would 404 the buyer at the checkout with no hint
|
|
13
|
+
// why, so take the numeric tail rather than trusting the caller's shape.
|
|
14
|
+
const path = usable
|
|
15
|
+
.map((l) => `${String(l.variantId).replace(/^.*\//, '')}:${l.quantity}`)
|
|
16
|
+
.join(',');
|
|
17
|
+
const qs = new URLSearchParams();
|
|
18
|
+
if (opts.discountCode)
|
|
19
|
+
qs.set('discount', opts.discountCode);
|
|
20
|
+
if (opts.note)
|
|
21
|
+
qs.set('note', opts.note);
|
|
22
|
+
for (const [k, v] of Object.entries(opts.attributes ?? {})) {
|
|
23
|
+
if (k && v)
|
|
24
|
+
qs.set(`attributes[${k}]`, v);
|
|
25
|
+
}
|
|
26
|
+
const query = qs.toString();
|
|
27
|
+
return `https://${config.shopDomain}/cart/${path}${query ? `?${query}` : ''}`;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=checkout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkout.js","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAeA,0EAA0E;AAC1E,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,sDAAsD;AACtD,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,MAAyC,EACzC,KAAiB,EACjB,OAaI,EAAE,EACE,EAAE;IACV,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;IAClG,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,gFAAgF,CACjF,CAAC;IACJ,CAAC;IACD,4EAA4E;IAC5E,yEAAyE;IACzE,MAAM,IAAI,GAAG,MAAM;SAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;SACvE,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;IACjC,IAAI,IAAI,CAAC,YAAY;QAAE,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IAC7D,IAAI,IAAI,CAAC,IAAI;QAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACzC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC;QAC3D,IAAI,CAAC,IAAI,CAAC;YAAE,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAC5B,OAAO,WAAW,MAAM,CAAC,UAAU,SAAS,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAChF,CAAC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export { shopifyConfigFromEnv, exchangeToken, shopifyGraphql } from './client.js';
|
|
2
2
|
export { fetchShopifyCatalog, productsToItems } from './catalog.js';
|
|
3
3
|
export { writebackShopifyOrder, refundShopifyOrder } from './orders.js';
|
|
4
|
+
export { fetchShopifyProducts, fetchProductVariants, resolveVariant, snapChoices, } from './products.js';
|
|
5
|
+
export { lookupShopifyOrder } from './order-lookup.js';
|
|
6
|
+
export { cartPermalink } from './checkout.js';
|
|
4
7
|
export { decimalsFor, toMinor, toMajorString } from './money.js';
|
|
8
|
+
export type { ShopifyProduct, ProductVariant, ProductOption } from './products.js';
|
|
9
|
+
export type { OrderLookupResult, OrderLookupLine } from './order-lookup.js';
|
|
10
|
+
export type { CartLine } from './checkout.js';
|
|
5
11
|
export type { CatalogItem, ShopifyConfig, OrderWriteback, WritebackResult, OrderRefund, RefundResult, } from './types.js';
|
|
6
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACjE,YAAY,EACV,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACjE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC5E,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EACV,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -15,5 +15,8 @@
|
|
|
15
15
|
export { shopifyConfigFromEnv, exchangeToken, shopifyGraphql } from './client.js';
|
|
16
16
|
export { fetchShopifyCatalog, productsToItems } from './catalog.js';
|
|
17
17
|
export { writebackShopifyOrder, refundShopifyOrder } from './orders.js';
|
|
18
|
+
export { fetchShopifyProducts, fetchProductVariants, resolveVariant, snapChoices, } from './products.js';
|
|
19
|
+
export { lookupShopifyOrder } from './order-lookup.js';
|
|
20
|
+
export { cartPermalink } from './checkout.js';
|
|
18
21
|
export { decimalsFor, toMinor, toMajorString } from './money.js';
|
|
19
22
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,8DAA8D;AAC9D,EAAE;AACF,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,iFAAiF;AACjF,uFAAuF;AACvF,4EAA4E;AAC5E,gDAAgD;AAChD,4FAA4F;AAC5F,wEAAwE;AACxE,8EAA8E;AAC9E,qEAAqE;AACrE,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,8DAA8D;AAC9D,EAAE;AACF,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,iFAAiF;AACjF,uFAAuF;AACvF,4EAA4E;AAC5E,gDAAgD;AAChD,4FAA4F;AAC5F,wEAAwE;AACxE,8EAA8E;AAC9E,qEAAqE;AACrE,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ShopifyConfig } from './types.js';
|
|
2
|
+
export type OrderLookupLine = {
|
|
3
|
+
title: string;
|
|
4
|
+
variantTitle?: string;
|
|
5
|
+
quantity: number;
|
|
6
|
+
priceMinor: number;
|
|
7
|
+
imageUrl?: string;
|
|
8
|
+
};
|
|
9
|
+
export type OrderLookupResult = {
|
|
10
|
+
found: true;
|
|
11
|
+
/** The merchant-facing number, e.g. "#1052". */
|
|
12
|
+
name: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
currency: string;
|
|
15
|
+
totalMinor: number;
|
|
16
|
+
/** Shopify's own vocabulary: PAID / REFUNDED / PARTIALLY_REFUNDED / … */
|
|
17
|
+
financialStatus: string;
|
|
18
|
+
/** UNFULFILLED / FULFILLED / …, or undefined when Shopify reports none. */
|
|
19
|
+
fulfillmentStatus?: string;
|
|
20
|
+
lines: OrderLookupLine[];
|
|
21
|
+
/** Carrier tracking URLs, when the merchant has shipped and entered them. */
|
|
22
|
+
trackingUrls: string[];
|
|
23
|
+
/** Shopify's own status page for this order — the buyer's canonical view. */
|
|
24
|
+
statusUrl?: string;
|
|
25
|
+
} | {
|
|
26
|
+
found: false;
|
|
27
|
+
reason: 'not-found';
|
|
28
|
+
};
|
|
29
|
+
export declare const lookupShopifyOrder: (config: ShopifyConfig, input: {
|
|
30
|
+
orderNumber: string;
|
|
31
|
+
email: string;
|
|
32
|
+
}, fetchImpl?: typeof fetch) => Promise<OrderLookupResult>;
|
|
33
|
+
//# sourceMappingURL=order-lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"order-lookup.d.ts","sourceRoot":"","sources":["../src/order-lookup.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IACE,KAAK,EAAE,IAAI,CAAC;IACZ,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,eAAe,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,CAAC;AAiD1C,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,aAAa,EACrB,OAAO;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC7C,YAAW,OAAO,KAAa,KAC9B,OAAO,CAAC,iBAAiB,CAoD3B,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// "Where is my order?" — for a storefront where the buyer has no account.
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ THE WHOLE DESIGN IS THE TWO-FACTOR CHECK. Checkout happens on Shopify's own
|
|
4
|
+
// page, so nothing here knows who is asking: an agent typing an order number is
|
|
5
|
+
// not proof of anything. Requiring the order number AND the email it was placed
|
|
6
|
+
// with is the same bar Shopify's own guest order lookup sets — one alone is
|
|
7
|
+
// guessable (order numbers are sequential), both together are not.
|
|
8
|
+
//
|
|
9
|
+
// So: never return an order because the number matched. The email must match
|
|
10
|
+
// too, and a mismatch is reported exactly like a miss — telling the caller
|
|
11
|
+
// "right order, wrong email" would confirm that the order exists, which is half
|
|
12
|
+
// the answer they were not entitled to.
|
|
13
|
+
import { exchangeToken, shopifyGraphql } from './client.js';
|
|
14
|
+
import { toMinor } from './money.js';
|
|
15
|
+
const QUERY = `
|
|
16
|
+
query FindOrder($q: String!) {
|
|
17
|
+
orders(first: 5, query: $q) {
|
|
18
|
+
nodes {
|
|
19
|
+
name createdAt email
|
|
20
|
+
displayFinancialStatus displayFulfillmentStatus statusPageUrl
|
|
21
|
+
currentTotalPriceSet { shopMoney { amount currencyCode } }
|
|
22
|
+
lineItems(first: 20) {
|
|
23
|
+
nodes {
|
|
24
|
+
title variantTitle quantity
|
|
25
|
+
originalUnitPriceSet { shopMoney { amount } }
|
|
26
|
+
image { url }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
fulfillments(first: 10) { trackingInfo { url } }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
// Shopify's search syntax is space-separated terms; a stray quote or colon in
|
|
35
|
+
// caller input would change the query's meaning rather than fail it. Keep only
|
|
36
|
+
// what an order number can legitimately contain.
|
|
37
|
+
const safeOrderNumber = (raw) => raw.trim().replace(/[^0-9A-Za-z#_-]/g, '');
|
|
38
|
+
export const lookupShopifyOrder = async (config, input, fetchImpl = fetch) => {
|
|
39
|
+
const number = safeOrderNumber(input.orderNumber);
|
|
40
|
+
const email = input.email.trim().toLowerCase();
|
|
41
|
+
// Both are required. An empty one must not degrade into "match on the other".
|
|
42
|
+
if (!number || !email.includes('@'))
|
|
43
|
+
return { found: false, reason: 'not-found' };
|
|
44
|
+
const token = await exchangeToken(config, fetchImpl);
|
|
45
|
+
const withHash = number.startsWith('#') ? number : `#${number}`;
|
|
46
|
+
const data = await shopifyGraphql(config, token, QUERY, { q: `name:${JSON.stringify(withHash)}` }, fetchImpl);
|
|
47
|
+
const order = data.orders.nodes.find((o) => o.name === withHash && (o.email ?? '').trim().toLowerCase() === email);
|
|
48
|
+
// Deliberately the same answer as "no such order": see the header.
|
|
49
|
+
if (!order)
|
|
50
|
+
return { found: false, reason: 'not-found' };
|
|
51
|
+
const currency = order.currentTotalPriceSet.shopMoney.currencyCode;
|
|
52
|
+
const minor = (amount) => {
|
|
53
|
+
try {
|
|
54
|
+
return toMinor(amount, currency);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
found: true,
|
|
62
|
+
name: order.name,
|
|
63
|
+
createdAt: order.createdAt,
|
|
64
|
+
currency,
|
|
65
|
+
totalMinor: minor(order.currentTotalPriceSet.shopMoney.amount),
|
|
66
|
+
financialStatus: order.displayFinancialStatus ?? 'UNKNOWN',
|
|
67
|
+
...(order.displayFulfillmentStatus
|
|
68
|
+
? { fulfillmentStatus: order.displayFulfillmentStatus }
|
|
69
|
+
: {}),
|
|
70
|
+
lines: order.lineItems.nodes.map((l) => ({
|
|
71
|
+
title: l.title,
|
|
72
|
+
...(l.variantTitle ? { variantTitle: l.variantTitle } : {}),
|
|
73
|
+
quantity: l.quantity,
|
|
74
|
+
priceMinor: minor(l.originalUnitPriceSet.shopMoney.amount),
|
|
75
|
+
...(l.image?.url ? { imageUrl: l.image.url } : {}),
|
|
76
|
+
})),
|
|
77
|
+
trackingUrls: order.fulfillments
|
|
78
|
+
.flatMap((f) => f.trackingInfo.map((t) => t.url))
|
|
79
|
+
.filter((u) => Boolean(u)),
|
|
80
|
+
...(order.statusPageUrl ? { statusUrl: order.statusPageUrl } : {}),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=order-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"order-lookup.js","sourceRoot":"","sources":["../src/order-lookup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ShopifyConfig } from './types.js';
|
|
2
|
+
export type ProductOption = {
|
|
3
|
+
/** The merchant's own label — "Color", "度数", "Size". Never normalized. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** In the merchant's own order, which is usually meaningful (-0.00 → -5.00). */
|
|
6
|
+
values: string[];
|
|
7
|
+
};
|
|
8
|
+
export type ProductVariant = {
|
|
9
|
+
/** Numeric variant id — what a cart permalink takes. */
|
|
10
|
+
variantId: string;
|
|
11
|
+
sku: string | null;
|
|
12
|
+
/** Shopify's own joined title, e.g. "Teal / -2.00". */
|
|
13
|
+
title: string;
|
|
14
|
+
priceMinor: number;
|
|
15
|
+
/** Struck-through price when the merchant set one, else undefined. */
|
|
16
|
+
compareAtMinor?: number;
|
|
17
|
+
available: boolean;
|
|
18
|
+
/** Which option values select this variant — the picker matches on these. */
|
|
19
|
+
options: {
|
|
20
|
+
name: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}[];
|
|
23
|
+
imageUrl?: string;
|
|
24
|
+
};
|
|
25
|
+
export type ShopifyProduct = {
|
|
26
|
+
/** Shopify numeric product id. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** URL slug — the storefront link a buyer can be sent to. */
|
|
29
|
+
handle: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
productType: string;
|
|
33
|
+
tags: string[];
|
|
34
|
+
currency: string;
|
|
35
|
+
imageUrl?: string;
|
|
36
|
+
images: string[];
|
|
37
|
+
options: ProductOption[];
|
|
38
|
+
/**
|
|
39
|
+
* EMPTY from the bulk read. A pair of lenses is Left × Right, 676 variants,
|
|
40
|
+
* and 382 of those products is a quarter-million objects held in memory to
|
|
41
|
+
* answer "what colours do you have" — while still not being complete enough to
|
|
42
|
+
* resolve a purchase. Load them per product, when someone is buying one.
|
|
43
|
+
*/
|
|
44
|
+
variants: ProductVariant[];
|
|
45
|
+
/** False until `fetchProductVariants` has filled them. Guards the resolver. */
|
|
46
|
+
variantsLoaded: boolean;
|
|
47
|
+
/** Cheapest available variant, for a list that shows one price per product. */
|
|
48
|
+
fromPriceMinor?: number;
|
|
49
|
+
};
|
|
50
|
+
export declare const fetchShopifyProducts: (config: ShopifyConfig, fetchImpl?: typeof fetch, warn?: (msg: string) => void) => Promise<ShopifyProduct[]>;
|
|
51
|
+
export declare const resolveVariant: (variants: ProductVariant[], choices: Record<string, string>) => ProductVariant | null;
|
|
52
|
+
export declare const snapChoices: (options: ProductOption[], choices: Record<string, string>) => Record<string, string>;
|
|
53
|
+
export declare const fetchProductVariants: (config: ShopifyConfig, handle: string, fetchImpl?: typeof fetch, warn?: (msg: string) => void) => Promise<ProductVariant[]>;
|
|
54
|
+
//# sourceMappingURL=products.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"products.d.ts","sourceRoot":"","sources":["../src/products.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG;IAC1B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,kCAAkC;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,cAAc,EAAE,OAAO,CAAC;IACxB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AA2DF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,aAAa,EACrB,YAAW,OAAO,KAAa,EAC/B,OAAM,CAAC,GAAG,EAAE,MAAM,KAAK,IAA4C,KAClE,OAAO,CAAC,cAAc,EAAE,CAmD1B,CAAC;AAiCF,eAAO,MAAM,cAAc,GACzB,UAAU,cAAc,EAAE,EAC1B,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,cAAc,GAAG,IASnB,CAAC;AAeF,eAAO,MAAM,WAAW,GACtB,SAAS,aAAa,EAAE,EACxB,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,CASvB,CAAC;AAyBF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,aAAa,EACrB,QAAQ,MAAM,EACd,YAAW,OAAO,KAAa,EAC/B,OAAM,CAAC,GAAG,EAAE,MAAM,KAAK,IAA4C,KAClE,OAAO,CAAC,cAAc,EAAE,CAuD1B,CAAC"}
|
package/dist/products.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Shopify store → PRODUCTS with their option matrix intact.
|
|
2
|
+
//
|
|
3
|
+
// `fetchShopifyCatalog` flattens every variant into a standalone CatalogItem,
|
|
4
|
+
// which is right for a gateway quoting one sku. It is wrong for a storefront:
|
|
5
|
+
// a buyer picks a product, then picks its options, and the two choices together
|
|
6
|
+
// name the variant. Contact lenses are the clearest case — one product is
|
|
7
|
+
// Colour × Prescription, and "Jade Flare" alone cannot be bought.
|
|
8
|
+
//
|
|
9
|
+
// So this returns the shape a picker needs: the option names in the merchant's
|
|
10
|
+
// own words, their values in the merchant's own order, and every variant with
|
|
11
|
+
// the option values that select it.
|
|
12
|
+
import { exchangeToken, shopifyGraphql } from './client.js';
|
|
13
|
+
import { toMinor } from './money.js';
|
|
14
|
+
const PAGE = 50;
|
|
15
|
+
// Shopify's own ceiling for a connection page. A pair of contact lenses is
|
|
16
|
+
// Left × Right — 26 × 26 = 676 variants — so even the maximum does not fetch one
|
|
17
|
+
// product whole, which is exactly why the bulk read no longer tries.
|
|
18
|
+
const VARIANT_PAGE = 250;
|
|
19
|
+
const QUERY = `
|
|
20
|
+
query Products($first: Int!, $after: String) {
|
|
21
|
+
shop { currencyCode }
|
|
22
|
+
products(first: $first, after: $after, query: "status:active") {
|
|
23
|
+
pageInfo { hasNextPage endCursor }
|
|
24
|
+
nodes {
|
|
25
|
+
id handle title description productType tags status
|
|
26
|
+
featuredImage { url }
|
|
27
|
+
media(first: 8) { nodes { preview { image { url } } } }
|
|
28
|
+
options { name optionValues { name } }
|
|
29
|
+
priceRangeV2 { minVariantPrice { amount currencyCode } }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
// `gid://shopify/ProductVariant/123` → `123`. Cart permalinks and the Admin URL
|
|
35
|
+
// both want the bare number, and a gid slipped into either fails in a way that
|
|
36
|
+
// says nothing about why.
|
|
37
|
+
const numericId = (gid) => gid.replace(/^.*\//, '');
|
|
38
|
+
export const fetchShopifyProducts = async (config, fetchImpl = fetch, warn = (m) => console.warn(`[shopify] ${m}`)) => {
|
|
39
|
+
const token = await exchangeToken(config, fetchImpl);
|
|
40
|
+
const out = [];
|
|
41
|
+
let cursor = null;
|
|
42
|
+
let currency = 'USD';
|
|
43
|
+
do {
|
|
44
|
+
const page = await shopifyGraphql(config, token, QUERY, { first: PAGE, after: cursor }, fetchImpl);
|
|
45
|
+
currency = page.shop?.currencyCode ?? currency;
|
|
46
|
+
for (const p of page.products.nodes) {
|
|
47
|
+
let fromPriceMinor;
|
|
48
|
+
const from = p.priceRangeV2?.minVariantPrice;
|
|
49
|
+
if (from) {
|
|
50
|
+
try {
|
|
51
|
+
fromPriceMinor = toMinor(from.amount, from.currencyCode || currency);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
warn(`"${p.title}" has an unparseable price ${JSON.stringify(from.amount)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const images = p.media.nodes
|
|
58
|
+
.map((n) => n.preview?.image?.url)
|
|
59
|
+
.filter((u) => Boolean(u));
|
|
60
|
+
out.push({
|
|
61
|
+
id: numericId(p.id),
|
|
62
|
+
handle: p.handle,
|
|
63
|
+
title: p.title,
|
|
64
|
+
description: p.description ?? '',
|
|
65
|
+
productType: p.productType ?? '',
|
|
66
|
+
tags: p.tags ?? [],
|
|
67
|
+
currency,
|
|
68
|
+
...(p.featuredImage?.url ? { imageUrl: p.featuredImage.url } : {}),
|
|
69
|
+
images,
|
|
70
|
+
options: p.options.map((o) => ({
|
|
71
|
+
name: o.name,
|
|
72
|
+
values: o.optionValues.map((v) => v.name),
|
|
73
|
+
})),
|
|
74
|
+
// EMPTY by design — see the type. Fetch them for the one product a buyer
|
|
75
|
+
// is actually looking at.
|
|
76
|
+
variants: [],
|
|
77
|
+
variantsLoaded: false,
|
|
78
|
+
...(fromPriceMinor !== undefined ? { fromPriceMinor } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
cursor = page.products.pageInfo.hasNextPage ? page.products.pageInfo.endCursor : null;
|
|
82
|
+
} while (cursor);
|
|
83
|
+
return out;
|
|
84
|
+
};
|
|
85
|
+
// Resolve a buyer's choices to ONE variant. Takes the VARIANTS, not the product:
|
|
86
|
+
// a product from the bulk read carries none, and passing it here would resolve
|
|
87
|
+
// to "nothing matches" — indistinguishable from a prescription the store does
|
|
88
|
+
// not stock, and the caller would ask the buyer a question they already answered.
|
|
89
|
+
//
|
|
90
|
+
// Option names and values are matched case- and space-insensitively because the
|
|
91
|
+
// buyer is typing, not clicking: they say "left" for "Left" and "-2.0" for
|
|
92
|
+
// "-2.00".
|
|
93
|
+
//
|
|
94
|
+
// Returns null when the choices do not name exactly one variant — the caller
|
|
95
|
+
// must then ask rather than pick, because guessing here means shipping someone
|
|
96
|
+
// the wrong prescription.
|
|
97
|
+
const norm = (s) => s.trim().toLowerCase().replace(/\s+/g, '');
|
|
98
|
+
// A numeric choice compares numerically: "-2.0", "-2.00" and "−2.00" (with a
|
|
99
|
+
// unicode minus, which is what a phone keyboard produces) are one value. The
|
|
100
|
+
// empty string is NOT a number here — Number('') is 0, and a blank answer that
|
|
101
|
+
// silently meant "0.00 dioptres" would be a plano lens nobody asked for.
|
|
102
|
+
const asNumber = (s) => {
|
|
103
|
+
const t = s.trim().replace(/[−–—]/g, '-');
|
|
104
|
+
if (t === '')
|
|
105
|
+
return null;
|
|
106
|
+
const n = Number(t);
|
|
107
|
+
return Number.isFinite(n) ? n : null;
|
|
108
|
+
};
|
|
109
|
+
/** Whether two option names — or two option values — mean the same thing. */
|
|
110
|
+
const same = (a, b) => {
|
|
111
|
+
const [na, nb] = [asNumber(a), asNumber(b)];
|
|
112
|
+
return na !== null && nb !== null ? na === nb : norm(a) === norm(b);
|
|
113
|
+
};
|
|
114
|
+
export const resolveVariant = (variants, choices) => {
|
|
115
|
+
const wanted = Object.entries(choices).filter(([, v]) => v !== undefined && v !== '');
|
|
116
|
+
if (wanted.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
const matches = variants.filter((v) => wanted.every(([name, value]) => v.options.some((o) => same(o.name, name) && same(o.value, value))));
|
|
119
|
+
return matches.length === 1 ? (matches[0] ?? null) : null;
|
|
120
|
+
};
|
|
121
|
+
// The buyer's choices, rewritten in the STORE's own words — `{left: '-2.0'}`
|
|
122
|
+
// against a `Left: -2.00` option becomes `{Left: '-2.00'}`.
|
|
123
|
+
//
|
|
124
|
+
// This exists because those choices get handed onwards. A picker that shows the
|
|
125
|
+
// buyer what was understood so far has to compare them against the option values
|
|
126
|
+
// it draws, and comparing `-2.0` to `-2.00` as strings highlights nothing while
|
|
127
|
+
// still counting as answered — so the buyer sees an unanswered question with the
|
|
128
|
+
// Buy button live, presses it, and gets the same screen back. Snapping here, in
|
|
129
|
+
// the one place that already knows how these compare, keeps that logic from
|
|
130
|
+
// being reimplemented (differently) further out.
|
|
131
|
+
//
|
|
132
|
+
// Anything that does not name exactly one value is DROPPED, not guessed: an
|
|
133
|
+
// unrecognised prescription has to come back as a question.
|
|
134
|
+
export const snapChoices = (options, choices) => {
|
|
135
|
+
const out = {};
|
|
136
|
+
for (const [name, value] of Object.entries(choices)) {
|
|
137
|
+
if (value === undefined || value === '')
|
|
138
|
+
continue;
|
|
139
|
+
const opt = options.find((o) => same(o.name, name));
|
|
140
|
+
const hits = opt?.values.filter((v) => same(v, value)) ?? [];
|
|
141
|
+
if (opt && hits.length === 1 && hits[0] !== undefined)
|
|
142
|
+
out[opt.name] = hits[0];
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
};
|
|
146
|
+
const VARIANTS_QUERY = `
|
|
147
|
+
query ProductVariants($handle: String!, $first: Int!, $after: String) {
|
|
148
|
+
shop { currencyCode }
|
|
149
|
+
productByHandle(handle: $handle) {
|
|
150
|
+
variants(first: $first, after: $after) {
|
|
151
|
+
pageInfo { hasNextPage endCursor }
|
|
152
|
+
nodes {
|
|
153
|
+
id sku title price compareAtPrice availableForSale
|
|
154
|
+
selectedOptions { name value }
|
|
155
|
+
image { url }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
// Every variant of ONE product, paginated to completion.
|
|
162
|
+
//
|
|
163
|
+
// Completeness is the point. The bulk read used to take the first 100 of 676 and
|
|
164
|
+
// warn about the rest — the warning was accurate and the behaviour was still
|
|
165
|
+
// wrong, because a buyer asking for -6.50 in the right eye was told it does not
|
|
166
|
+
// exist when it merely sat past a page boundary. Most of the store was
|
|
167
|
+
// unbuyable and every refusal looked like a legitimate "we don't stock that".
|
|
168
|
+
export const fetchProductVariants = async (config, handle, fetchImpl = fetch, warn = (m) => console.warn(`[shopify] ${m}`)) => {
|
|
169
|
+
const token = await exchangeToken(config, fetchImpl);
|
|
170
|
+
const out = [];
|
|
171
|
+
let cursor = null;
|
|
172
|
+
let currency = 'USD';
|
|
173
|
+
do {
|
|
174
|
+
const page = await shopifyGraphql(config, token, VARIANTS_QUERY, { handle, first: VARIANT_PAGE, after: cursor }, fetchImpl);
|
|
175
|
+
currency = page.shop?.currencyCode ?? currency;
|
|
176
|
+
const conn = page.productByHandle?.variants;
|
|
177
|
+
if (!conn)
|
|
178
|
+
return out;
|
|
179
|
+
for (const v of conn.nodes) {
|
|
180
|
+
let priceMinor;
|
|
181
|
+
try {
|
|
182
|
+
priceMinor = toMinor(v.price, currency);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
warn(`skipping ${handle} / ${v.title} — unparseable price ${JSON.stringify(v.price)}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let compareAtMinor;
|
|
189
|
+
if (v.compareAtPrice) {
|
|
190
|
+
try {
|
|
191
|
+
compareAtMinor = toMinor(v.compareAtPrice, currency);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Cosmetic only — drop the strike-through, keep the variant buyable.
|
|
195
|
+
compareAtMinor = undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
out.push({
|
|
199
|
+
variantId: numericId(v.id),
|
|
200
|
+
sku: v.sku,
|
|
201
|
+
title: v.title,
|
|
202
|
+
priceMinor,
|
|
203
|
+
...(compareAtMinor !== undefined ? { compareAtMinor } : {}),
|
|
204
|
+
available: v.availableForSale,
|
|
205
|
+
options: v.selectedOptions ?? [],
|
|
206
|
+
...(v.image?.url ? { imageUrl: v.image.url } : {}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
cursor = conn.pageInfo.hasNextPage ? conn.pageInfo.endCursor : null;
|
|
210
|
+
} while (cursor);
|
|
211
|
+
return out;
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=products.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"products.js","sourceRoot":"","sources":["../src/products.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@curless/shopify-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Use a Shopify store as an agentbank catalog source, and record agent-settled sales back as paid Shopify orders. Maps Shopify Admin products → a generic CatalogItem your merchant backend prices from, writes an order back after an agent pays through agentbank, and records the reversal when that sale is refunded. Zero runtime dependencies (uses the platform fetch).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"README.md"
|
|
18
|
-
],
|
|
15
|
+
"files": ["dist", "README.md"],
|
|
19
16
|
"publishConfig": {
|
|
20
17
|
"access": "public"
|
|
21
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
25
|
+
},
|
|
22
26
|
"devDependencies": {
|
|
23
27
|
"@types/node": "^22.10.1",
|
|
24
28
|
"typescript": "^5.6.3",
|
|
@@ -41,11 +45,5 @@
|
|
|
41
45
|
"url": "git+https://gitlab.com/robin-ruan/agentbank.git",
|
|
42
46
|
"directory": "packages/sdks/shopify-adapter"
|
|
43
47
|
},
|
|
44
|
-
"author": "robin"
|
|
45
|
-
|
|
46
|
-
"build": "tsc",
|
|
47
|
-
"typecheck": "tsc --noEmit",
|
|
48
|
-
"test": "vitest run",
|
|
49
|
-
"clean": "rm -rf dist"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
48
|
+
"author": "robin"
|
|
49
|
+
}
|