Files
qipai/backend/tests/marketing-benefit-service.test.mjs

251 lines
8.1 KiB
JavaScript

import assert from 'node:assert/strict';
import {
MarketingBenefitError,
MarketingBenefitService
} from '../dist/wallets/marketing-benefit-service.js';
const validFrom = new Date(Date.now() - 86_400_000);
const validTo = new Date(Date.now() + 86_400_000);
function createHarness(overrides = {}) {
const state = {
coupon: {
id: '401',
status: 'AVAILABLE',
validFrom,
validTo,
remainingUses: 1,
couponType: 'FULL_REDUCTION',
discountAmountCents: 3000,
timeMinutes: 0,
minOrderAmountCents: 10000,
storeId: '11',
roomCategoryId: null,
roomId: null,
weekdaysJson: null,
holidayOnly: 0,
...overrides.coupon
},
packageHolding: {
id: '501',
status: 'ACTIVE',
validFrom,
validTo,
remainingMinutes: 180,
remainingAmountCents: 6000,
storeId: '11',
roomCategoryId: null,
roomId: null,
weekdaysJson: null,
holidayOnly: 0,
...overrides.packageHolding
},
usages: [],
nextUsageId: 700
};
const connection = {
async beginTransaction() {},
async commit() {},
async rollback() {},
release() {},
async execute(sql, params) {
if (sql.includes('FROM qipai_benefit_usages') && sql.includes('client_request_id')) {
return [state.usages.filter((usage) =>
usage.tenantId === String(params[0])
&& usage.userId === String(params[1])
&& usage.clientRequestId === params[2]
).map(toUsageRow), []];
}
if (sql.includes('FROM qipai_coupon_grants')) {
if (state.coupon && String(params[2]) === state.coupon.id) {
return [[state.coupon], []];
}
return [[], []];
}
if (sql.includes('FROM qipai_package_holdings')) {
if (state.packageHolding && String(params[2]) === state.packageHolding.id) {
return [[state.packageHolding], []];
}
return [[], []];
}
if (sql.includes('INSERT INTO qipai_benefit_usages')) {
const usage = {
id: String(state.nextUsageId++),
tenantId: String(params[0]),
userId: String(params[1]),
orderId: String(params[2]),
benefitType: params[3],
benefitId: String(params[4]),
clientRequestId: params[5],
discountCents: Number(params[6]),
packageCreditCents: Number(params[7]),
minutes: Number(params[8]),
traceId: params[9],
status: 'FROZEN'
};
state.usages.push(usage);
return [{ insertId: usage.id, affectedRows: 1 }, []];
}
if (sql.includes("SET status = 'FROZEN'") && sql.includes('qipai_coupon_grants')) {
state.coupon.status = 'FROZEN';
state.coupon.frozenOrderId = String(params[0]);
return [{ affectedRows: 1 }, []];
}
if (sql.includes("SET status = 'FROZEN'") && sql.includes('qipai_package_holdings')) {
state.packageHolding.status = 'FROZEN';
state.packageHolding.frozenOrderId = String(params[0]);
return [{ affectedRows: 1 }, []];
}
if (sql.includes('FROM qipai_benefit_usages') && sql.includes('order_id')) {
return [state.usages.filter((usage) =>
usage.tenantId === String(params[0])
&& usage.userId === String(params[1])
&& usage.orderId === String(params[2])
).map(toUsageRow), []];
}
if (sql.includes('UPDATE qipai_coupon_grants') && sql.includes("used_count = used_count + 1")) {
state.coupon.remainingUses = Math.max(state.coupon.remainingUses - 1, 0);
state.coupon.status = state.coupon.remainingUses === 0 ? 'USED' : 'AVAILABLE';
state.coupon.frozenOrderId = null;
return [{ affectedRows: 1 }, []];
}
if (sql.includes('UPDATE qipai_package_holdings') && sql.includes('remaining_minutes')) {
const usage = state.usages.find((item) =>
item.benefitType === 'PACKAGE' && item.benefitId === state.packageHolding.id
);
state.packageHolding.remainingMinutes -= usage.minutes;
state.packageHolding.remainingAmountCents -= usage.packageCreditCents;
state.packageHolding.status =
state.packageHolding.remainingMinutes === 0
&& state.packageHolding.remainingAmountCents === 0
? 'EXHAUSTED'
: 'ACTIVE';
state.packageHolding.frozenOrderId = null;
return [{ affectedRows: 1 }, []];
}
if (sql.includes('UPDATE qipai_benefit_usages')) {
const usage = state.usages.find((item) =>
item.tenantId === String(params[1]) && item.id === String(params[2])
);
usage.status = params[0];
return [{ affectedRows: 1 }, []];
}
if (sql.includes("SET status = 'AVAILABLE'")) {
state.coupon.status = 'AVAILABLE';
state.coupon.frozenOrderId = null;
return [{ affectedRows: 1 }, []];
}
if (sql.includes("SET status = 'ACTIVE'")) {
state.packageHolding.status = 'ACTIVE';
state.packageHolding.frozenOrderId = null;
return [{ affectedRows: 1 }, []];
}
throw new Error(`Unexpected SQL: ${sql}`);
}
};
const pool = {
async getConnection() {
return connection;
}
};
return { service: new MarketingBenefitService(pool), state };
}
function toUsageRow(usage) {
return {
id: usage.id,
benefitType: usage.benefitType,
benefitId: usage.benefitId,
status: usage.status,
discountCents: usage.discountCents,
packageCreditCents: usage.packageCreditCents,
minutes: usage.minutes
};
}
const baseInput = {
tenantId: '7',
userId: '21',
orderId: '31',
storeId: '11',
orderAmountCents: 12000,
orderStartAt: new Date(Date.now() + 3_600_000),
couponGrantId: '401',
packageHoldingId: '501',
packageMinutes: 60,
packageCreditCents: 2000,
clientRequestId: 'benefit-001',
traceId: 'benefit-test'
};
{
const { service, state } = createHarness();
const reserved = await service.reserveForOrder(baseInput);
assert.equal(reserved.status, 'FROZEN');
assert.equal(reserved.discountCents, 3000);
assert.equal(reserved.packageCreditCents, 2000);
assert.equal(reserved.minutes, 60);
assert.equal(state.coupon.status, 'FROZEN');
assert.equal(state.packageHolding.status, 'FROZEN');
const duplicate = await service.reserveForOrder(baseInput);
assert.equal(duplicate.idempotent, true);
assert.equal(state.usages.length, 2);
const confirmed = await service.confirmReserved({
tenantId: '7',
userId: '21',
orderId: '31',
traceId: 'benefit-test-confirm'
});
assert.equal(confirmed.status, 'CONFIRMED');
assert.equal(state.coupon.status, 'USED');
assert.equal(state.packageHolding.status, 'ACTIVE');
assert.equal(state.packageHolding.remainingMinutes, 120);
assert.equal(state.packageHolding.remainingAmountCents, 4000);
const confirmAgain = await service.confirmReserved({
tenantId: '7',
userId: '21',
orderId: '31',
traceId: 'benefit-test-confirm'
});
assert.equal(confirmAgain.idempotent, true);
}
{
const { service } = createHarness({ coupon: { minOrderAmountCents: 20000 } });
await assert.rejects(
() => service.reserveForOrder(baseInput),
(error) => error instanceof MarketingBenefitError
&& error.code === 'BENEFIT_COUPON_MIN_AMOUNT_NOT_MET'
);
}
{
const { service } = createHarness({ packageHolding: { remainingMinutes: 30 } });
await assert.rejects(
() => service.reserveForOrder(baseInput),
(error) => error instanceof MarketingBenefitError
&& error.code === 'BENEFIT_PACKAGE_MINUTES_INSUFFICIENT'
);
}
{
const { service, state } = createHarness();
await service.reserveForOrder({ ...baseInput, clientRequestId: 'benefit-release' });
const released = await service.releaseReserved({
tenantId: '7',
userId: '21',
orderId: '31',
traceId: 'benefit-test-release'
});
assert.equal(released.status, 'RELEASED');
assert.equal(state.coupon.status, 'AVAILABLE');
assert.equal(state.packageHolding.status, 'ACTIVE');
assert.equal(state.packageHolding.remainingMinutes, 180);
}
console.log('PASS: M07-C benefits freeze, confirm, release and validate coupon/package usage.');