251 lines
8.1 KiB
JavaScript
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.');
|