hookwright 1.9.0 → 1.10.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 +119 -28
  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.9.0",
27
+ version: "1.10.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",
@@ -1142,7 +1142,6 @@ var EVENTS2 = [
1142
1142
  { eventName: "collection_page_viewed", type: "shopflo_collection_page_viewed", label: "Collection Page View", selection: "collection", subject: "one collection", envelope: "data" },
1143
1143
  { eventName: "product_page_viewed", type: "shopflo_product_page_viewed", label: "Product Page View", selection: "product", subject: "one product", envelope: "data" },
1144
1144
  { eventName: "added_to_cart_ui", type: "shopflo_added_to_cart", label: "Added To Cart", selection: "cart", subject: "cart contents", envelope: "root" },
1145
- { eventName: "checkout_clicked", type: "shopflo_checkout_clicked", label: "Checkout Clicked", selection: "cart", subject: "cart contents", envelope: "root" },
1146
1145
  { eventName: "checkout_abandoned", type: "shopflo_checkout_abandoned", label: "Abandoned Checkout", selection: "cart", subject: "cart contents", envelope: "root" },
1147
1146
  { eventName: "order_completed", type: "shopflo_order_completed", label: "Order Completed", selection: "cart", subject: "ordered items", envelope: "root" }
1148
1147
  ];
@@ -1173,16 +1172,20 @@ var BROWSING_FIELDS = [
1173
1172
  { source: "session_id", target: "sessionId" }
1174
1173
  ];
1175
1174
  var CART_FIELDS = [
1176
- { source: "line_items[].title", target: "cart.productName", required: true, note: "the first PAID line wins; freebies rank last" },
1177
- { source: "line_items[].product_image", target: "cart.image" },
1175
+ { source: "line_items[].title", target: "cart.productDetails", required: true, note: "the first PAID line wins; freebies rank last" },
1176
+ { source: "line_items[].title", target: "cart.productName", note: 'the featured line alone, without the "and N more"' },
1177
+ { source: "line_items[].image", target: "cart.image", note: "looked up from product_id when the line has none" },
1178
+ { source: "\u2014 (store domain + handle)", target: "cart.productUrl", note: "resolved, not sent" },
1179
+ { source: "\u2014 (store domain + line ids)", target: "cartLink", note: "resolved, not sent" },
1180
+ { source: "\u2014 (company shopify integration)", target: "storeUrl", note: "resolved, not sent" },
1178
1181
  { source: "line_items[].price", target: "\u2014 (ranking only)", note: "a zero price marks the line a freebie" },
1179
1182
  { source: "line_items[].quantity", target: "cart.quantity", note: "summed across the cart" },
1180
1183
  { source: "line_items[]", target: "cart.itemCount", note: "the line count, not the quantity" },
1181
- { source: "total_price", target: "pricing.totalPrice" },
1182
- { source: "subtotal_price", target: "pricing.subtotal" },
1183
- { source: "total_discount", target: "pricing.totalDiscount" },
1184
- { source: "currency", target: "pricing.currency" },
1185
- { source: "note_attributes[shopflo_checkout_url]", target: "checkoutUrl", note: "checkout_abandoned puts the link here, not in token_id" }
1184
+ { source: "total_price", target: "total_price", note: "the body is stored as sent" },
1185
+ { source: "subtotal_price", target: "subtotal_price" },
1186
+ { source: "total_discount", target: "total_discount" },
1187
+ { source: "currency", target: "currency" },
1188
+ { source: "abandoned_checkout_url", target: "checkoutUrl", required: true, note: "token_id is a bare uuid, only usable once built into a link" }
1186
1189
  ];
1187
1190
  var FIELD_MAPS2 = {
1188
1191
  store_page_view: [...COMMON_FIELDS2, ...BROWSING_FIELDS],
@@ -1198,7 +1201,6 @@ var FIELD_MAPS2 = {
1198
1201
  { source: "data.product_type", target: "product.type" }
1199
1202
  ],
1200
1203
  added_to_cart_ui: [...COMMON_FIELDS2, ...CART_FIELDS],
1201
- checkout_clicked: [...COMMON_FIELDS2, ...CART_FIELDS],
1202
1204
  checkout_abandoned: [...COMMON_FIELDS2, ...CART_FIELDS],
1203
1205
  order_completed: [
1204
1206
  ...COMMON_FIELDS2,
@@ -1311,7 +1313,7 @@ function buildPayload4(ctx) {
1311
1313
  title: product.title,
1312
1314
  price: money2(variant?.price ?? 0).toFixed(2),
1313
1315
  quantity,
1314
- product_image: imageForVariant(product, variant)
1316
+ image: imageForVariant(product, variant)
1315
1317
  }));
1316
1318
  const subtotal = money2(lineItems.reduce((sum, li) => sum + Number(li.price) * li.quantity, 0));
1317
1319
  const discount = money2(Math.min(ctx.discount ?? 0, subtotal));
@@ -1320,17 +1322,38 @@ function buildPayload4(ctx) {
1320
1322
  return {
1321
1323
  event_name: eventName,
1322
1324
  session_id: ctx.sessionId ?? `sf-${now.getTime()}`,
1323
- token_id: checkoutUrl,
1325
+ token_id: `sf-${now.getTime()}`,
1326
+ abandoned_checkout_url: checkoutUrl,
1324
1327
  source: "shopflo",
1325
1328
  channel: "web",
1326
1329
  email: cust.email,
1327
1330
  phone,
1328
1331
  created_at: now.toISOString(),
1329
- // checkout_abandoned carries the recovery link here; the consumer prefers it
1332
+ // the live note_attributes carry utm and the landing page, never a checkout url
1330
1333
  note_attributes: [
1331
1334
  { name: "landing_page", value: `/products/${items?.[0]?.product?.handle ?? ""}` },
1332
- { name: "shopflo_checkout_url", value: checkoutUrl }
1335
+ { name: "utm_source", value: "ig" },
1336
+ { name: "customer_type", value: "NEW" }
1333
1337
  ],
1338
+ utm_params: {
1339
+ utm_source: "ig",
1340
+ utm_medium: "Instagram_Feed",
1341
+ utm_campaign: "hookwright",
1342
+ utm_content: "hookwright",
1343
+ utm_term: "0",
1344
+ landing_page: `/products/${items?.[0]?.product?.handle ?? ""}`,
1345
+ landing_page_url: `https://${domain}/products/${items?.[0]?.product?.handle ?? ""}`
1346
+ },
1347
+ shipping_address: {
1348
+ city: "Bangalore",
1349
+ province: "Karnataka",
1350
+ province_code: "KA",
1351
+ zip: "560024",
1352
+ country: "India",
1353
+ country_code: "IN",
1354
+ phone,
1355
+ email: cust.email
1356
+ },
1334
1357
  line_items: lineItems,
1335
1358
  customer: { uid: "sf-user", first_name: cust.firstName, last_name: cust.lastName, email: cust.email, phone },
1336
1359
  currency,
@@ -1360,7 +1383,7 @@ function webhookSecret4(config) {
1360
1383
  }
1361
1384
  function storeOf(payload) {
1362
1385
  const brandUrl = payload?.eventPayload?.clientData?.brandUrl;
1363
- const candidate = brandUrl ?? payload?.token_id;
1386
+ const candidate = brandUrl ?? payload?.utm_params?.landing_page_url ?? payload?.data?.page_url;
1364
1387
  if (!candidate) return void 0;
1365
1388
  try {
1366
1389
  return new URL(candidate).host;
@@ -1494,7 +1517,19 @@ function nationalNumber(raw, regionCode) {
1494
1517
  // src/providers/flexype.mjs
1495
1518
  var GATE_PATH3 = "event_type";
1496
1519
  var GATE_VALUE2 = "ABANDONED_CHECKOUT";
1520
+ var EVENTS3 = [
1521
+ { eventName: "ABANDONED_CHECKOUT", type: "flexype_abandoned_checkout", label: "Abandoned Checkout", selection: "cart", subject: "cart contents" },
1522
+ { eventName: "COLLECTION_VIEWED", type: "flexype_collection_viewed", label: "Collection Viewed", selection: "collection", subject: "one collection" },
1523
+ { eventName: "PRODUCT_VIEWED", type: "flexype_product_viewed", label: "Product Viewed", selection: "product", subject: "one product" }
1524
+ ];
1497
1525
  var DEFAULT_EVENT3 = GATE_VALUE2;
1526
+ var eventDef2 = (eventName) => EVENTS3.find((e) => e.eventName === eventName) ?? EVENTS3[0];
1527
+ function eventFromPayload3(payload) {
1528
+ return payload?.event_type;
1529
+ }
1530
+ function selectionFor3(eventName) {
1531
+ return eventDef2(eventName).selection;
1532
+ }
1498
1533
  var fieldMap5 = [
1499
1534
  { source: "event_type", target: "\xABgate\xBB", required: true, note: "must be ABANDONED_CHECKOUT or the event is dropped" },
1500
1535
  { source: "payload.user.phone_with_dial_code", target: "phone", required: true, note: "falls back to dial_code + phone" },
@@ -1541,12 +1576,64 @@ var editableFields5 = [
1541
1576
  { group: "Attribution", path: "payload.metadata.landing_page", label: "Landing page", optional: true }
1542
1577
  ];
1543
1578
  function buildPayload5(ctx) {
1544
- const { config, items, phone, live } = ctx;
1579
+ const { config, items, phone, live, collection } = ctx;
1580
+ const eventName = ctx.eventName ?? DEFAULT_EVENT3;
1545
1581
  const now = ctx.now ?? /* @__PURE__ */ new Date();
1546
1582
  const currency = ctx.currency || config.defaults.currency;
1547
1583
  const domain = config.shopify.domain;
1548
1584
  const cust = config.customer;
1549
1585
  const { dialCode, national } = splitPhone(phone, cust.countryCode);
1586
+ const envelope = {
1587
+ event_id: `fx-${now.getTime()}`,
1588
+ event_type: eventName,
1589
+ event_time: now.toISOString(),
1590
+ store: {
1591
+ id: 1,
1592
+ name: config.shopify.shop?.name ?? config.shopify.domain,
1593
+ shopify_domain: config.shopify.domain,
1594
+ main_domain: config.shopify.domain
1595
+ }
1596
+ };
1597
+ const user = {
1598
+ email: config.customer.email,
1599
+ full_name: `${config.customer.firstName} ${config.customer.lastName}`.trim(),
1600
+ first_name: config.customer.firstName,
1601
+ last_name: config.customer.lastName,
1602
+ dial_code: splitPhone(phone, config.customer.countryCode).dialCode,
1603
+ phone: splitPhone(phone, config.customer.countryCode).national,
1604
+ phone_with_dial_code: phone
1605
+ };
1606
+ if (eventName === "COLLECTION_VIEWED") {
1607
+ return {
1608
+ ...envelope,
1609
+ payload: {
1610
+ user,
1611
+ collection_viewed: {
1612
+ collection_id: Number(collection?.id) || 0,
1613
+ name: collection?.title ?? "",
1614
+ handle: collection?.handle ?? "",
1615
+ image: collection?.image?.src ?? "",
1616
+ timestamp: now.toISOString()
1617
+ }
1618
+ }
1619
+ };
1620
+ }
1621
+ if (eventName === "PRODUCT_VIEWED") {
1622
+ const first = items?.[0];
1623
+ return {
1624
+ ...envelope,
1625
+ payload: {
1626
+ user,
1627
+ product_viewed: {
1628
+ product_id: Number(first?.product?.id) || 0,
1629
+ title: first?.product?.title ?? "",
1630
+ handle: first?.product?.handle ?? "",
1631
+ image: imageForVariant(first?.product, first?.variant) ?? "",
1632
+ timestamp: now.toISOString()
1633
+ }
1634
+ }
1635
+ };
1636
+ }
1550
1637
  const cartItems = (items ?? []).map(({ product, variant, quantity }) => {
1551
1638
  const unit = money2(variant?.price ?? 0);
1552
1639
  return {
@@ -1638,7 +1725,11 @@ var flexype_default = {
1638
1725
  urlKey: "flexype",
1639
1726
  label: "FlexyPe",
1640
1727
  eventType: "flexype_abandoned_checkout",
1728
+ events: EVENTS3,
1641
1729
  defaultEvent: DEFAULT_EVENT3,
1730
+ gateFor: (eventName) => ({ path: GATE_PATH3, value: eventName }),
1731
+ selectionFor: selectionFor3,
1732
+ eventFromPayload: eventFromPayload3,
1642
1733
  gate: { path: GATE_PATH3, value: GATE_VALUE2 },
1643
1734
  signatureScheme: "static shared secret in the header (no body signature)",
1644
1735
  signatureHeader: "x-flexype-authorization",
@@ -1654,7 +1745,7 @@ var flexype_default = {
1654
1745
  // src/providers/return-prime.mjs
1655
1746
  var STATUSES = ["requested", "approved", "received", "inspected", "rejected"];
1656
1747
  var titleCase = (v) => v.charAt(0).toUpperCase() + v.slice(1);
1657
- var EVENTS3 = ["return", "exchange"].flatMap(
1748
+ var EVENTS4 = ["return", "exchange"].flatMap(
1658
1749
  (requestType) => STATUSES.map((status) => ({
1659
1750
  eventName: `${requestType}_${status}`,
1660
1751
  requestType,
@@ -1666,14 +1757,14 @@ var EVENTS3 = ["return", "exchange"].flatMap(
1666
1757
  }))
1667
1758
  );
1668
1759
  var DEFAULT_EVENT4 = "return_requested";
1669
- var eventDef2 = (eventName) => EVENTS3.find((e) => e.eventName === eventName) ?? EVENTS3.find((e) => e.eventName === DEFAULT_EVENT4);
1670
- function eventFromPayload3(payload) {
1760
+ var eventDef3 = (eventName) => EVENTS4.find((e) => e.eventName === eventName) ?? EVENTS4.find((e) => e.eventName === DEFAULT_EVENT4);
1761
+ function eventFromPayload4(payload) {
1671
1762
  const request = payload?.request ?? payload ?? {};
1672
1763
  if (!request.request_type || !request.status) return void 0;
1673
1764
  return `${request.request_type}_${request.status}`;
1674
1765
  }
1675
- function selectionFor3(eventName) {
1676
- return eventDef2(eventName).selection;
1766
+ function selectionFor4(eventName) {
1767
+ return eventDef3(eventName).selection;
1677
1768
  }
1678
1769
  var COMMON_FIELDS3 = [
1679
1770
  { source: "request_type", target: "\xABgate\xBB + requestType", required: true, note: "return or exchange" },
@@ -1699,10 +1790,10 @@ var EXCHANGE_FIELDS = [
1699
1790
  ];
1700
1791
  var fieldMap6 = COMMON_FIELDS3;
1701
1792
  function fieldMapFor(eventName) {
1702
- return eventDef2(eventName).requestType === "exchange" ? [...COMMON_FIELDS3, ...EXCHANGE_FIELDS] : COMMON_FIELDS3;
1793
+ return eventDef3(eventName).requestType === "exchange" ? [...COMMON_FIELDS3, ...EXCHANGE_FIELDS] : COMMON_FIELDS3;
1703
1794
  }
1704
1795
  function editableFieldsFor3(eventName) {
1705
- const def = eventDef2(eventName);
1796
+ const def = eventDef3(eventName);
1706
1797
  const p = (path6) => def.envelope === "root" ? path6 : `request.${path6}`;
1707
1798
  return [
1708
1799
  { group: "Request", path: p("request_number"), label: "Request number" },
@@ -1728,7 +1819,7 @@ function statusBlock(reached, at, comment = null) {
1728
1819
  function buildPayload6(ctx) {
1729
1820
  const { config, items, phone } = ctx;
1730
1821
  const eventName = ctx.eventName ?? DEFAULT_EVENT4;
1731
- const def = eventDef2(eventName);
1822
+ const def = eventDef3(eventName);
1732
1823
  const now = ctx.now ?? /* @__PURE__ */ new Date();
1733
1824
  const currency = ctx.currency || config.defaults.currency;
1734
1825
  const cust = config.customer;
@@ -1930,19 +2021,19 @@ var return_prime_default = {
1930
2021
  id: "return-prime",
1931
2022
  urlKey: "return_prime",
1932
2023
  label: "Return Prime",
1933
- events: EVENTS3,
2024
+ events: EVENTS4,
1934
2025
  defaultEvent: DEFAULT_EVENT4,
1935
2026
  eventType: "return_prime_return_requested",
1936
2027
  gate: { path: "request.request_type", value: "return" },
1937
- gateFor: (eventName) => ({ path: "request.status", value: eventDef2(eventName).status }),
2028
+ gateFor: (eventName) => ({ path: "request.status", value: eventDef3(eventName).status }),
1938
2029
  signatureScheme: "none documented \u2014 every request is accepted",
1939
2030
  signatureHeader: null,
1940
2031
  fieldMap: fieldMap6,
1941
2032
  fieldMapFor,
1942
2033
  editableFields: editableFields6,
1943
2034
  editableFieldsFor: editableFieldsFor3,
1944
- selectionFor: selectionFor3,
1945
- eventFromPayload: eventFromPayload3,
2035
+ selectionFor: selectionFor4,
2036
+ eventFromPayload: eventFromPayload4,
1946
2037
  buildPayload: buildPayload6,
1947
2038
  sign: sign6,
1948
2039
  webhookUrl: webhookUrl6,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hookwright",
3
- "version": "1.9.0",
3
+ "version": "1.10.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",