@porulle/plugin-channel-connector 0.70.2 → 0.72.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/service.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { z } from "zod";
3
- import { CommerceInvalidTransitionError, CommerceNotFoundError, CommerceValidationError, Ok, PluginErr, createTxContext, createSystemActor, linkFieldPaths, removeEntityLinks, writeEntityLinks, } from "@porulle/core";
3
+ import { CHANNEL_OUT_OF_STOCK, CommerceInvalidTransitionError, CommerceNotFoundError, CommerceValidationError, Ok, PluginErr, createTxContext, createSystemActor, linkFieldPaths, removeEntityLinks, writeEntityLinks, } from "@porulle/core";
4
4
  import { isValidFieldPath, requireUserId } from "@porulle/core";
5
5
  import { CHANNEL_CONVERGENCE_CTX } from "./catalog-push-trigger.js";
6
6
  import { resolveLiveCredentials, withLiveCredentials } from "./live-credentials.js";
@@ -4452,7 +4452,14 @@ export class ChannelConnectorService {
4452
4452
  .where(and(eq(channelOrderExports.organizationId, orgId), eq(channelOrderExports.id, created.value.id)));
4453
4453
  const pushed = await connector.pushOrder(store, slice);
4454
4454
  if (!pushed.ok) {
4455
- return this.transitionExport(orgId, created.value.id, "failed", requireUserId(actor), pushed.error.message, pushed.error.retriable === true ? "transient" : "definitive");
4455
+ const failed = await this.transitionExport(orgId, created.value.id, "failed", requireUserId(actor), pushed.error.message, pushed.error.retriable === true ? "transient" : "definitive");
4456
+ if (pushed.error.code === CHANNEL_OUT_OF_STOCK) {
4457
+ // The store has said nobody will send these goods: cancel, and let the host's cancel path
4458
+ // refund the shopper. Any other refusal can be fixed and retried, so it waits for an operator.
4459
+ const ordersService = this.services.orders;
4460
+ await ordersService.changeStatus({ orderId: slice.orderId, newStatus: "cancelled", reason: "channel_out_of_stock" }, actor);
4461
+ }
4462
+ return failed;
4456
4463
  }
4457
4464
  await this.db
4458
4465
  .update(channelOrderExports)
@@ -4535,7 +4542,10 @@ export class ChannelConnectorService {
4535
4542
  const linesTotal = lines.reduce((sum, line) => sum + line.totalPrice, 0);
4536
4543
  // ponytail: a multi-store order sends no shipping to any store; apportion it when such orders exist.
4537
4544
  const shipping = selected.length === lineItems.length && order.shippingTotal > 0 ? { title: "Shipping", amount: order.shippingTotal } : null;
4538
- return Ok({ orderId, currency: order.currency, grandTotal: linesTotal + (shipping?.amount ?? 0), lines, ...(shipping ? { shipping } : {}), customer: { name, email, shippingAddress } });
4545
+ // ponytail: like delivery, a discount is sent only with the whole order; apportion it when multi-store orders exist.
4546
+ const discountCode = typeof metadata.promotionCode === "string" && metadata.promotionCode.trim() !== "" ? metadata.promotionCode.trim() : "DISCOUNT";
4547
+ const discount = selected.length === lineItems.length && order.discountTotal > 0 ? { code: discountCode, amount: order.discountTotal } : null;
4548
+ return Ok({ orderId, currency: order.currency, grandTotal: linesTotal + (shipping?.amount ?? 0) - (discount?.amount ?? 0), lines, ...(shipping ? { shipping } : {}), ...(discount ? { discount } : {}), customer: { name, email, shippingAddress } });
4539
4549
  }
4540
4550
  async reapExports(input) {
4541
4551
  const now = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/plugin-channel-connector",
3
- "version": "0.70.2",
3
+ "version": "0.72.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -22,15 +22,15 @@
22
22
  "dependencies": {
23
23
  "@hono/zod-openapi": "^1.2.2",
24
24
  "hono": "^4.12.5",
25
- "@porulle/core": "0.70.2"
25
+ "@porulle/core": "0.72.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^24.5.2",
29
29
  "eslint": "^9.39.1",
30
30
  "typescript": "5.9.2",
31
31
  "vitest": "^3.2.4",
32
- "@porulle/eslint-config": "0.1.0",
33
- "@porulle/typescript-config": "0.1.0"
32
+ "@porulle/typescript-config": "0.1.0",
33
+ "@porulle/eslint-config": "0.1.0"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"
package/src/service.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { z } from "zod";
3
3
  import {
4
+ CHANNEL_OUT_OF_STOCK,
4
5
  CommerceInvalidTransitionError,
5
6
  CommerceNotFoundError,
6
7
  CommerceValidationError,
@@ -5613,7 +5614,7 @@ export class ChannelConnectorService {
5613
5614
 
5614
5615
  const pushed = await connector.pushOrder(store as ChannelStore, slice);
5615
5616
  if (!pushed.ok) {
5616
- return this.transitionExport(
5617
+ const failed = await this.transitionExport(
5617
5618
  orgId,
5618
5619
  created.value.id,
5619
5620
  "failed",
@@ -5621,6 +5622,13 @@ export class ChannelConnectorService {
5621
5622
  pushed.error.message,
5622
5623
  pushed.error.retriable === true ? "transient" : "definitive",
5623
5624
  );
5625
+ if (pushed.error.code === CHANNEL_OUT_OF_STOCK) {
5626
+ // The store has said nobody will send these goods: cancel, and let the host's cancel path
5627
+ // refund the shopper. Any other refusal can be fixed and retried, so it waits for an operator.
5628
+ const ordersService = this.services.orders as { changeStatus(input: { orderId: string; newStatus: "cancelled"; reason: string }, actor: Actor): Promise<{ ok: boolean }> };
5629
+ await ordersService.changeStatus({ orderId: slice.orderId, newStatus: "cancelled", reason: "channel_out_of_stock" }, actor);
5630
+ }
5631
+ return failed;
5624
5632
  }
5625
5633
 
5626
5634
  await this.db
@@ -5733,7 +5741,10 @@ export class ChannelConnectorService {
5733
5741
  const linesTotal = lines.reduce((sum, line) => sum + line.totalPrice, 0);
5734
5742
  // ponytail: a multi-store order sends no shipping to any store; apportion it when such orders exist.
5735
5743
  const shipping = selected.length === lineItems.length && order.shippingTotal > 0 ? { title: "Shipping", amount: order.shippingTotal } : null;
5736
- return Ok({ orderId, currency: order.currency, grandTotal: linesTotal + (shipping?.amount ?? 0), lines, ...(shipping ? { shipping } : {}), customer: { name, email, shippingAddress } });
5744
+ // ponytail: like delivery, a discount is sent only with the whole order; apportion it when multi-store orders exist.
5745
+ const discountCode = typeof metadata.promotionCode === "string" && metadata.promotionCode.trim() !== "" ? metadata.promotionCode.trim() : "DISCOUNT";
5746
+ const discount = selected.length === lineItems.length && order.discountTotal > 0 ? { code: discountCode, amount: order.discountTotal } : null;
5747
+ return Ok({ orderId, currency: order.currency, grandTotal: linesTotal + (shipping?.amount ?? 0) - (discount?.amount ?? 0), lines, ...(shipping ? { shipping } : {}), ...(discount ? { discount } : {}), customer: { name, email, shippingAddress } });
5737
5748
  }
5738
5749
 
5739
5750
  async reapExports(input: { definitiveMs: number; transientMs: number }): Promise<{ abandonedCount: number; refundedOrderIds: string[] }> {