@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/dist/index.d.ts +5 -0
- package/dist/index.js +42 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/index.ts +40 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/adapter-shopify",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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/
|
|
30
|
-
"@porulle/
|
|
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
|