diff --git a/actions/total-collector-discounts/cheapest-quantity-discount/index.js b/actions/total-collector-discounts/cheapest-quantity-discount/index.js index a59a4f1..8870420 100644 --- a/actions/total-collector-discounts/cheapest-quantity-discount/index.js +++ b/actions/total-collector-discounts/cheapest-quantity-discount/index.js @@ -1,6 +1,6 @@ /** - * Buy 10+ units (cart total qty) → 50% off each of the 3 lowest base-subtotal lines (`sales2cd`). - * Stacks new promo on existing `base_discount_amount` only (same pattern as `total-collector`). + * Buy 10+ units (cart total qty) → 50% off the 3 lowest base-subtotal lines. + * When eligible, returns a single `result` replace with percent and those line ids. * * With `raw-http: true`, body is base64 in `__ow_body`. Verifies * `x-adobe-commerce-webhook-signature` like `collect-taxes` (requires @@ -12,8 +12,8 @@ import { } from "../../../lib/adobe-commerce.js"; import { HTTP_OK } from "../../../lib/http.js"; import { - discountOperation, - getExistingItemBaseDiscount, + discountResultOperation, + getShippingAssignmentItemIds, getShippingItems, parseJsonBody, round2, @@ -36,43 +36,19 @@ function totalCartQty(items) { return items.reduce((sum, item) => sum + (Number(item?.qty ?? 0) || 0), 0); } -/** - * @returns {{ totalNewBase: number, discountByIndex: Record, ruleLabel: string | null }} - */ -function calculateThreeCheapestHalfOff(items) { - const totalQty = totalCartQty(items); - if (totalQty < MIN_TOTAL_QTY) { - return { totalNewBase: 0, discountByIndex: {}, ruleLabel: null }; - } - - const lines = items.map((item, idx) => ({ - idx, - subtotal: lineSubtotal(item), - })); - lines.sort((a, b) => a.subtotal - b.subtotal || a.idx - b.idx); - - /** @type {Record} */ - const discountByIndex = {}; - let totalNewBase = 0; - for (const { idx, subtotal } of lines.slice(0, NUM_CHEAPEST)) { - const d = round2(subtotal * (DISCOUNT_PERCENT / 100)); - discountByIndex[idx] = d; - totalNewBase = round2(totalNewBase + d); - } - - return { - totalNewBase, - discountByIndex, - ruleLabel: RULE_LABEL, - }; +function isEligible(items) { + return items.length > 0 && totalCartQty(items) >= MIN_TOTAL_QTY; } -function createItemBaseDiscountReplaceOp(index, combinedAmount) { - return { - op: "replace", - path: `shippingAssignment/items/${index}/base_discount_amount`, - value: round2(combinedAmount), - }; +/** @returns {number[]} Up to 3 line `item_id` values with the lowest base subtotals. */ +function threeCheapestItemIdsOnly(items) { + const ranked = items + .map((item, idx) => ({ item, idx, subtotal: lineSubtotal(item) })) + .sort((a, b) => a.subtotal - b.subtotal || a.idx - b.idx) + .slice(0, NUM_CHEAPEST); + return getShippingAssignmentItemIds(ranked.map((r) => r.item)).filter( + (id) => !Number.isNaN(id), + ); } function collectCheapestQuantityDiscount(params) { @@ -105,10 +81,7 @@ function collectCheapestQuantityDiscount(params) { }; } - const { totalNewBase, discountByIndex, ruleLabel } = - calculateThreeCheapestHalfOff(items); - - if (totalNewBase <= 0 || !ruleLabel) { + if (!isEligible(items)) { return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, @@ -116,30 +89,26 @@ function collectCheapestQuantityDiscount(params) { }; } - const discount_description_array = { 1: ruleLabel }; - - const operations = [ - discountOperation(totalNewBase, discount_description_array), - ]; + const discountItemIds = threeCheapestItemIdsOnly(items); - for (const [indexStr, newShare] of Object.entries(discountByIndex)) { - const index = Number(indexStr); - if (Number.isNaN(index) || newShare <= 0) { - continue; - } - const item = items[index]; - if (!item) { - continue; - } - const existing = getExistingItemBaseDiscount(item); - const combinedLine = round2(existing + newShare); - operations.push(createItemBaseDiscountReplaceOp(index, combinedLine)); + if (!discountItemIds.length) { + return { + statusCode: HTTP_OK, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([zeroDiscountOperation()]), + }; } return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, - body: JSON.stringify(operations), + body: JSON.stringify([ + discountResultOperation( + DISCOUNT_PERCENT, + { 1: RULE_LABEL }, + discountItemIds, + ), + ]), }; } catch (err) { return webhookErrorResponse(`Server error: ${err.message}`); diff --git a/actions/total-collector-discounts/multi-condition-discount/index.js b/actions/total-collector-discounts/multi-condition-discount/index.js index 2d04330..06562d6 100644 --- a/actions/total-collector-discounts/multi-condition-discount/index.js +++ b/actions/total-collector-discounts/multi-condition-discount/index.js @@ -1,7 +1,7 @@ /** * Multi-condition discount: * Buy at least 5 items AND spend $200+ (base subtotal) → 25% off. - * Applies promo proportionally to each line and stacks with existing line discounts. + * When eligible, returns a single `result` replace with percent and all line ids. * * With `raw-http: true`, body is base64 in `__ow_body`. Verifies * `x-adobe-commerce-webhook-signature` like `collect-taxes` (requires @@ -13,8 +13,8 @@ import { } from "../../../lib/adobe-commerce.js"; import { HTTP_OK } from "../../../lib/http.js"; import { - getExistingItemBaseDiscount, - getExistingItemDiscountAmount, + discountResultOperation, + getShippingAssignmentItemIds, getShippingItems, parseJsonBody, round2, @@ -29,10 +29,8 @@ const RULE_LABEL = "Buy at least 5 items & spend $200 or more → 25% off"; function lineAmounts(item) { const qty = Number(item?.qty ?? 0) || 0; const basePrice = Number(item?.base_price ?? 0) || 0; - const storePrice = Number(item?.price ?? item?.base_price ?? 0) || 0; return { lineBase: round2(basePrice * qty), - lineStore: round2(storePrice * qty), }; } @@ -51,29 +49,6 @@ function isEligible(items) { return qty >= MIN_QTY && baseSubtotal >= MIN_SUBTOTAL; } -/** - * Promo-only per-line 25% discounts before stacking existing amounts. - * Mirrors sales3a calculate_line_discounts_25. - */ -function calculatePromoPerLine(items) { - let totalBase = 0; - const perLine = []; - for (let idx = 0; idx < items.length; idx++) { - const { lineBase, lineStore } = lineAmounts(items[idx]); - const promoBase = round2(lineBase * (DISCOUNT_PERCENT / 100)); - const promoStore = round2(lineStore * (DISCOUNT_PERCENT / 100)); - totalBase = round2(totalBase + promoBase); - perLine.push({ - item_index: idx, - line_base: lineBase, - line_store: lineStore, - base_discount: promoBase, - store_discount: promoStore, - }); - } - return { totalBase, perLine }; -} - function collectMultiConditionDiscount(params) { try { const { success, error } = webhookVerify(params); @@ -112,8 +87,11 @@ function collectMultiConditionDiscount(params) { }; } - const { totalBase: totalPromoBase, perLine } = calculatePromoPerLine(items); - if (totalPromoBase <= 0) { + const discountItemIds = getShippingAssignmentItemIds(items).filter( + (id) => !Number.isNaN(id), + ); + + if (!discountItemIds.length) { return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, @@ -121,52 +99,16 @@ function collectMultiConditionDiscount(params) { }; } - const operations = []; - - for (const row of perLine) { - if (row.base_discount <= 0) { - continue; - } - const idx = row.item_index; - const item = items[idx]; - const combinedBase = round2( - getExistingItemBaseDiscount(item) + row.base_discount, - ); - const combinedStore = round2( - getExistingItemDiscountAmount(item) + row.store_discount, - ); - - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/base_discount_amount`, - value: combinedBase, - }); - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/discount_amount`, - value: combinedStore, - }); - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/discount_percent`, - value: DISCOUNT_PERCENT, - }); - } - - operations.push({ - op: "replace", - path: "result", - value: { - code: "discount", - base_discount: Number(totalPromoBase), - discount_description_array: { 1: RULE_LABEL }, - }, - }); - return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, - body: JSON.stringify(operations), + body: JSON.stringify([ + discountResultOperation( + DISCOUNT_PERCENT, + { 1: RULE_LABEL }, + discountItemIds, + ), + ]), }; } catch (err) { return webhookErrorResponse(`Server error: ${err.message}`); diff --git a/actions/total-collector-discounts/step-price-discount/index.js b/actions/total-collector-discounts/step-price-discount/index.js index 75d1c3a..20ad968 100644 --- a/actions/total-collector-discounts/step-price-discount/index.js +++ b/actions/total-collector-discounts/step-price-discount/index.js @@ -7,17 +7,16 @@ * `x-adobe-commerce-webhook-signature` like `collect-taxes` (requires * `COMMERCE_WEBHOOKS_PUBLIC_KEY` on the action). */ -import { - webhookErrorResponse, - webhookVerify, -} from "../../../lib/adobe-commerce.js"; +// import { +// webhookErrorResponse, +// webhookVerify, +// } from "../../../lib/adobe-commerce.js"; import { HTTP_OK } from "../../../lib/http.js"; import { - getExistingItemBaseDiscount, - getExistingItemDiscountAmount, + discountResultOperation, + getShippingAssignmentItemIds, getShippingItems, parseJsonBody, - round2, zeroDiscountOperation, } from "../../../lib/total-collector-discounts.js"; @@ -31,16 +30,6 @@ const STEP_PCT_1 = 20; const RULE_LABEL = "Step % by cart qty: 1 → 20%, 2 → 35%, 3+ → 45% off subtotal"; -function lineAmounts(item) { - const qty = Number(item?.qty ?? 0) || 0; - const basePrice = Number(item?.base_price ?? 0) || 0; - const storePrice = Number(item?.price ?? item?.base_price ?? 0) || 0; - return { - lineBase: round2(basePrice * qty), - lineStore: round2(storePrice * qty), - }; -} - function totalCartQty(items) { return items.reduce((sum, item) => sum + (Number(item?.qty ?? 0) || 0), 0); } @@ -59,178 +48,63 @@ function tierPercentByTotalQty(totalQty) { return { percent: null, tierNote: null }; } -/** - * Split **new** step discount across lines by subtotal share; last line absorbs rounding. - * Returns promo-only base_discount / store_discount per line (before stacking). - */ -function proportionalLineNewDiscounts( - items, - totalBaseDiscount, - totalStoreDiscount, -) { - const lines = items.map((item, idx) => { - const { lineBase, lineStore } = lineAmounts(item); - return { item_index: idx, line_base: lineBase, line_store: lineStore }; - }); +function collectStepPriceDiscount(params) { + // try { + // const { success, error } = webhookVerify(params); + // if (!success) { + // return webhookErrorResponse( + // `Failed to verify the webhook signature: ${error}`, + // ); + // } - const baseSub = round2(lines.reduce((s, r) => s + r.line_base, 0)); - const storeSub = round2(lines.reduce((s, r) => s + r.line_store, 0)); + const data = parseJsonBody(params); - if (baseSub <= 0 || totalBaseDiscount <= 0) { - return lines.map((row) => ({ - ...row, - base_discount: 0, - store_discount: 0, - })); + if (data === null) { + return { + statusCode: HTTP_OK, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([ + { op: "exception", message: "Invalid webhook payload" }, + ]), + }; } - let remainB = totalBaseDiscount; - let remainS = totalStoreDiscount; - const n = lines.length; - const out = []; + const items = getShippingItems(data); + const discountItemIds = getShippingAssignmentItemIds(items); - for (let i = 0; i < n; i++) { - const row = lines[i]; - let bd; - let sd; - if (i < n - 1) { - const share = row.line_base / baseSub; - bd = round2(totalBaseDiscount * share); - sd = - storeSub > 0 - ? round2(totalStoreDiscount * (row.line_store / storeSub)) - : 0; - remainB = round2(remainB - bd); - remainS = round2(remainS - sd); - } else { - bd = round2(remainB); - sd = round2(remainS); - } - out.push({ ...row, base_discount: bd, store_discount: sd }); + if (!items.length) { + return { + statusCode: HTTP_OK, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([zeroDiscountOperation()]), + }; } - return out; -} -function collectStepPriceDiscount(params) { - try { - const { success, error } = webhookVerify(params); - if (!success) { - return webhookErrorResponse( - `Failed to verify the webhook signature: ${error}`, - ); - } - - const data = parseJsonBody(params); - - if (data === null) { - return { - statusCode: HTTP_OK, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify([ - { op: "exception", message: "Invalid webhook payload" }, - ]), - }; - } - - const items = getShippingItems(data); - if (!items.length) { - return { - statusCode: HTTP_OK, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify([zeroDiscountOperation()]), - }; - } - - const totalQty = totalCartQty(items); - const baseSubtotal = round2( - items.reduce((s, it) => s + lineAmounts(it).lineBase, 0), - ); - const storeSubtotal = round2( - items.reduce((s, it) => s + lineAmounts(it).lineStore, 0), - ); - - const { percent, tierNote } = tierPercentByTotalQty(totalQty); - - if (percent == null) { - return { - statusCode: HTTP_OK, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify([zeroDiscountOperation()]), - }; - } - - const totalPromoBase = round2((baseSubtotal * percent) / 100); - const totalPromoStore = round2((storeSubtotal * percent) / 100); - - if (totalPromoBase <= 0) { - return { - statusCode: HTTP_OK, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify([zeroDiscountOperation()]), - }; - } - - const perLine = proportionalLineNewDiscounts( - items, - totalPromoBase, - totalPromoStore, - ); - - const operations = []; - - for (const row of perLine) { - if (row.base_discount <= 0) { - continue; - } - const idx = row.item_index; - const item = items[idx]; - const existingBase = getExistingItemBaseDiscount(item); - const existingStore = getExistingItemDiscountAmount(item); - const combinedBase = round2(existingBase + row.base_discount); - const combinedStore = round2(existingStore + row.store_discount); - - const discountPercent = - row.line_base > 0 - ? Math.round((100 * 10_000 * combinedBase) / row.line_base) / 10_000 - : 0; - - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/base_discount_amount`, - value: combinedBase, - }); - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/discount_amount`, - value: combinedStore, - }); - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/discount_percent`, - value: discountPercent, - }); - } - - operations.push({ - op: "replace", - path: "result", - value: { - code: "discount", - base_discount: Number(totalPromoBase), - discount_description_array: { - 1: `${RULE_LABEL} (${tierNote})`, - }, - }, - }); + const totalQty = totalCartQty(items); + const { percent, tierNote } = tierPercentByTotalQty(totalQty); + if (percent == null || percent <= 0) { return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, - body: JSON.stringify(operations), + body: JSON.stringify([zeroDiscountOperation()]), }; - } catch (err) { - return webhookErrorResponse(`Server error: ${err.message}`); } + + return { + statusCode: HTTP_OK, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([ + discountResultOperation( + percent, + { 1: `${RULE_LABEL} (${tierNote})` }, + discountItemIds, + ), + ]), + }; + // } catch (err) { + // return webhookErrorResponse(`Server error: ${err.message}`); + // } } export function main(params) { diff --git a/actions/total-collector-discounts/tiered-quantity-discount/index.js b/actions/total-collector-discounts/tiered-quantity-discount/index.js index 9222e37..42b9e80 100644 --- a/actions/total-collector-discounts/tiered-quantity-discount/index.js +++ b/actions/total-collector-discounts/tiered-quantity-discount/index.js @@ -1,7 +1,7 @@ /** * Quantity-tiered discount webhook. - * Buy 3+ → 10% off; Buy 6+ → 15% off (on each line's price × qty). - * Stacks new tier discount on top of existing line discounts (same idea as `total-collector`). + * Buy 3+ → 10% off; Buy 6+ → 15% off. + * When eligible, returns a single `result` replace with percent and all line ids. * * With `raw-http: true`, body is base64 in `__ow_body`. Verifies * `x-adobe-commerce-webhook-signature` like `collect-taxes` (requires @@ -13,11 +13,10 @@ import { } from "../../../lib/adobe-commerce.js"; import { HTTP_OK } from "../../../lib/http.js"; import { - getExistingItemBaseDiscount, - getExistingItemDiscountAmount, + discountResultOperation, + getShippingAssignmentItemIds, getShippingItems, parseJsonBody, - round2, zeroDiscountOperation, } from "../../../lib/total-collector-discounts.js"; @@ -33,44 +32,15 @@ function totalCartQty(items) { return items.reduce((sum, item) => sum + (Number(item?.qty ?? 0) || 0), 0); } -function lineAmounts(item) { - const qty = Number(item?.qty ?? 0) || 0; - const basePrice = Number(item?.base_price ?? 0) || 0; - const storePrice = Number(item?.price ?? item?.base_price ?? 0) || 0; - return { - lineBase: round2(basePrice * qty), - lineStore: round2(storePrice * qty), - }; -} - +/** @returns {{ percent: number; ruleLabel: string } | { percent: null; ruleLabel: null }} */ function tierPercentForQty(totalQty) { if (totalQty >= QTY_TIER_2) { - return { percentage: PERCENT_TIER_2, ruleLabel: RULE_LABEL_TIER_2 }; + return { percent: PERCENT_TIER_2, ruleLabel: RULE_LABEL_TIER_2 }; } if (totalQty >= QTY_TIER_1) { - return { percentage: PERCENT_TIER_1, ruleLabel: RULE_LABEL_TIER_1 }; + return { percent: PERCENT_TIER_1, ruleLabel: RULE_LABEL_TIER_1 }; } - return { percentage: 0, ruleLabel: null }; -} - -function calculateTieredLineDiscounts(items, percentage) { - let totalBase = 0; - const perLine = []; - for (let idx = 0; idx < items.length; idx++) { - const item = items[idx]; - const { lineBase, lineStore } = lineAmounts(item); - const baseDisc = round2(lineBase * (percentage / 100)); - const storeDisc = round2(lineStore * (percentage / 100)); - totalBase = round2(totalBase + baseDisc); - perLine.push({ - item_index: idx, - line_base: lineBase, - line_store: lineStore, - base_discount: baseDisc, - store_discount: storeDisc, - }); - } - return { totalBase: round2(totalBase), perLine }; + return { percent: null, ruleLabel: null }; } function collectTieredQuantityDiscount(params) { @@ -104,9 +74,9 @@ function collectTieredQuantityDiscount(params) { } const totalQty = totalCartQty(items); - const { percentage, ruleLabel } = tierPercentForQty(totalQty); + const { percent, ruleLabel } = tierPercentForQty(totalQty); - if (percentage <= 0 || !ruleLabel) { + if (percent == null || percent <= 0 || !ruleLabel) { return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, @@ -114,10 +84,11 @@ function collectTieredQuantityDiscount(params) { }; } - const { totalBase: totalTierBaseDiscount, perLine } = - calculateTieredLineDiscounts(items, percentage); + const discountItemIds = getShippingAssignmentItemIds(items).filter( + (id) => !Number.isNaN(id), + ); - if (totalTierBaseDiscount <= 0) { + if (!discountItemIds.length) { return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, @@ -125,53 +96,12 @@ function collectTieredQuantityDiscount(params) { }; } - const operations = []; - - for (const row of perLine) { - if (row.base_discount <= 0) { - continue; - } - - const item = items[row.item_index]; - const existingBase = getExistingItemBaseDiscount(item); - const existingStore = getExistingItemDiscountAmount(item); - - const combinedBase = round2(existingBase + row.base_discount); - const combinedStore = round2(existingStore + row.store_discount); - - const idx = row.item_index; - - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/base_discount_amount`, - value: combinedBase, - }); - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/discount_amount`, - value: combinedStore, - }); - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/discount_percent`, - value: Number(percentage), - }); - } - - operations.push({ - op: "replace", - path: "result", - value: { - code: "discount", - base_discount: Number(totalTierBaseDiscount), - discount_description_array: { 1: ruleLabel }, - }, - }); - return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, - body: JSON.stringify(operations), + body: JSON.stringify([ + discountResultOperation(percent, { 1: ruleLabel }, discountItemIds), + ]), }; } catch (err) { return webhookErrorResponse(`Server error: ${err.message}`); diff --git a/actions/total-collector-discounts/tiered-total-spend-discount/index.js b/actions/total-collector-discounts/tiered-total-spend-discount/index.js index bfbf733..4465d86 100644 --- a/actions/total-collector-discounts/tiered-total-spend-discount/index.js +++ b/actions/total-collector-discounts/tiered-total-spend-discount/index.js @@ -1,7 +1,7 @@ /** * Tiered total-spend discount: * Spend $100+ → 10% off, Spend $200+ → 20% off. - * Applies promo proportionally to each line and stacks with existing line discounts. + * When eligible, returns a single `result` replace with percent and all line ids. * * With `raw-http: true`, body is base64 in `__ow_body`. Verifies * `x-adobe-commerce-webhook-signature` like `collect-taxes` (requires @@ -13,7 +13,8 @@ import { } from "../../../lib/adobe-commerce.js"; import { HTTP_OK } from "../../../lib/http.js"; import { - getExistingItemBaseDiscount, + discountResultOperation, + getShippingAssignmentItemIds, getShippingItems, parseJsonBody, round2, @@ -28,39 +29,25 @@ const TIERS = [ function lineAmounts(item) { const qty = Number(item?.qty ?? 0) || 0; const basePrice = Number(item?.base_price ?? 0) || 0; - const storePrice = Number(item?.price ?? item?.base_price ?? 0) || 0; return { lineBase: round2(basePrice * qty), - lineStore: round2(storePrice * qty), }; } -function getTierForSubtotal(baseSubtotal) { +function cartBaseSubtotal(items) { + return round2( + items.reduce((sum, item) => sum + lineAmounts(item).lineBase, 0), + ); +} + +/** @returns {{ percent: number; ruleLabel: string } | { percent: null; ruleLabel: null }} */ +function tierPercentForSubtotal(baseSubtotal) { for (const tier of TIERS) { if (baseSubtotal >= tier.minSubtotal) { - return tier; + return { percent: tier.percent, ruleLabel: tier.label }; } } - return null; -} - -function calculatePromoPerLine(items, percent) { - let totalBase = 0; - const perLine = []; - for (let idx = 0; idx < items.length; idx++) { - const { lineBase, lineStore } = lineAmounts(items[idx]); - const promoBase = round2(lineBase * (percent / 100)); - const promoStore = round2(lineStore * (percent / 100)); - totalBase = round2(totalBase + promoBase); - perLine.push({ - item_index: idx, - line_base: lineBase, - line_store: lineStore, - base_discount: promoBase, - store_discount: promoStore, - }); - } - return { totalBase, perLine }; + return { percent: null, ruleLabel: null }; } function collectTieredTotalSpendDiscount(params) { @@ -93,12 +80,10 @@ function collectTieredTotalSpendDiscount(params) { }; } - const baseSubtotal = round2( - items.reduce((sum, item) => sum + lineAmounts(item).lineBase, 0), - ); + const baseSubtotal = cartBaseSubtotal(items); + const { percent, ruleLabel } = tierPercentForSubtotal(baseSubtotal); - const tier = getTierForSubtotal(baseSubtotal); - if (!tier) { + if (percent == null || percent <= 0 || !ruleLabel) { return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, @@ -106,11 +91,11 @@ function collectTieredTotalSpendDiscount(params) { }; } - const { totalBase: totalPromoBase, perLine } = calculatePromoPerLine( - items, - tier.percent, + const discountItemIds = getShippingAssignmentItemIds(items).filter( + (id) => !Number.isNaN(id), ); - if (totalPromoBase <= 0) { + + if (!discountItemIds.length) { return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, @@ -118,38 +103,12 @@ function collectTieredTotalSpendDiscount(params) { }; } - const operations = []; - - for (const row of perLine) { - if (row.base_discount <= 0) { - continue; - } - const idx = row.item_index; - const existing = getExistingItemBaseDiscount(items[idx]); - const combinedLine = round2(existing + row.base_discount); - - operations.push({ - op: "replace", - path: `shippingAssignment/items/${idx}/base_discount_amount`, - value: combinedLine, - }); - } - - operations.push({ - op: "replace", - path: "result", - value: { - code: "discount", - // Cart result sends promo-only discount for this rule execution. - base_discount: Number(totalPromoBase), - discount_description_array: { 1: tier.label }, - }, - }); - return { statusCode: HTTP_OK, headers: { "Content-Type": "application/json" }, - body: JSON.stringify(operations), + body: JSON.stringify([ + discountResultOperation(percent, { 1: ruleLabel }, discountItemIds), + ]), }; } catch (err) { return webhookErrorResponse(`Server error: ${err.message}`); diff --git a/lib/total-collector-discounts.js b/lib/total-collector-discounts.js index 24a05f2..99677e4 100644 --- a/lib/total-collector-discounts.js +++ b/lib/total-collector-discounts.js @@ -23,6 +23,11 @@ export function getShippingItems(webhookData) { return Array.isArray(assignment.items) ? assignment.items : []; } +/** One `item_id` per `shippingAssignment.items` entry, e.g. `[1, 2, 3, 4]`. */ +export function getShippingAssignmentItemIds(shippingItems) { + return shippingItems.map((item) => Number(item?.item_id ?? item?.id)); +} + /** Numeric `item_id` or `id` from a shipping line, for matching `quote.items`. */ export function itemIdentifierForLookup(item) { for (const key of ["item_id", "id"]) { @@ -108,6 +113,25 @@ export function discountOperation(totalDiscount, descriptionDict) { }; } +/** Single `result` replace: percentage discount scoped to `discountItemIds`. */ +export function discountResultOperation( + percent, + descriptionDict, + discountItemIds, +) { + return { + op: "replace", + path: "result", + value: { + code: "discount", + base_discount: Number(percent), + discount_type: "percentage", + discount_item_id_array: discountItemIds, + discount_description_array: descriptionDict, + }, + }; +} + function categoryFromSku(sku) { if (typeof sku !== "string") { return null;