@porulle/adapter-shopify 0.68.4 → 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/dist/index.d.ts CHANGED
@@ -19,6 +19,11 @@ export declare const INVENTORY_QUERY = "query PorulleInventoryPage($after: Strin
19
19
  export declare const VARIANT_INVENTORY_QUERY = "query PorulleVariantInventory($ids: [ID!]!) {\n nodes(ids: $ids) { ... on ProductVariant { legacyResourceId inventoryQuantity } }\n}";
20
20
  export declare const STORE_PROFILE_QUERY = "query PorulleStoreProfile {\n shop { name currencyCode myshopifyDomain primaryDomain { host } }\n}";
21
21
  export declare const ORDER_CREATE_MUTATION = "mutation PorulleOrderCreate($order: OrderCreateOrderInput!, $options: OrderCreateOptionsInput) {\n orderCreate(order: $order, options: $options) { order { legacyResourceId } userErrors { field message } }\n}";
22
+ /**
23
+ * Restocks at the store and refunds nothing there, and tells the customer nothing: the marketplace
24
+ * took the payment, so it refunds and it writes to the shopper.
25
+ */
26
+ export declare const ORDER_CANCEL_MUTATION = "mutation PorulleOrderCancel($orderId: ID!, $reason: OrderCancelReason!, $staffNote: String) {\n orderCancel(orderId: $orderId, reason: $reason, restock: true, notifyCustomer: false, staffNote: $staffNote) { orderCancelUserErrors { code message } }\n}";
22
27
  export declare const ORDER_BY_SOURCE_QUERY = "query PorulleOrderBySource($query: String!) {\n orders(first: 1, query: $query) { nodes { legacyResourceId } }\n}";
23
28
  export declare const ORDER_STATUS_QUERY = "query PorulleOrderStatus($id: ID!) {\n order(id: $id) { cancelledAt displayFinancialStatus displayFulfillmentStatus }\n}";
24
29
  export declare function shopifyConnector(options: ShopifyConnectorOptions): ChannelConnector;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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 { z } from "zod";
4
4
  import { readCatalogItems, readCatalogPage } from "./catalog.js";
5
5
  import { shopifyGid, shopifyGraphql } from "./graphql.js";
@@ -19,6 +19,13 @@ export const STORE_PROFILE_QUERY = `query PorulleStoreProfile {
19
19
  export const ORDER_CREATE_MUTATION = `mutation PorulleOrderCreate($order: OrderCreateOrderInput!, $options: OrderCreateOptionsInput) {
20
20
  orderCreate(order: $order, options: $options) { order { legacyResourceId } userErrors { field message } }
21
21
  }`;
22
+ /**
23
+ * Restocks at the store and refunds nothing there, and tells the customer nothing: the marketplace
24
+ * took the payment, so it refunds and it writes to the shopper.
25
+ */
26
+ export const ORDER_CANCEL_MUTATION = `mutation PorulleOrderCancel($orderId: ID!, $reason: OrderCancelReason!, $staffNote: String) {
27
+ orderCancel(orderId: $orderId, reason: $reason, restock: true, notifyCustomer: false, staffNote: $staffNote) { orderCancelUserErrors { code message } }
28
+ }`;
22
29
  export const ORDER_BY_SOURCE_QUERY = `query PorulleOrderBySource($query: String!) {
23
30
  orders(first: 1, query: $query) { nodes { legacyResourceId } }
24
31
  }`;
@@ -39,6 +46,17 @@ const orderCreateSchema = z.object({
39
46
  userErrors: z.array(z.object({ field: z.array(z.string()).nullable(), message: z.string() })),
40
47
  }),
41
48
  });
49
+ const orderCancelSchema = z.object({
50
+ orderCancel: z.object({ orderCancelUserErrors: z.array(z.object({ code: z.string().nullable(), message: z.string() })) }),
51
+ });
52
+ const CANCEL_REASON = {
53
+ customer: "CUSTOMER",
54
+ inventory: "INVENTORY",
55
+ declined: "DECLINED",
56
+ fraud: "FRAUD",
57
+ staff: "STAFF",
58
+ other: "OTHER",
59
+ };
42
60
  const orderBySourceSchema = z.object({ orders: z.object({ nodes: z.array(z.object({ legacyResourceId: z.string() })) }) });
43
61
  const orderStatusSchema = z.object({
44
62
  order: z.object({ cancelledAt: z.string().nullable(), displayFinancialStatus: z.string().nullable(), displayFulfillmentStatus: z.string() }).nullable(),
@@ -251,6 +269,27 @@ export function shopifyConnector(options) {
251
269
  return Err({ code: "SHOPIFY_ORDER_NOT_FOUND", message: `Shopify has no order ${remoteId}.`, retriable: false });
252
270
  return Ok(orderStatus(read.value.order));
253
271
  },
272
+ async cancelOrder(store, remoteId, input) {
273
+ const shop = target(store);
274
+ if (!shop)
275
+ return Err(credentialsRequired);
276
+ const orderId = `gid://shopify/Order/${remoteId}`;
277
+ const cancelled = await shopifyGraphql(shop, ORDER_CANCEL_MUTATION, {
278
+ orderId,
279
+ reason: CANCEL_REASON[input.reason],
280
+ staffNote: input.staffNote?.slice(0, 255) ?? null,
281
+ }, orderCancelSchema);
282
+ if (!cancelled.ok)
283
+ return cancelled;
284
+ const refusals = cancelled.value.orderCancel.orderCancelUserErrors;
285
+ if (refusals.length === 0)
286
+ return Ok(undefined);
287
+ // Cancelling twice is not a refusal: an order the store already cancelled is the outcome asked for.
288
+ const read = await shopifyGraphql(shop, ORDER_STATUS_QUERY, { id: orderId }, orderStatusSchema);
289
+ if (read.ok && read.value.order?.cancelledAt)
290
+ return Ok(undefined);
291
+ return Err({ code: CHANNEL_CANCEL_REFUSED, message: `Shopify refused to cancel the order: ${refusals.map((error) => error.message).join("; ")}.`, retriable: false });
292
+ },
254
293
  /**
255
294
  * Every Shopify delivery — catalogue, stock, orders, uninstall and the mandatory compliance topics —
256
295
  * arrives at ONE app-level address declared in `shopify.app.toml`, signed with the app's client