@curless/shopify-adapter 0.5.0 → 0.7.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.
@@ -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"}
@@ -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"}
@@ -0,0 +1,51 @@
1
+ import type { ShopifyConfig } from './types.js';
2
+ export type ShopifyDiscount = {
3
+ /**
4
+ * The merchant's own words, which is usually the whole story — they write
5
+ * "Buy 2 get 5% OFF | Buy 3 get 10% OFF" precisely because it is what the
6
+ * shopper reads on the product page.
7
+ */
8
+ title: string;
9
+ /** ACTIVE / SCHEDULED / EXPIRED, straight from Shopify. */
10
+ status: string;
11
+ /**
12
+ * How it applies, when Shopify says so in a form we can read:
13
+ * percentage a plain "10% off" automatic discount
14
+ * amount a fixed sum off
15
+ * bxgy buy X get Y
16
+ * app a discount FUNCTION — the tiers live in the app's own
17
+ * metafields and are not readable generically. The title is
18
+ * what we have, and it is usually enough.
19
+ */
20
+ kind: 'percentage' | 'amount' | 'bxgy' | 'app' | 'unknown';
21
+ /** For `percentage`, e.g. 0.05 for 5%. */
22
+ percentage?: number;
23
+ /** For `amount`, in minor units of the shop's currency. */
24
+ amountOffMinor?: number;
25
+ /** Minimum spend that unlocks it, in minor units, when there is one. */
26
+ minSubtotalMinor?: number;
27
+ /** Minimum quantity that unlocks it, when there is one. */
28
+ minQuantity?: number;
29
+ /** ISO timestamps, for a promotion that has not started or is ending. */
30
+ startsAt?: string;
31
+ endsAt?: string;
32
+ };
33
+ export type DiscountsResult = {
34
+ ok: true;
35
+ discounts: ShopifyDiscount[];
36
+ }
37
+ /**
38
+ * The token predates the scope. Distinguished from "no discounts" on purpose:
39
+ * one means the shop is not running any, the other means we cannot see them,
40
+ * and telling a merchant their promotions are invisible is only possible if we
41
+ * know which it is.
42
+ */
43
+ | {
44
+ ok: false;
45
+ reason: 'unauthorized' | 'failed';
46
+ detail: string;
47
+ };
48
+ export declare const fetchShopifyDiscounts: (config: ShopifyConfig, fetchImpl?: typeof fetch, first?: number) => Promise<DiscountsResult>;
49
+ /** Only what a shopper can actually get right now. */
50
+ export declare const activeDiscounts: (discounts: ShopifyDiscount[], now?: Date) => ShopifyDiscount[];
51
+ //# sourceMappingURL=discounts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discounts.d.ts","sourceRoot":"","sources":["../src/discounts.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,IAAI,EAAE,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;IAC3D,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,eAAe,EAAE,CAAA;CAAE;AAC5C;;;;;GAKG;GACD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAqDrE,eAAO,MAAM,qBAAqB,GAChC,QAAQ,aAAa,EACrB,YAAW,OAAO,KAAa,EAC/B,cAAU,KACT,OAAO,CAAC,eAAe,CAmDzB,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,eAAe,GAC1B,WAAW,eAAe,EAAE,EAC5B,UAAgB,KACf,eAAe,EAMd,CAAC"}
@@ -0,0 +1,119 @@
1
+ // The shop's live promotions — "Buy 2 get 5% OFF", "Gift over $66".
2
+ //
3
+ // These are NOT on the product or the variant. A merchant sets them up as
4
+ // automatic discounts, so nothing about a lens says it is 5% off in threes; the
5
+ // storefront reads them separately and prints the banner. A catalogue that only
6
+ // reads products will show a price the buyer beats at checkout, and never
7
+ // mention the offer that made them buy two.
8
+ //
9
+ // ⚠️ Needs the `read_discounts` scope. Scopes are granted at INSTALL, so adding
10
+ // it to an app's config does nothing until the merchant reinstalls — until then
11
+ // the query 403s. That must not take the storefront down, so `unauthorized` is a
12
+ // first-class result rather than an exception: the catalogue keeps serving and
13
+ // the caller can say what is missing and why.
14
+ import { exchangeToken, shopifyGraphql } from './client.js';
15
+ import { toMinor } from './money.js';
16
+ // Automatic discounts only. Code discounts need the shopper to type a code, and
17
+ // a storefront advertising one it cannot apply is worse than silence.
18
+ const QUERY = `
19
+ query Discounts($first: Int!) {
20
+ shop { currencyCode }
21
+ automaticDiscountNodes(first: $first) {
22
+ nodes {
23
+ automaticDiscount {
24
+ __typename
25
+ ... on DiscountAutomaticBasic {
26
+ title status startsAt endsAt
27
+ customerGets {
28
+ value {
29
+ ... on DiscountPercentage { percentage }
30
+ ... on DiscountAmount { amount { amount } }
31
+ }
32
+ }
33
+ minimumRequirement {
34
+ ... on DiscountMinimumSubtotal { greaterThanOrEqualToSubtotal { amount } }
35
+ ... on DiscountMinimumQuantity { greaterThanOrEqualToQuantity }
36
+ }
37
+ }
38
+ ... on DiscountAutomaticBxgy { title status startsAt endsAt }
39
+ ... on DiscountAutomaticApp { title status startsAt endsAt }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ `;
45
+ const kindOf = (typename) => {
46
+ if (typename === 'DiscountAutomaticBxgy')
47
+ return 'bxgy';
48
+ if (typename === 'DiscountAutomaticApp')
49
+ return 'app';
50
+ return 'unknown';
51
+ };
52
+ export const fetchShopifyDiscounts = async (config, fetchImpl = fetch, first = 25) => {
53
+ let token;
54
+ try {
55
+ token = await exchangeToken(config, fetchImpl);
56
+ }
57
+ catch (err) {
58
+ return { ok: false, reason: 'failed', detail: err instanceof Error ? err.message : 'auth' };
59
+ }
60
+ let page;
61
+ try {
62
+ page = await shopifyGraphql(config, token, QUERY, { first }, fetchImpl);
63
+ }
64
+ catch (err) {
65
+ const detail = err instanceof Error ? err.message : String(err);
66
+ // Shopify says ACCESS_DENIED when the scope is missing. Anything else is a
67
+ // real failure and should read as one.
68
+ const denied = /ACCESS_DENIED|access denied|not approved|required access/i.test(detail);
69
+ return { ok: false, reason: denied ? 'unauthorized' : 'failed', detail };
70
+ }
71
+ const currency = page.shop?.currencyCode ?? 'USD';
72
+ const out = [];
73
+ for (const n of page.automaticDiscountNodes?.nodes ?? []) {
74
+ const d = n.automaticDiscount;
75
+ if (!d?.title)
76
+ continue;
77
+ const pct = d.customerGets?.value?.percentage;
78
+ const amt = d.customerGets?.value?.amount?.amount;
79
+ const minSub = d.minimumRequirement?.greaterThanOrEqualToSubtotal?.amount;
80
+ const minQty = d.minimumRequirement?.greaterThanOrEqualToQuantity;
81
+ // A price that cannot be parsed is left off rather than guessed — a wrong
82
+ // discount is a wrong price, and the buyer finds out at checkout.
83
+ const money = (v) => {
84
+ if (v === undefined)
85
+ return undefined;
86
+ try {
87
+ return toMinor(v, currency);
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ };
93
+ out.push({
94
+ title: d.title,
95
+ status: d.status ?? 'UNKNOWN',
96
+ kind: pct !== undefined ? 'percentage' : amt !== undefined ? 'amount' : kindOf(d.__typename),
97
+ ...(pct !== undefined ? { percentage: pct } : {}),
98
+ ...(money(amt) !== undefined ? { amountOffMinor: money(amt) } : {}),
99
+ ...(money(minSub) !== undefined ? { minSubtotalMinor: money(minSub) } : {}),
100
+ ...(minQty !== undefined && Number.isFinite(Number(minQty))
101
+ ? { minQuantity: Number(minQty) }
102
+ : {}),
103
+ ...(d.startsAt ? { startsAt: d.startsAt } : {}),
104
+ ...(d.endsAt ? { endsAt: d.endsAt } : {}),
105
+ });
106
+ }
107
+ return { ok: true, discounts: out };
108
+ };
109
+ /** Only what a shopper can actually get right now. */
110
+ export const activeDiscounts = (discounts, now = new Date()) => discounts.filter((d) => {
111
+ if (d.status !== 'ACTIVE')
112
+ return false;
113
+ if (d.startsAt && new Date(d.startsAt) > now)
114
+ return false;
115
+ if (d.endsAt && new Date(d.endsAt) < now)
116
+ return false;
117
+ return true;
118
+ });
119
+ //# sourceMappingURL=discounts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discounts.js","sourceRoot":"","sources":["../src/discounts.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,14 @@
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 { fetchShopifyDiscounts, activeDiscounts } from './discounts.js';
7
+ export { cartPermalink } from './checkout.js';
4
8
  export { decimalsFor, toMinor, toMajorString } from './money.js';
9
+ export type { ShopifyProduct, ProductVariant, ProductOption } from './products.js';
10
+ export type { OrderLookupResult, OrderLookupLine } from './order-lookup.js';
11
+ export type { ShopifyDiscount, DiscountsResult } from './discounts.js';
12
+ export type { CartLine } from './checkout.js';
5
13
  export type { CatalogItem, ShopifyConfig, OrderWriteback, WritebackResult, OrderRefund, RefundResult, } from './types.js';
6
14
  //# sourceMappingURL=index.d.ts.map
@@ -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,qBAAqB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxE,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,eAAe,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACvE,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,9 @@
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 { fetchShopifyDiscounts, activeDiscounts } from './discounts.js';
21
+ export { cartPermalink } from './checkout.js';
18
22
  export { decimalsFor, toMinor, toMajorString } from './money.js';
19
23
  //# 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,qBAAqB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxE,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,61 @@
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
+ /**
31
+ * The product's page on the merchant's OWN storefront, built from the shop's
32
+ * primary domain rather than the *.myshopify.com host — that is the address
33
+ * they advertise, the one with their reviews and size guide on it, and the one
34
+ * a buyer should land on if they want to read more before choosing.
35
+ */
36
+ url?: string;
37
+ title: string;
38
+ description: string;
39
+ productType: string;
40
+ tags: string[];
41
+ currency: string;
42
+ imageUrl?: string;
43
+ images: string[];
44
+ options: ProductOption[];
45
+ /**
46
+ * EMPTY from the bulk read. A pair of lenses is Left × Right, 676 variants,
47
+ * and 382 of those products is a quarter-million objects held in memory to
48
+ * answer "what colours do you have" — while still not being complete enough to
49
+ * resolve a purchase. Load them per product, when someone is buying one.
50
+ */
51
+ variants: ProductVariant[];
52
+ /** False until `fetchProductVariants` has filled them. Guards the resolver. */
53
+ variantsLoaded: boolean;
54
+ /** Cheapest available variant, for a list that shows one price per product. */
55
+ fromPriceMinor?: number;
56
+ };
57
+ export declare const fetchShopifyProducts: (config: ShopifyConfig, fetchImpl?: typeof fetch, warn?: (msg: string) => void) => Promise<ShopifyProduct[]>;
58
+ export declare const resolveVariant: (variants: ProductVariant[], choices: Record<string, string>) => ProductVariant | null;
59
+ export declare const snapChoices: (options: ProductOption[], choices: Record<string, string>) => Record<string, string>;
60
+ export declare const fetchProductVariants: (config: ShopifyConfig, handle: string, fetchImpl?: typeof fetch, warn?: (msg: string) => void) => Promise<ProductVariant[]>;
61
+ //# 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;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,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,CAsD1B,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"}
@@ -0,0 +1,216 @@
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 primaryDomain { url } }
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
+ let storefront;
44
+ do {
45
+ const page = await shopifyGraphql(config, token, QUERY, { first: PAGE, after: cursor }, fetchImpl);
46
+ currency = page.shop?.currencyCode ?? currency;
47
+ storefront = page.shop?.primaryDomain?.url ?? storefront;
48
+ for (const p of page.products.nodes) {
49
+ let fromPriceMinor;
50
+ const from = p.priceRangeV2?.minVariantPrice;
51
+ if (from) {
52
+ try {
53
+ fromPriceMinor = toMinor(from.amount, from.currencyCode || currency);
54
+ }
55
+ catch {
56
+ warn(`"${p.title}" has an unparseable price ${JSON.stringify(from.amount)}`);
57
+ }
58
+ }
59
+ const images = p.media.nodes
60
+ .map((n) => n.preview?.image?.url)
61
+ .filter((u) => Boolean(u));
62
+ out.push({
63
+ id: numericId(p.id),
64
+ handle: p.handle,
65
+ ...(storefront ? { url: `${storefront.replace(/\/$/, '')}/products/${p.handle}` } : {}),
66
+ title: p.title,
67
+ description: p.description ?? '',
68
+ productType: p.productType ?? '',
69
+ tags: p.tags ?? [],
70
+ currency,
71
+ ...(p.featuredImage?.url ? { imageUrl: p.featuredImage.url } : {}),
72
+ images,
73
+ options: p.options.map((o) => ({
74
+ name: o.name,
75
+ values: o.optionValues.map((v) => v.name),
76
+ })),
77
+ // EMPTY by design — see the type. Fetch them for the one product a buyer
78
+ // is actually looking at.
79
+ variants: [],
80
+ variantsLoaded: false,
81
+ ...(fromPriceMinor !== undefined ? { fromPriceMinor } : {}),
82
+ });
83
+ }
84
+ cursor = page.products.pageInfo.hasNextPage ? page.products.pageInfo.endCursor : null;
85
+ } while (cursor);
86
+ return out;
87
+ };
88
+ // Resolve a buyer's choices to ONE variant. Takes the VARIANTS, not the product:
89
+ // a product from the bulk read carries none, and passing it here would resolve
90
+ // to "nothing matches" — indistinguishable from a prescription the store does
91
+ // not stock, and the caller would ask the buyer a question they already answered.
92
+ //
93
+ // Option names and values are matched case- and space-insensitively because the
94
+ // buyer is typing, not clicking: they say "left" for "Left" and "-2.0" for
95
+ // "-2.00".
96
+ //
97
+ // Returns null when the choices do not name exactly one variant — the caller
98
+ // must then ask rather than pick, because guessing here means shipping someone
99
+ // the wrong prescription.
100
+ const norm = (s) => s.trim().toLowerCase().replace(/\s+/g, '');
101
+ // A numeric choice compares numerically: "-2.0", "-2.00" and "−2.00" (with a
102
+ // unicode minus, which is what a phone keyboard produces) are one value. The
103
+ // empty string is NOT a number here — Number('') is 0, and a blank answer that
104
+ // silently meant "0.00 dioptres" would be a plano lens nobody asked for.
105
+ const asNumber = (s) => {
106
+ const t = s.trim().replace(/[−–—]/g, '-');
107
+ if (t === '')
108
+ return null;
109
+ const n = Number(t);
110
+ return Number.isFinite(n) ? n : null;
111
+ };
112
+ /** Whether two option names — or two option values — mean the same thing. */
113
+ const same = (a, b) => {
114
+ const [na, nb] = [asNumber(a), asNumber(b)];
115
+ return na !== null && nb !== null ? na === nb : norm(a) === norm(b);
116
+ };
117
+ export const resolveVariant = (variants, choices) => {
118
+ const wanted = Object.entries(choices).filter(([, v]) => v !== undefined && v !== '');
119
+ if (wanted.length === 0)
120
+ return null;
121
+ const matches = variants.filter((v) => wanted.every(([name, value]) => v.options.some((o) => same(o.name, name) && same(o.value, value))));
122
+ return matches.length === 1 ? (matches[0] ?? null) : null;
123
+ };
124
+ // The buyer's choices, rewritten in the STORE's own words — `{left: '-2.0'}`
125
+ // against a `Left: -2.00` option becomes `{Left: '-2.00'}`.
126
+ //
127
+ // This exists because those choices get handed onwards. A picker that shows the
128
+ // buyer what was understood so far has to compare them against the option values
129
+ // it draws, and comparing `-2.0` to `-2.00` as strings highlights nothing while
130
+ // still counting as answered — so the buyer sees an unanswered question with the
131
+ // Buy button live, presses it, and gets the same screen back. Snapping here, in
132
+ // the one place that already knows how these compare, keeps that logic from
133
+ // being reimplemented (differently) further out.
134
+ //
135
+ // Anything that does not name exactly one value is DROPPED, not guessed: an
136
+ // unrecognised prescription has to come back as a question.
137
+ export const snapChoices = (options, choices) => {
138
+ const out = {};
139
+ for (const [name, value] of Object.entries(choices)) {
140
+ if (value === undefined || value === '')
141
+ continue;
142
+ const opt = options.find((o) => same(o.name, name));
143
+ const hits = opt?.values.filter((v) => same(v, value)) ?? [];
144
+ if (opt && hits.length === 1 && hits[0] !== undefined)
145
+ out[opt.name] = hits[0];
146
+ }
147
+ return out;
148
+ };
149
+ const VARIANTS_QUERY = `
150
+ query ProductVariants($handle: String!, $first: Int!, $after: String) {
151
+ shop { currencyCode }
152
+ productByHandle(handle: $handle) {
153
+ variants(first: $first, after: $after) {
154
+ pageInfo { hasNextPage endCursor }
155
+ nodes {
156
+ id sku title price compareAtPrice availableForSale
157
+ selectedOptions { name value }
158
+ image { url }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ `;
164
+ // Every variant of ONE product, paginated to completion.
165
+ //
166
+ // Completeness is the point. The bulk read used to take the first 100 of 676 and
167
+ // warn about the rest — the warning was accurate and the behaviour was still
168
+ // wrong, because a buyer asking for -6.50 in the right eye was told it does not
169
+ // exist when it merely sat past a page boundary. Most of the store was
170
+ // unbuyable and every refusal looked like a legitimate "we don't stock that".
171
+ export const fetchProductVariants = async (config, handle, fetchImpl = fetch, warn = (m) => console.warn(`[shopify] ${m}`)) => {
172
+ const token = await exchangeToken(config, fetchImpl);
173
+ const out = [];
174
+ let cursor = null;
175
+ let currency = 'USD';
176
+ do {
177
+ const page = await shopifyGraphql(config, token, VARIANTS_QUERY, { handle, first: VARIANT_PAGE, after: cursor }, fetchImpl);
178
+ currency = page.shop?.currencyCode ?? currency;
179
+ const conn = page.productByHandle?.variants;
180
+ if (!conn)
181
+ return out;
182
+ for (const v of conn.nodes) {
183
+ let priceMinor;
184
+ try {
185
+ priceMinor = toMinor(v.price, currency);
186
+ }
187
+ catch {
188
+ warn(`skipping ${handle} / ${v.title} — unparseable price ${JSON.stringify(v.price)}`);
189
+ continue;
190
+ }
191
+ let compareAtMinor;
192
+ if (v.compareAtPrice) {
193
+ try {
194
+ compareAtMinor = toMinor(v.compareAtPrice, currency);
195
+ }
196
+ catch {
197
+ // Cosmetic only — drop the strike-through, keep the variant buyable.
198
+ compareAtMinor = undefined;
199
+ }
200
+ }
201
+ out.push({
202
+ variantId: numericId(v.id),
203
+ sku: v.sku,
204
+ title: v.title,
205
+ priceMinor,
206
+ ...(compareAtMinor !== undefined ? { compareAtMinor } : {}),
207
+ available: v.availableForSale,
208
+ options: v.selectedOptions ?? [],
209
+ ...(v.image?.url ? { imageUrl: v.image.url } : {}),
210
+ });
211
+ }
212
+ cursor = conn.pageInfo.hasNextPage ? conn.pageInfo.endCursor : null;
213
+ } while (cursor);
214
+ return out;
215
+ };
216
+ //# sourceMappingURL=products.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"products.js","sourceRoot":"","sources":["../src/products.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,gFAAgF;AAChF,0EAA0E;AAC1E,kEAAkE;AAClE,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,oCAAoC;AACpC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA0DrC,MAAM,IAAI,GAAG,EAAE,CAAC;AAChB,2EAA2E;AAC3E,iFAAiF;AACjF,qEAAqE;AACrE,MAAM,YAAY,GAAG,GAAG,CAAC;AAgCzB,MAAM,KAAK,GAAG;;;;;;;;;;;;;;CAcb,CAAC;AAEF,gFAAgF;AAChF,+EAA+E;AAC/E,0BAA0B;AAC1B,MAAM,SAAS,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAEpE,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,MAAqB,EACrB,YAA0B,KAAK,EAC/B,OAA8B,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,EACxC,EAAE;IAC7B,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAqB,EAAE,CAAC;IACjC,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,UAA8B,CAAC;IACnC,GAAG,CAAC;QACF,MAAM,IAAI,GAAS,MAAM,cAAc,CACrC,MAAM,EACN,KAAK,EACL,KAAK,EACL,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAC9B,SAAS,CACV,CAAC;QACF,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,IAAI,QAAQ,CAAC;QAC/C,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,IAAI,UAAU,CAAC;QACzD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,cAAkC,CAAC;YACvC,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,eAAe,CAAC;YAC7C,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC;oBACH,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,IAAI,QAAQ,CAAC,CAAC;gBACvE,CAAC;gBAAC,MAAM,CAAC;oBACP,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,8BAA8B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBAC/E,CAAC;YACH,CAAC;YACD,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK;iBACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC;iBACjC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1C,GAAG,CAAC,IAAI,CAAC;gBACP,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;gBACnB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvF,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,EAAE;gBAChC,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,EAAE;gBAChC,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;gBAClB,QAAQ;gBACR,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClE,MAAM;gBACN,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;oBAC7B,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,MAAM,EAAE,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;iBAC1C,CAAC,CAAC;gBACH,yEAAyE;gBACzE,0BAA0B;gBAC1B,QAAQ,EAAE,EAAE;gBACZ,cAAc,EAAE,KAAK;gBACrB,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5D,CAAC,CAAC;QACL,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IACxF,CAAC,QAAQ,MAAM,EAAE;IACjB,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,iFAAiF;AACjF,+EAA+E;AAC/E,8EAA8E;AAC9E,kFAAkF;AAClF,EAAE;AACF,gFAAgF;AAChF,2EAA2E;AAC3E,WAAW;AACX,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,0BAA0B;AAC1B,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAEvE,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,yEAAyE;AACzE,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAiB,EAAE;IAC5C,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC1C,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvC,CAAC,CAAC;AAEF,6EAA6E;AAC7E,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,CAAS,EAAW,EAAE;IAC7C,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,QAA0B,EAC1B,OAA+B,EACR,EAAE;IACzB,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACtF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACpC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAC7B,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAClE,CACF,CAAC;IACF,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC,CAAC;AAEF,6EAA6E;AAC7E,4DAA4D;AAC5D,EAAE;AACF,gFAAgF;AAChF,iFAAiF;AACjF,gFAAgF;AAChF,iFAAiF;AACjF,gFAAgF;AAChF,4EAA4E;AAC5E,iDAAiD;AACjD,EAAE;AACF,4EAA4E;AAC5E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,OAAwB,EACxB,OAA+B,EACP,EAAE;IAC1B,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;YAAE,SAAS;QAClD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7D,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;;;;;;;;;;CActB,CAAC;AAEF,yDAAyD;AACzD,EAAE;AACF,iFAAiF;AACjF,6EAA6E;AAC7E,gFAAgF;AAChF,uEAAuE;AACvE,8EAA8E;AAC9E,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,MAAqB,EACrB,MAAc,EACd,YAA0B,KAAK,EAC/B,OAA8B,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,EACxC,EAAE;IAC7B,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAqB,EAAE,CAAC;IACjC,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,GAAG,CAAC;QACF,MAAM,IAAI,GAQN,MAAM,cAAc,CACtB,MAAM,EACN,KAAK,EACL,cAAc,EACd,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,EAC9C,SAAS,CACV,CAAC;QACF,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,IAAI,QAAQ,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC;QAC5C,IAAI,CAAC,IAAI;YAAE,OAAO,GAAG,CAAC;QACtB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC3B,IAAI,UAAkB,CAAC;YACvB,IAAI,CAAC;gBACH,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YAC1C,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,YAAY,MAAM,MAAM,CAAC,CAAC,KAAK,wBAAwB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBACvF,SAAS;YACX,CAAC;YACD,IAAI,cAAkC,CAAC;YACvC,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;gBACrB,IAAI,CAAC;oBACH,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;gBACvD,CAAC;gBAAC,MAAM,CAAC;oBACP,qEAAqE;oBACrE,cAAc,GAAG,SAAS,CAAC;gBAC7B,CAAC;YACH,CAAC;YACD,GAAG,CAAC,IAAI,CAAC;gBACP,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1B,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,UAAU;gBACV,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3D,SAAS,EAAE,CAAC,CAAC,gBAAgB;gBAC7B,OAAO,EAAE,CAAC,CAAC,eAAe,IAAI,EAAE;gBAChC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACnD,CAAC,CAAC;QACL,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,CAAC,QAAQ,MAAM,EAAE;IACjB,OAAO,GAAG,CAAC;AACb,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@curless/shopify-adapter",
3
- "version": "0.5.0",
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).",
3
+ "version": "0.7.0",
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 \u2192 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",
7
7
  "main": "dist/index.js",
@@ -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
- "scripts": {
46
- "build": "tsc",
47
- "typecheck": "tsc --noEmit",
48
- "test": "vitest run",
49
- "clean": "rm -rf dist"
50
- }
51
- }
48
+ "author": "robin"
49
+ }