@porulle/adapter-woocommerce 0.75.0 → 0.77.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.js CHANGED
@@ -4,7 +4,7 @@ import { catalogItems, importPage, storeProfile } from "./catalog.js";
4
4
  import { pushCatalog, pushCaches } from "./catalog-push.js";
5
5
  import { discoverStore, normalizeStoreDomain, probeStore, refusedStoreUrl, wooClient, wooCredentialsSchema } from "./client.js";
6
6
  import { inventoryFor, inventoryPage } from "./inventory.js";
7
- import { cancelOrder, orderStatus, pushOrder } from "./orders.js";
7
+ import { cancelOrder, orderStatus, pushOrder, recordRefund } from "./orders.js";
8
8
  import { decodeDelivery, orderEvents, registerWebhooks, unregisterWebhooks, verifyDelivery, webhookHealth, WOO_WEBHOOK_TOPICS } from "./webhooks.js";
9
9
  export { normalizeStoreDomain, probeStore, refusedStoreUrl, WOO_BLOCKED_BY_FIREWALL } from "./client.js";
10
10
  export { ORDER_META_KEY } from "./orders.js";
@@ -157,6 +157,9 @@ export function wooConnector(options = {}) {
157
157
  async cancelOrder(store, remoteId, input) {
158
158
  return withClient(store, (client) => cancelOrder(client, remoteId, input));
159
159
  },
160
+ async recordRefund(store, remoteId, input) {
161
+ return withClient(store, (client) => recordRefund(client, remoteId, input));
162
+ },
160
163
  async pushCatalog(store, items, opts) {
161
164
  return withClient(store, (client) => pushCatalog(client, store, items, opts, caches));
162
165
  },
package/dist/orders.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ChannelCancelOrderInput, ChannelConnectorError, ChannelOrderSlice, ChannelOrderStatus, ChannelPushOrderResult, Result } from "@porulle/core";
1
+ import type { ChannelCancelOrderInput, ChannelConnectorError, ChannelRefundRecord, ChannelOrderSlice, ChannelOrderStatus, ChannelPushOrderResult, Result } from "@porulle/core";
2
2
  import { z } from "zod";
3
3
  import type { WooClient } from "./client.js";
4
4
  export declare const wooOrderSchema: z.ZodObject<{
@@ -18,6 +18,10 @@ export declare const wooOrderSchema: z.ZodObject<{
18
18
  quantity: z.ZodNumber;
19
19
  total: z.ZodOptional<z.ZodNullable<z.ZodString>>;
20
20
  }, z.core.$strip>>>;
21
+ shipping_lines: z.ZodDefault<z.ZodArray<z.ZodObject<{
22
+ id: z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<string, string | number>>;
23
+ total: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
+ }, z.core.$strip>>>;
21
25
  refunds: z.ZodDefault<z.ZodArray<z.ZodObject<{
22
26
  id: z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<string, string | number>>;
23
27
  total: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -48,4 +52,11 @@ export declare function pushOrder(client: WooClient, slice: ChannelOrderSlice, o
48
52
  * completed or refunded one is the merchant's (shipped) and is refused, never moved backwards.
49
53
  */
50
54
  export declare function cancelOrder(client: WooClient, remoteId: string, input: ChannelCancelOrderInput): Promise<Result<void, ChannelConnectorError>>;
55
+ /**
56
+ * Books at the store a refund the marketplace already paid the shopper: `api_refund: false` moves no
57
+ * money, `api_restock` puts the stock back, and each line is the store's own order line for the variant.
58
+ */
59
+ export declare function recordRefund(client: WooClient, remoteId: string, input: ChannelRefundRecord): Promise<Result<{
60
+ remoteRefundId: string;
61
+ }, ChannelConnectorError>>;
51
62
  export declare function orderStatus(client: WooClient, remoteId: string): Promise<Result<ChannelOrderStatus, ChannelConnectorError>>;
package/dist/orders.js CHANGED
@@ -10,6 +10,7 @@ export const wooOrderSchema = z.object({
10
10
  date_created_gmt: z.string().nullish(),
11
11
  meta_data: z.array(z.object({ key: z.string(), value: z.unknown() })).default([]),
12
12
  line_items: z.array(z.object({ id, product_id: id, variation_id: id, quantity: z.number(), total: z.string().nullish() })).default([]),
13
+ shipping_lines: z.array(z.object({ id, total: z.string().nullish() })).default([]),
13
14
  refunds: z.array(z.object({ id, total: z.string().nullish() })).default([]),
14
15
  });
15
16
  /** The meta key naming the marketplace order a store order was created for: how a retry finds it. */
@@ -97,7 +98,12 @@ export async function pushOrder(client, slice, options) {
97
98
  }
98
99
  const currency = slice.currency;
99
100
  const decimals = client.credentials.priceDecimals;
100
- const discounts = allocate(slice.lines.map((line) => line.totalPrice), slice.discount?.amount ?? 0);
101
+ // The platform's own per-line discount when it sends one, so the store's line totals are what the
102
+ // platform refunds a line at; otherwise the slice's discount split here.
103
+ const shares = slice.lines.map((line) => line.discountAmount ?? 0);
104
+ const discounts = slice.discount !== undefined && shares.reduce((sum, share) => sum + share, 0) === slice.discount.amount
105
+ ? shares
106
+ : allocate(slice.lines.map((line) => line.totalPrice), slice.discount?.amount ?? 0);
101
107
  const [firstName = "", ...rest] = slice.customer.name.trim().split(/\s+/);
102
108
  const source = slice.customer.shippingAddress;
103
109
  const address = {
@@ -184,6 +190,45 @@ export async function cancelOrder(client, remoteId, input) {
184
190
  const cancelled = await cancel(client, remoteId, `Runvae: cancelled (${input.reason})${input.staffNote ? `: ${input.staffNote}` : ""}.`);
185
191
  return cancelled.ok ? Ok(undefined) : cancelled;
186
192
  }
193
+ /**
194
+ * Books at the store a refund the marketplace already paid the shopper: `api_refund: false` moves no
195
+ * money, `api_restock` puts the stock back, and each line is the store's own order line for the variant.
196
+ */
197
+ export async function recordRefund(client, remoteId, input) {
198
+ const read = await client.get(`/wc/v3/orders/${encodeURIComponent(remoteId)}`, wooOrderSchema);
199
+ if (!read.ok)
200
+ return read;
201
+ const order = read.value.data;
202
+ const currency = order.currency ?? client.credentials.currency;
203
+ if (!currency)
204
+ return Err({ code: "WOO_CURRENCY_UNKNOWN", message: `Order ${remoteId} names no currency.`, retriable: false });
205
+ const decimals = client.credentials.priceDecimals;
206
+ const lineItems = [];
207
+ for (const line of input.lines) {
208
+ const storeLine = order.line_items.find((candidate) => (candidate.variation_id !== "0" ? candidate.variation_id : candidate.product_id) === line.externalVariantId);
209
+ if (!storeLine)
210
+ return Err({ code: "CHANNEL_MAPPING_MISSING", message: `Order ${remoteId} has no line for ${line.externalVariantId}.`, retriable: false });
211
+ lineItems.push({ id: storeLine.id, quantity: line.quantity, refund_total: moneyString(line.amount, currency, decimals) });
212
+ }
213
+ // Delivery is refunded against the order's shipping item, as wp-admin does it.
214
+ const shippingAmount = input.shippingAmount ?? 0;
215
+ if (shippingAmount > 0) {
216
+ const shippingLine = order.shipping_lines[0];
217
+ if (!shippingLine)
218
+ return Err({ code: "CHANNEL_MAPPING_MISSING", message: `Order ${remoteId} has no delivery line to refund.`, retriable: false });
219
+ lineItems.push({ id: shippingLine.id, quantity: 0, refund_total: moneyString(shippingAmount, currency, decimals) });
220
+ }
221
+ const created = await client.send("POST", `/wc/v3/orders/${encodeURIComponent(remoteId)}/refunds`, {
222
+ amount: moneyString(input.amount, currency, decimals),
223
+ reason: input.reason,
224
+ api_refund: false,
225
+ api_restock: input.restock,
226
+ line_items: lineItems,
227
+ }, z.object({ id: z.union([z.number(), z.string()]).transform(String) }));
228
+ if (!created.ok)
229
+ return Err({ code: created.error.code, message: `The store would not record the refund: ${created.error.message}`, retriable: created.error.retriable === true });
230
+ return Ok({ remoteRefundId: created.value.id });
231
+ }
187
232
  export async function orderStatus(client, remoteId) {
188
233
  const read = await client.get(`/wc/v3/orders/${encodeURIComponent(remoteId)}`, wooOrderSchema);
189
234
  if (!read.ok)