Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -36,43 +36,19 @@ function totalCartQty(items) {
return items.reduce((sum, item) => sum + (Number(item?.qty ?? 0) || 0), 0);
}

/**
* @returns {{ totalNewBase: number, discountByIndex: Record<number, number>, 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<number, number>} */
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) {
Expand Down Expand Up @@ -105,41 +81,34 @@ 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" },
body: JSON.stringify([zeroDiscountOperation()]),
};
}

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}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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),
};
}

Expand All @@ -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);
Expand Down Expand Up @@ -112,61 +87,28 @@ 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" },
body: JSON.stringify([zeroDiscountOperation()]),
};
}

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}`);
Expand Down
Loading