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.
- package/dist/cli.js +147 -12
- 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.
|
|
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,
|