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
27 changes: 27 additions & 0 deletions packages/apps/discounts/lib-mjs/apply-discount.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -517,9 +517,11 @@ export default async ({ params, application }) => {
});

const discountRules = getValidDiscountRules(config.discount_rules, params);
let isCouponFound = false;
if (discountRules.length) {
const { discountRule, discountMatchEnum } = matchDiscountRule(discountRules, params);
if (discountRule) {
isCouponFound = discountMatchEnum === 'COUPON';
const {
valid: isValidByItems,
items: filteredItems,
Expand Down Expand Up @@ -679,11 +681,36 @@ export default async ({ params, application }) => {
};
}
}
} else if (isCouponFound && discountRule.discount.min_amount > checkAmount) {
// coupon exists but cart is still under the minimum amount
const missingAmount = ecomUtils.formatMoney(
discountRule.discount.min_amount - checkAmount,
params.currency_id,
params.lang,
);
response.invalid_coupon_message = params.lang === 'pt_br'
? `Adicione mais ${missingAmount} ao carrinho para usar este cupom.`
: `Add ${missingAmount} more to the cart to use this coupon.`;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional. Dois pontos aqui:

  1. Isso muda comportamento também sem campanha ativa. O DiscountApplier já tratava available_extra_discount.min_amount sem discount_rule com "Adicione mais $1 ao carrinho para ganhar o desconto." em variante info; invalid_coupon_message sobrescreve isso e vira warning com outro texto. O valor daqui é até mais correto (desconta brindes), mas vale usar o mesmo texto do i18n.
  2. Com amount_field: 'freight' a mensagem diz "ao carrinho" quando o que falta é frete (regra min_amount: 50, frete R$ 20 → "Adicione mais R$ 30,00 ao carrinho"). Sugiro pular a mensagem nesse campo, com teste cobrindo.

}
}
}

addFreebies();
if (params.discount_coupon && !isCouponFound) {
const couponCode = params.discount_coupon.toUpperCase();
const isFreebieCoupon = Array.isArray(config.freebies_rules)
&& config.freebies_rules.some((rule) => {
return rule.freebie_coupon?.toUpperCase() === couponCode
&& validateDateRange(rule)
&& validateCustomerId(rule, params);
});

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required. Essa checagem só cobre data e cliente. Uma regra de brinde com freebie_coupon igual ao digitado mas com domain diferente, check_product_ids fora do carrinho ou min_subtotal acima do carrinho não gera freebie_product_ids e também não gera mensagem. Com kit ativo o checkout mostra "Cupom aplicado com sucesso" e grava o cupom, o mesmo sintoma que a PR corrige.

Sugestão: só considerar o cupom encontrado se a regra passou pelo mesmo filtro de getFreebiesPreview e de fato entrou em response.freebie_product_ids. Testei esta variante e ela fecha os três casos mantendo os 6 testes da PR verdes:

return rule.freebie_coupon?.toUpperCase() === couponCode
  && validateDateRange(rule)
  && validateCustomerId(rule, params)
  && mapCampaignProducts({ product_ids: rule.check_product_ids }, params).valid
  && Array.isArray(rule.product_ids) && rule.product_ids.length
  && matchFreebieRule(rule, params)
  && Array.isArray(response.freebie_product_ids)
  && rule.product_ids.every((id) => response.freebie_product_ids.includes(id));

Para min_subtotal vale devolver "Adicione mais R$ X" como no caso de min_amount.

Testes de regressão: os três cenários acima esperando invalid_coupon_message definido.

if (!isFreebieCoupon) {
// kit/freebie campaigns may still be applied, but not the typed coupon
response.invalid_coupon_message = params.lang === 'pt_br'
? 'O cupom de desconto inserido é inválido.'
: 'Discount coupon entered is invalid.';
}
}
// response with no error nor discount applied
return respondSuccess();
};
112 changes: 112 additions & 0 deletions packages/apps/discounts/tests/invalid-coupon.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import assert from 'node:assert';
import test, { describe } from 'node:test';
import applyDiscount from '../lib-mjs/apply-discount.mjs';

/*
Com uma campanha de kit ativa (ex.: leve 4 pague 3) a resposta sempre traz
`discount_rule`, e o DiscountApplier da loja trata qualquer desconto com cupom
digitado como "Cupom de desconto aplicado com sucesso." a menos que o app
devolva `invalid_coupon_message`. O cupom inválido ainda era gravado no
carrinho como se tivesse sido aceito.
*/
const categoryId = '68371790f3c4f8399c4948c2';
const items = [70, 70, 60, 80].map((price, i) => ({
product_id: `00000000000000000000000${i + 1}`,
sku: `SKU-${i + 1}`,
name: `Produto ${i + 1}`,
quantity: 1,
price,
categories: [{ _id: categoryId, name: 'Leve 4 pague 3' }],
}));

const getApplication = () => ({
data: {
product_kit_discounts: [{
label: 'leve4pague3',
category_ids: [categoryId],
min_quantity: 4,
discount_lowest_price: true,
discount: { apply_at: 'total' },
}],
freebies_rules: [{
label: 'brinde',
freebie_coupon: 'BRINDE',
product_ids: ['683748d6f3c4f8399c499b51'],
min_subtotal: 1,
}],
},
hidden_data: {
discount_rules: [{
discount_coupon: 'ANIVERSARIO',
case_insensitive: true,
discount: { apply_at: 'total', type: 'percentage', value: 10 },
}, {
discount_coupon: 'VENCIDO',
date_range: { end: '2020-01-01T00:00:00.000Z' },
discount: { apply_at: 'total', type: 'fixed', value: 15 },
}, {
discount_coupon: 'MINIMO500',
discount: {
apply_at: 'total',
type: 'fixed',
value: 30,
min_amount: 500,
},
}],
},
});

const run = (discountCoupon) => applyDiscount({
params: {
lang: 'pt_br',
currency_id: 'BRL',
amount: { subtotal: 280, total: 280, discount: 0 },
items,
discount_coupon: discountCoupon,
},
application: getApplication(),
});

describe('Cupom digitado com campanha de kit ativa', () => {
test('cupom inexistente mantém o kit e avisa que o cupom é inválido', async () => {
const response = await run('testealeatorio');
assert.deepStrictEqual(response.discount_rule.extra_discount, { value: 60, flags: ['KIT-1'] });
assert.strictEqual(response.invalid_coupon_message, 'O cupom de desconto inserido é inválido.');
});

test('cupom vencido é tratado como inválido', async () => {
const response = await run('VENCIDO');
assert.strictEqual(response.discount_rule.extra_discount.value, 60);
assert.strictEqual(response.invalid_coupon_message, 'O cupom de desconto inserido é inválido.');
});

test('cupom abaixo do valor mínimo informa quanto falta', async () => {
const response = await run('MINIMO500');
assert.strictEqual(response.discount_rule.extra_discount.value, 60);
assert.strictEqual(
response.invalid_coupon_message?.replace(/\s/g, ' '),
'Adicione mais R$ 220,00 ao carrinho para usar este cupom.',
);
});

test('cupom válido soma ao kit sem mensagem de erro', async () => {
const response = await run('aniversario');
assert.deepStrictEqual(response.discount_rule.extra_discount, {
value: 82,
flags: ['KIT-1', 'COUPON'],
});
assert.strictEqual(response.invalid_coupon_message, undefined);
});

test('cupom de brinde não é tratado como inválido', async () => {
const response = await run('brinde');
assert.deepStrictEqual(response.freebie_product_ids, ['683748d6f3c4f8399c499b51']);
assert.strictEqual(response.invalid_coupon_message, undefined);
});

test('sem cupom não há mensagem de cupom', async () => {
const response = await run(undefined);
assert.strictEqual(response.discount_rule.extra_discount.value, 60);
assert.strictEqual(response.invalid_coupon_message, undefined);
});
});
Loading