@porulle/adapter-shopify 0.68.3 → 0.69.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.68.3",
3
+ "version": "0.69.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.68.3"
19
+ "@porulle/core": "0.69.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^24.5.2",
@@ -26,8 +26,8 @@
26
26
  "miniflare": "4.20260722.1",
27
27
  "typescript": "5.9.2",
28
28
  "vitest": "^3.2.4",
29
- "@porulle/typescript-config": "0.1.0",
30
- "@porulle/eslint-config": "0.1.0"
29
+ "@porulle/eslint-config": "0.1.0",
30
+ "@porulle/typescript-config": "0.1.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHmac, timingSafeEqual } from "node:crypto";
2
- import { defineChannelConnector, Err, Ok } from "@porulle/core";
2
+ import { CHANNEL_CANCEL_REFUSED, defineChannelConnector, Err, Ok } from "@porulle/core";
3
3
  import type {
4
+ ChannelCancelReason,
4
5
  ChannelConnector,
5
6
  ChannelConnectorError,
6
7
  ChannelInventoryLevel,
@@ -58,6 +59,14 @@ export const ORDER_CREATE_MUTATION = `mutation PorulleOrderCreate($order: OrderC
58
59
  orderCreate(order: $order, options: $options) { order { legacyResourceId } userErrors { field message } }
59
60
  }`;
60
61
 
62
+ /**
63
+ * Restocks at the store and refunds nothing there, and tells the customer nothing: the marketplace
64
+ * took the payment, so it refunds and it writes to the shopper.
65
+ */
66
+ export const ORDER_CANCEL_MUTATION = `mutation PorulleOrderCancel($orderId: ID!, $reason: OrderCancelReason!, $staffNote: String) {
67
+ orderCancel(orderId: $orderId, reason: $reason, restock: true, notifyCustomer: false, staffNote: $staffNote) { orderCancelUserErrors { code message } }
68
+ }`;
69
+
61
70
  export const ORDER_BY_SOURCE_QUERY = `query PorulleOrderBySource($query: String!) {
62
71
  orders(first: 1, query: $query) { nodes { legacyResourceId } }
63
72
  }`;
@@ -80,6 +89,17 @@ const orderCreateSchema = z.object({
80
89
  userErrors: z.array(z.object({ field: z.array(z.string()).nullable(), message: z.string() })),
81
90
  }),
82
91
  });
92
+ const orderCancelSchema = z.object({
93
+ orderCancel: z.object({ orderCancelUserErrors: z.array(z.object({ code: z.string().nullable(), message: z.string() })) }),
94
+ });
95
+ const CANCEL_REASON: Record<ChannelCancelReason, string> = {
96
+ customer: "CUSTOMER",
97
+ inventory: "INVENTORY",
98
+ declined: "DECLINED",
99
+ fraud: "FRAUD",
100
+ staff: "STAFF",
101
+ other: "OTHER",
102
+ };
83
103
  const orderBySourceSchema = z.object({ orders: z.object({ nodes: z.array(z.object({ legacyResourceId: z.string() })) }) });
84
104
  const orderStatusSchema = z.object({
85
105
  order: z.object({ cancelledAt: z.string().nullable(), displayFinancialStatus: z.string().nullable(), displayFulfillmentStatus: z.string() }).nullable(),
@@ -257,6 +277,8 @@ export function shopifyConnector(options: ShopifyConnectorOptions): ChannelConne
257
277
  requiresShipping: true,
258
278
  priceSet: money(line.unitPrice, slice.currency),
259
279
  })),
280
+ // The delivery the shopper paid this seller, so the order total matches the payment below.
281
+ ...(slice.shipping ? { shippingLines: [{ title: slice.shipping.title, priceSet: money(slice.shipping.amount, slice.currency) }] } : {}),
260
282
  transactions: [{ kind: "SALE", status: "SUCCESS", gateway: SOURCE_NAME, amountSet: money(slice.grandTotal, slice.currency) }],
261
283
  },
262
284
  options: { inventoryBehaviour: "DECREMENT_OBEYING_POLICY", sendReceipt: false, sendFulfillmentReceipt: false },
@@ -276,6 +298,23 @@ export function shopifyConnector(options: ShopifyConnectorOptions): ChannelConne
276
298
  if (!read.value.order) return Err({ code: "SHOPIFY_ORDER_NOT_FOUND", message: `Shopify has no order ${remoteId}.`, retriable: false });
277
299
  return Ok(orderStatus(read.value.order));
278
300
  },
301
+ async cancelOrder(store, remoteId, input) {
302
+ const shop = target(store);
303
+ if (!shop) return Err(credentialsRequired);
304
+ const orderId = `gid://shopify/Order/${remoteId}`;
305
+ const cancelled = await shopifyGraphql(shop, ORDER_CANCEL_MUTATION, {
306
+ orderId,
307
+ reason: CANCEL_REASON[input.reason],
308
+ staffNote: input.staffNote?.slice(0, 255) ?? null,
309
+ }, orderCancelSchema);
310
+ if (!cancelled.ok) return cancelled;
311
+ const refusals = cancelled.value.orderCancel.orderCancelUserErrors;
312
+ if (refusals.length === 0) return Ok(undefined);
313
+ // Cancelling twice is not a refusal: an order the store already cancelled is the outcome asked for.
314
+ const read = await shopifyGraphql(shop, ORDER_STATUS_QUERY, { id: orderId }, orderStatusSchema);
315
+ if (read.ok && read.value.order?.cancelledAt) return Ok(undefined);
316
+ return Err({ code: CHANNEL_CANCEL_REFUSED, message: `Shopify refused to cancel the order: ${refusals.map((error) => error.message).join("; ")}.`, retriable: false });
317
+ },
279
318
  /**
280
319
  * Every Shopify delivery — catalogue, stock, orders, uninstall and the mandatory compliance topics —
281
320
  * arrives at ONE app-level address declared in `shopify.app.toml`, signed with the app's client