hookwright 1.5.0 → 1.6.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.
Files changed (2) hide show
  1. package/dist/cli.js +147 -12
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ var init_package = __esm({
24
24
  "package.json"() {
25
25
  package_default = {
26
26
  name: "hookwright",
27
- version: "1.5.0",
27
+ version: "1.6.0",
28
28
  description: "Build real, signed e-commerce webhooks from a live Shopify catalogue \u2014 interactive terminal UI, no backend",
29
29
  keywords: [
30
30
  "webhook",
@@ -911,7 +911,7 @@ function editableFieldsFor(eventName) {
911
911
  }
912
912
  var editableFields3 = editableFieldsFor(DEFAULT_EVENT);
913
913
  function buildPayload3(ctx) {
914
- const { config, items, phone } = ctx;
914
+ const { config, items, phone, live } = ctx;
915
915
  const eventName = ctx.eventName ?? DEFAULT_EVENT;
916
916
  const now = ctx.now ?? /* @__PURE__ */ new Date();
917
917
  const currency = ctx.currency || config.defaults.currency;
@@ -1039,8 +1039,8 @@ function buildPayload3(ctx) {
1039
1039
  }
1040
1040
  case "checkout":
1041
1041
  eventVal = {
1042
- checkout: cartPermalink(domain, items.map(({ variant, quantity }) => ({ variantId: variant?.id, quantity }))),
1043
- cart_value: cartValue.toFixed(2),
1042
+ checkout: live?.kind === "checkout" ? live.url : cartPermalink(domain, items.map(({ variant, quantity }) => ({ variantId: variant?.id, quantity }))),
1043
+ cart_value: live?.kind === "checkout" ? live.cartValue : cartValue.toFixed(2),
1044
1044
  customer,
1045
1045
  bik_customer_id: null
1046
1046
  };
@@ -1048,7 +1048,17 @@ function buildPayload3(ctx) {
1048
1048
  case "orders/create":
1049
1049
  case "orders/updated": {
1050
1050
  const orderNumber = 1e3 + Math.floor(Math.random() * 9e3);
1051
- eventVal = {
1051
+ eventVal = live?.kind === "order" ? {
1052
+ url: live.statusUrl,
1053
+ order_id: live.id,
1054
+ order_number: live.number,
1055
+ order_name: live.name,
1056
+ price: live.price,
1057
+ currency: live.currency ?? currency,
1058
+ order_created_at: live.createdAt ?? now.toISOString(),
1059
+ customer,
1060
+ bik_customer_id: null
1061
+ } : {
1052
1062
  url: `https://${domain}/${crypto3.randomBytes(5).toString("hex")}/orders/${crypto3.randomBytes(16).toString("hex")}/authenticate?key=${crypto3.randomBytes(16).toString("hex")}`,
1053
1063
  order_id: Number(`${Date.now()}`.slice(-13)),
1054
1064
  order_number: orderNumber,
@@ -2472,6 +2482,38 @@ var ShopifyClient = class {
2472
2482
  const { json } = await this.request("/products/count.json");
2473
2483
  return json?.count ?? 0;
2474
2484
  }
2485
+ /**
2486
+ * Abandoned checkouts, newest first. Nitro's checkout webhook carries a real
2487
+ * recovery link, and the cart token inside it is what the receiving side looks
2488
+ * the checkout up by — so it has to come from a checkout that actually exists.
2489
+ */
2490
+ async abandonedCheckouts({ limit = 10 } = {}) {
2491
+ const { json } = await this.request("/checkouts.json", { query: { limit } });
2492
+ return json?.checkouts ?? [];
2493
+ }
2494
+ /** Orders, newest first. */
2495
+ async orders({ limit = 10, status = "any" } = {}) {
2496
+ const { json } = await this.request("/orders.json", { query: { limit, status } });
2497
+ return json?.orders ?? [];
2498
+ }
2499
+ /**
2500
+ * Creates a real order. Receipts and inventory are suppressed: this exists to
2501
+ * give an order webhook something true to point at, not to trade.
2502
+ */
2503
+ async createOrder(order) {
2504
+ const { json } = await this.request("/orders.json", {
2505
+ method: "POST",
2506
+ body: {
2507
+ order: {
2508
+ ...order,
2509
+ send_receipt: false,
2510
+ send_fulfillment_receipt: false,
2511
+ inventory_behaviour: "bypass"
2512
+ }
2513
+ }
2514
+ });
2515
+ return json?.order ?? null;
2516
+ }
2475
2517
  /**
2476
2518
  * Collections, from both endpoints Shopify splits them across: custom
2477
2519
  * (manually curated) and smart (rule based). Either can back a category page.
@@ -2866,7 +2908,7 @@ function loadPayload(file) {
2866
2908
  }
2867
2909
 
2868
2910
  // src/core/build.mjs
2869
- function draftPayload({ cfg, providerId = "cashfree-occ", items, discount = 0, phone, eventName, collection, envelope }) {
2911
+ function draftPayload({ cfg, providerId = "cashfree-occ", items, discount = 0, phone, eventName, collection, envelope, live }) {
2870
2912
  const provider = getProvider(providerId);
2871
2913
  const event = eventName ?? provider.defaultEvent;
2872
2914
  const resolvedPhone = phone ?? cfg.customer.phone;
@@ -2878,6 +2920,7 @@ function draftPayload({ cfg, providerId = "cashfree-occ", items, discount = 0, p
2878
2920
  eventName: event,
2879
2921
  collection,
2880
2922
  envelope,
2923
+ live,
2881
2924
  phone: parsed.ok ? parsed.value : String(resolvedPhone ?? "")
2882
2925
  });
2883
2926
  const editable = provider.editableFieldsFor ? provider.editableFieldsFor(event) : provider.editableFields;
@@ -2893,7 +2936,7 @@ function applyEdits(payload, fields, edits) {
2893
2936
  }
2894
2937
  return payload;
2895
2938
  }
2896
- async function buildPayload8({ cfg, providerId = "cashfree-occ", items = [], discount = 0, phone, checkImageUrls = true, payload: prebuilt, eventName, collection, envelope }) {
2939
+ async function buildPayload8({ cfg, providerId = "cashfree-occ", items = [], discount = 0, phone, checkImageUrls = true, payload: prebuilt, eventName, collection, envelope, live }) {
2897
2940
  const provider0 = getProvider(providerId);
2898
2941
  const needsItems = !provider0.selectionFor || ["cart", "product"].includes(provider0.selectionFor(eventName ?? provider0.defaultEvent));
2899
2942
  if (needsItems && !items.length) throw new ConfigError("no products selected");
@@ -2902,7 +2945,7 @@ async function buildPayload8({ cfg, providerId = "cashfree-occ", items = [], dis
2902
2945
  const parsed = toE164(resolvedPhone, cfg.customer.countryCode);
2903
2946
  if (!parsed.ok) throw new ConfigError(`phone is unusable: ${parsed.reason}`);
2904
2947
  const event = eventName ?? provider.defaultEvent;
2905
- const payload = prebuilt ?? provider.buildPayload({ config: cfg, items, discount, phone: parsed.value, eventName: event, collection, envelope });
2948
+ const payload = prebuilt ?? provider.buildPayload({ config: cfg, items, discount, phone: parsed.value, eventName: event, collection, envelope, live });
2906
2949
  const body = JSON.stringify(payload);
2907
2950
  const report = await runAll({ payload, body, provider, config: cfg, checkImageUrls, eventName: event });
2908
2951
  const summary = provider.summarize ? provider.summarize(payload) : {};
@@ -2930,6 +2973,80 @@ async function buildPayload8({ cfg, providerId = "cashfree-occ", items = [], dis
2930
2973
  return { provider, payload, body, report, file, meta, summary, record };
2931
2974
  }
2932
2975
 
2976
+ // src/shopify/live.mjs
2977
+ var LIVE_EVENTS = {
2978
+ nitro: /* @__PURE__ */ new Set(["checkout", "orders/create", "orders/updated"])
2979
+ };
2980
+ function needsLive(providerId, eventName) {
2981
+ return LIVE_EVENTS[providerId]?.has(eventName) ?? false;
2982
+ }
2983
+ function clientFor(config) {
2984
+ return new ShopifyClient({
2985
+ domain: config.shopify.domain,
2986
+ accessToken: config.shopify.accessToken,
2987
+ apiVersion: config.shopify.apiVersion
2988
+ });
2989
+ }
2990
+ async function liveCheckout({ config, items = [], client = clientFor(config) }) {
2991
+ const checkouts = await client.abandonedCheckouts({ limit: 20 });
2992
+ if (checkouts.length === 0) return null;
2993
+ const wanted = new Set(items.map(({ variant }) => Number(variant?.id)).filter(Boolean));
2994
+ const matching = checkouts.find((c2) => (c2.line_items ?? []).some((li) => wanted.has(Number(li.variant_id))));
2995
+ const checkout = matching ?? checkouts[0];
2996
+ return {
2997
+ kind: "checkout",
2998
+ url: checkout.abandoned_checkout_url,
2999
+ cartValue: money2(checkout.total_price ?? 0).toFixed(2),
3000
+ cartToken: checkout.cart_token,
3001
+ lineItems: checkout.line_items ?? []
3002
+ };
3003
+ }
3004
+ async function liveOrder({ config, items = [], phone, client = clientFor(config) }) {
3005
+ const cust = config.customer ?? {};
3006
+ const lineItems = items.map(({ variant, product, quantity }) => ({
3007
+ variant_id: Number(variant?.id ?? product?.id),
3008
+ quantity: Number(quantity) || 1
3009
+ })).filter((li) => Number.isFinite(li.variant_id) && li.variant_id > 0);
3010
+ if (lineItems.length === 0) return null;
3011
+ const order = await client.createOrder({
3012
+ line_items: lineItems,
3013
+ financial_status: "paid",
3014
+ email: cust.email || void 0,
3015
+ phone: phone || cust.phone || void 0,
3016
+ customer: cust.email || cust.firstName ? { first_name: cust.firstName, last_name: cust.lastName, email: cust.email } : void 0,
3017
+ shipping_address: cust.address1 ? {
3018
+ first_name: cust.firstName,
3019
+ last_name: cust.lastName,
3020
+ address1: cust.address1,
3021
+ address2: cust.address2 || void 0,
3022
+ city: cust.city,
3023
+ province: cust.province,
3024
+ country_code: cust.countryCode,
3025
+ zip: cust.zip,
3026
+ phone: phone || cust.phone || void 0
3027
+ } : void 0,
3028
+ tags: "hookwright",
3029
+ note: "created by hookwright for webhook testing"
3030
+ });
3031
+ if (!order) return null;
3032
+ return {
3033
+ kind: "order",
3034
+ id: Number(order.id),
3035
+ number: Number(order.order_number),
3036
+ name: order.name,
3037
+ price: String(order.total_price ?? "0.00"),
3038
+ currency: order.currency,
3039
+ createdAt: order.created_at,
3040
+ statusUrl: order.order_status_url
3041
+ };
3042
+ }
3043
+ async function resolveLive({ config, providerId, eventName, items, phone, client }) {
3044
+ if (!needsLive(providerId, eventName)) return null;
3045
+ if (!config?.shopify?.domain || !config?.shopify?.accessToken) return null;
3046
+ if (eventName === "checkout") return await liveCheckout({ config, items, ...client ? { client } : {} });
3047
+ return await liveOrder({ config, items, phone, ...client ? { client } : {} });
3048
+ }
3049
+
2933
3050
  // src/core/send.mjs
2934
3051
  import fs4 from "node:fs";
2935
3052
  import path4 from "node:path";
@@ -3108,6 +3225,7 @@ var STEP = {
3108
3225
  VARIANT: "variant",
3109
3226
  QUANTITY: "quantity",
3110
3227
  DISCOUNT: "discount",
3228
+ LIVE: "live",
3111
3229
  MODE: "mode",
3112
3230
  FIELDS: "fields",
3113
3231
  BUILDING: "building",
@@ -3163,6 +3281,7 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
3163
3281
  setItems(items.slice(0, -1));
3164
3282
  return stepInto(last);
3165
3283
  }
3284
+ case STEP.LIVE:
3166
3285
  case STEP.MODE:
3167
3286
  return setStep(STEP.DISCOUNT);
3168
3287
  case STEP.FIELDS:
@@ -3306,17 +3425,23 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
3306
3425
  const nextItems = [...items, { product, variant: pendingVariant, quantity: qty }];
3307
3426
  advance(nextItems, cursor + 1);
3308
3427
  };
3309
- const startReview = (discount, chosenCollection = collection, chosenItems = items) => {
3428
+ const startReview = async (discount, chosenCollection = collection, chosenItems = items) => {
3310
3429
  try {
3430
+ let live = null;
3431
+ if (needsLive(providerId, eventName)) {
3432
+ setStep(STEP.LIVE);
3433
+ live = await resolveLive({ config, providerId, eventName, items: chosenItems, phone: config.customer.phone });
3434
+ }
3311
3435
  const next = draftPayload({
3312
3436
  cfg: config,
3313
3437
  providerId,
3314
3438
  items: chosenItems,
3315
3439
  discount,
3316
3440
  eventName,
3317
- collection: chosenCollection
3441
+ collection: chosenCollection,
3442
+ live
3318
3443
  });
3319
- setDraft({ ...next, discount });
3444
+ setDraft({ ...next, discount, live });
3320
3445
  setStep(STEP.MODE);
3321
3446
  } catch (err) {
3322
3447
  setError({ message: err.message, hint: err.hint, failures: err.failures });
@@ -3326,7 +3451,7 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
3326
3451
  const onDiscount = (raw) => startReview(Math.max(0, Number.parseFloat(raw) || 0));
3327
3452
  const finalise = (payload) => {
3328
3453
  setStep(STEP.BUILDING);
3329
- buildPayload8({ cfg: config, providerId, items, discount: draft.discount, payload, eventName, collection }).then(async (result) => {
3454
+ buildPayload8({ cfg: config, providerId, items, discount: draft.discount, payload, eventName, collection, live: draft.live }).then(async (result) => {
3330
3455
  setBuilt(result);
3331
3456
  let req = null;
3332
3457
  let command = "";
@@ -3400,6 +3525,7 @@ function Build({ config, providerId = "cashfree-occ", onDone }) {
3400
3525
  ) })
3401
3526
  ] }),
3402
3527
  step === STEP.LOADING && /* @__PURE__ */ jsx8(Spinner, { label: `fetching live products from ${store}\u2026` }),
3528
+ step === STEP.LIVE && /* @__PURE__ */ jsx8(Spinner, { label: eventName === "checkout" ? `finding a real abandoned checkout in ${store}\u2026` : `creating a real order in ${store}\u2026` }),
3403
3529
  step === STEP.COLLECTION && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
3404
3530
  /* @__PURE__ */ jsx8(Text8, { children: "Which collection was viewed?" }),
3405
3531
  /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(
@@ -4570,10 +4696,19 @@ async function cmdBuild(args, config) {
4570
4696
  } else {
4571
4697
  log.ok(`${eventName} needs no product or collection`);
4572
4698
  }
4699
+ let live = null;
4700
+ if (needsLive(providerId, eventName)) {
4701
+ log.step(eventName === "checkout" ? `finding a real abandoned checkout in ${config.shopify.domain}\u2026` : `creating a real order in ${config.shopify.domain}\u2026`);
4702
+ live = await resolveLive({ config, providerId, eventName, items, phone: typeof args.flags.phone === "string" ? args.flags.phone : void 0 });
4703
+ if (live?.kind === "checkout") log.ok(`checkout cart token ${live.cartToken}`);
4704
+ else if (live?.kind === "order") log.ok(`order ${live.name} (${live.id})`);
4705
+ else log.warn("no live record available \u2014 falling back to a generated one");
4706
+ }
4573
4707
  const built = await buildPayload8({
4574
4708
  cfg: config,
4575
4709
  providerId,
4576
4710
  eventName,
4711
+ live,
4577
4712
  collection,
4578
4713
  envelope: typeof args.flags.envelope === "string" ? args.flags.envelope : void 0,
4579
4714
  items,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hookwright",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Build real, signed e-commerce webhooks from a live Shopify catalogue — interactive terminal UI, no backend",
5
5
  "keywords": [
6
6
  "webhook",