@porulle/adapter-shopify 0.71.0 → 0.73.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/adapter-shopify",
3
- "version": "0.71.0",
3
+ "version": "0.73.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "zod": "^4.1.11",
19
- "@porulle/core": "0.71.0"
19
+ "@porulle/core": "0.73.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^24.5.2",
package/src/index.ts CHANGED
@@ -67,6 +67,15 @@ export const ORDER_CANCEL_MUTATION = `mutation PorulleOrderCancel($orderId: ID!,
67
67
  orderCancel(orderId: $orderId, reason: $reason, restock: true, notifyCustomer: false, staffNote: $staffNote) { orderCancelUserErrors { code message } }
68
68
  }`;
69
69
 
70
+ /** The order's shipped lines: a return names what it takes back by fulfilment line, not order line. */
71
+ export const ORDER_FULFILLMENT_LINES_QUERY = `query PorulleOrderFulfillmentLines($id: ID!) {
72
+ order(id: $id) { fulfillments(first: 20) { fulfillmentLineItems(first: 50) { nodes { id quantity lineItem { variant { legacyResourceId } } } } } }
73
+ }`;
74
+
75
+ export const RETURN_REQUEST_MUTATION = `mutation PorulleReturnRequest($input: ReturnRequestInput!) {
76
+ returnRequest(input: $input) { return { id } userErrors { code message } }
77
+ }`;
78
+
70
79
  export const ORDER_BY_SOURCE_QUERY = `query PorulleOrderBySource($query: String!) {
71
80
  orders(first: 1, query: $query) { nodes { legacyResourceId } }
72
81
  }`;
@@ -100,6 +109,16 @@ const CANCEL_REASON: Record<ChannelCancelReason, string> = {
100
109
  staff: "STAFF",
101
110
  other: "OTHER",
102
111
  };
112
+ const fulfillmentLinesSchema = z.object({
113
+ order: z.object({
114
+ fulfillments: z.array(z.object({
115
+ fulfillmentLineItems: z.object({ nodes: z.array(z.object({ id: z.string(), quantity: z.number(), lineItem: z.object({ variant: z.object({ legacyResourceId: z.string() }).nullable() }) })) }),
116
+ })),
117
+ }).nullable(),
118
+ });
119
+ const returnRequestSchema = z.object({
120
+ returnRequest: z.object({ return: z.object({ id: z.string() }).nullable(), userErrors: z.array(z.object({ code: z.string().nullable(), message: z.string() })) }),
121
+ });
103
122
  const orderBySourceSchema = z.object({ orders: z.object({ nodes: z.array(z.object({ legacyResourceId: z.string() })) }) });
104
123
  const orderStatusSchema = z.object({
105
124
  order: z.object({ cancelledAt: z.string().nullable(), displayFinancialStatus: z.string().nullable(), displayFulfillmentStatus: z.string() }).nullable(),
@@ -279,6 +298,8 @@ export function shopifyConnector(options: ShopifyConnectorOptions): ChannelConne
279
298
  })),
280
299
  // The delivery the shopper paid this seller, so the order total matches the payment below.
281
300
  ...(slice.shipping ? { shippingLines: [{ title: slice.shipping.title, priceSet: money(slice.shipping.amount, slice.currency) }] } : {}),
301
+ // The shopper's discount as a fixed amount off the items, so the store's total is what was paid.
302
+ ...(slice.discount ? { discountCode: { itemFixedDiscountCode: { code: slice.discount.code, amountSet: money(slice.discount.amount, slice.currency) } } } : {}),
282
303
  transactions: [{ kind: "SALE", status: "SUCCESS", gateway: SOURCE_NAME, amountSet: money(slice.grandTotal, slice.currency) }],
283
304
  },
284
305
  options: { inventoryBehaviour: "DECREMENT_OBEYING_POLICY", sendReceipt: false, sendFulfillmentReceipt: false },
@@ -318,6 +339,32 @@ export function shopifyConnector(options: ShopifyConnectorOptions): ChannelConne
318
339
  if (read.ok && read.value.order?.cancelledAt) return Ok(undefined);
319
340
  return Err({ code: CHANNEL_CANCEL_REFUSED, message: `Shopify refused to cancel the order: ${refusals.map((error) => error.message).join("; ")}.`, retriable: false });
320
341
  },
342
+ async requestReturn(store, remoteOrderId, input) {
343
+ const shop = target(store);
344
+ if (!shop) return Err(credentialsRequired);
345
+ const shipped = await shopifyGraphql(shop, ORDER_FULFILLMENT_LINES_QUERY, { id: `gid://shopify/Order/${remoteOrderId}` }, fulfillmentLinesSchema);
346
+ if (!shipped.ok) return shipped;
347
+ const fulfilmentLines = (shipped.value.order?.fulfillments ?? []).flatMap((fulfillment) => fulfillment.fulfillmentLineItems.nodes);
348
+ const returnLineItems: Array<{ fulfillmentLineItemId: string; quantity: number; customerNote: string }> = [];
349
+ const note = [input.reason, input.note].filter((part) => part !== undefined && part !== "").join(" — ").slice(0, 300);
350
+ for (const wanted of input.lines) {
351
+ let remaining = wanted.quantity;
352
+ for (const line of fulfilmentLines.filter((candidate) => candidate.lineItem.variant?.legacyResourceId === wanted.externalVariantId)) {
353
+ if (remaining === 0) break;
354
+ const take = Math.min(remaining, line.quantity);
355
+ returnLineItems.push({ fulfillmentLineItemId: line.id, quantity: take, customerNote: note });
356
+ remaining -= take;
357
+ }
358
+ if (remaining > 0) return Err({ code: "SHOPIFY_RETURN_NOT_SHIPPED", message: `Shopify has not shipped ${remaining} of variant ${wanted.externalVariantId}, so it cannot take them back.`, retriable: false });
359
+ }
360
+ const requested = await shopifyGraphql(shop, RETURN_REQUEST_MUTATION, { input: { orderId: `gid://shopify/Order/${remoteOrderId}`, returnLineItems } }, returnRequestSchema);
361
+ if (!requested.ok) return requested;
362
+ const { return: created, userErrors } = requested.value.returnRequest;
363
+ if (userErrors.length > 0 || !created) {
364
+ return Err({ code: "SHOPIFY_RETURN_REFUSED", message: `Shopify refused the return: ${userErrors.map((error) => error.message).join("; ") || "no return created"}.`, retriable: false });
365
+ }
366
+ return Ok({ remoteReturnId: created.id.split("/").pop() ?? created.id });
367
+ },
321
368
  /**
322
369
  * Every Shopify delivery — catalogue, stock, orders, uninstall and the mandatory compliance topics —
323
370
  * arrives at ONE app-level address declared in `shopify.app.toml`, signed with the app's client