初始化
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const os = require('node:os')
|
||||
const path = require('node:path')
|
||||
|
||||
const { createMockContext } = require('../src/context/createMockContext')
|
||||
|
||||
function withEnv(overrides, fn) {
|
||||
const previousValues = new Map()
|
||||
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
previousValues.set(key, process.env[key])
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return fn()
|
||||
} finally {
|
||||
for (const [key, value] of previousValues.entries()) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('uses a timestamp seed when MOCK_PROMOTION_REDEMPTION_ID_START is unset', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_PROMOTION_REDEMPTION_ID_START: undefined,
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
},
|
||||
() => {
|
||||
const originalDateNow = Date.now
|
||||
Date.now = () => 1760000000123
|
||||
|
||||
try {
|
||||
const context = createMockContext()
|
||||
assert.equal(context.nextMockPromotionRedemptionId(), 1760000000)
|
||||
assert.equal(context.nextMockPromotionRedemptionId(), 1760000001)
|
||||
} finally {
|
||||
Date.now = originalDateNow
|
||||
}
|
||||
},
|
||||
))
|
||||
|
||||
test('falls back to an int-safe timestamp seed when configured redemption id start is out of range', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_PROMOTION_REDEMPTION_ID_START: '1774243846923',
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
},
|
||||
() => {
|
||||
const originalDateNow = Date.now
|
||||
Date.now = () => 1760000000123
|
||||
|
||||
try {
|
||||
const context = createMockContext()
|
||||
assert.equal(context.nextMockPromotionRedemptionId(), 1760000000)
|
||||
} finally {
|
||||
Date.now = originalDateNow
|
||||
}
|
||||
},
|
||||
))
|
||||
|
||||
test('fallback redeemed promotion detail consumes the same incrementing redemption id sequence', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_PROMOTION_REDEMPTION_ID_START: '910000',
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
},
|
||||
() => {
|
||||
const context = createMockContext()
|
||||
|
||||
const firstOutcome = context.buildMockRedeemedPromotionOutcome({
|
||||
data: { loginId: 'tester' },
|
||||
schema: {},
|
||||
})
|
||||
const secondOutcome = context.buildMockRedeemedPromotionOutcome({
|
||||
data: { loginId: 'tester' },
|
||||
schema: {},
|
||||
})
|
||||
|
||||
assert.equal(firstOutcome.redemptionId, 910000)
|
||||
assert.equal(secondOutcome.redemptionId, 910001)
|
||||
},
|
||||
))
|
||||
|
||||
test('uses the birthday promotion fixture as the default promotion source when no override is configured', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_PROMOTION_CAPTURE_FILE: undefined,
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
},
|
||||
() => {
|
||||
const context = createMockContext()
|
||||
const promotions = context.getPromotionLists('910700001')
|
||||
|
||||
assert.equal(promotions.eligiblePromotionList.length, 1)
|
||||
assert.equal(promotions.eligiblePromotionList[0].code, 'PROMO-BIRTHDAY-2026')
|
||||
assert.equal(promotions.eligiblePromotionList[0].ruleId, 3800801)
|
||||
assert.equal(promotions.eligiblePromotionList[0].outcomeList.length, 3)
|
||||
assert.equal(promotions.eligiblePromotionList[0].isEntitled, true)
|
||||
},
|
||||
))
|
||||
|
||||
test('backdate promotions returned by the fixed fixture satisfy the frontend redeemable contract', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
},
|
||||
() => {
|
||||
const context = createMockContext()
|
||||
const promotions = context.getBackdatePromotionLists('800038008')
|
||||
|
||||
assert.ok(promotions.entitledPromotionList.length > 0)
|
||||
promotions.entitledPromotionList.forEach((item) => {
|
||||
assert.equal(item.isEntitled, true)
|
||||
assert.equal(item.isRedeemCompleted, false)
|
||||
assert.ok(Number(item.awardsEntitled) > Number(item.awardsCount))
|
||||
})
|
||||
},
|
||||
))
|
||||
|
||||
test('backdate promotions can be resolved through the default 80038008 member alias', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
},
|
||||
() => {
|
||||
const context = createMockContext()
|
||||
const promotions = context.getBackdatePromotionLists('80038008')
|
||||
|
||||
assert.ok(promotions.entitledPromotionList.length > 0)
|
||||
assert.equal(promotions.entitledPromotionList[0].ruleId, 3800801)
|
||||
assert.equal(promotions.entitledPromotionList[0].isEntitled, true)
|
||||
},
|
||||
))
|
||||
|
||||
test('persists runtime recorded redeemed promotions and resumes redemption id sequence', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mock-gateway-recording-'))
|
||||
const recordingFile = path.join(tempDir, 'redeemed-promotions.recorded.json')
|
||||
|
||||
try {
|
||||
withEnv(
|
||||
{
|
||||
MOCK_PROMOTION_REDEMPTION_ID_START: '910000',
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'true',
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_FILE: recordingFile,
|
||||
},
|
||||
() => {
|
||||
const firstContext = createMockContext()
|
||||
firstContext.recordRedeemedPromotionOutcome('80010991', {
|
||||
redemptionId: 910005,
|
||||
promotionId: 11225,
|
||||
outcome: {
|
||||
redeemedList: [
|
||||
{
|
||||
outcomeRemark: 'qa-ebonus-auto',
|
||||
transId: 'tx-910005',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const persisted = JSON.parse(fs.readFileSync(recordingFile, 'utf8'))
|
||||
assert.equal(persisted.accounts['80010991'][0].redemptionId, 910005)
|
||||
|
||||
const secondContext = createMockContext()
|
||||
assert.equal(secondContext.getRecordedRedeemedPromotionOutcomes('80010991')[0].redemptionId, 910005)
|
||||
assert.equal(secondContext.nextMockPromotionRedemptionId(), 910006)
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('account-specific redeemed promotions fixture preserves voidedByEmployeeId for the 2026-04-06 voided record', () => {
|
||||
const context = createMockContext()
|
||||
const fixture = context.getAccountSpecificRedeemedPromotionsFixture('100000059')
|
||||
|
||||
const targetOutcome = fixture.data.outcomeList.find(
|
||||
(item) =>
|
||||
item.voidedDtm === '2026-04-06T16:11:05.584548' &&
|
||||
item.outcome?.redeemedList?.[0]?.transId === '43a9ab81-bbff-4812-bd64-bfd4eaa50d72',
|
||||
)
|
||||
|
||||
assert.ok(targetOutcome)
|
||||
assert.equal(targetOutcome.voidedBy, 'qa-randyng')
|
||||
assert.equal(targetOutcome.voidedByEmployeeId, '171853')
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const { createMockContext } = require('../src/context/createMockContext')
|
||||
const { buildCheckPrizeStockResponseXml } = require('../src/routes/gis/xmlResponse')
|
||||
|
||||
test('mock gis stock response marks default test unique codes as in stock', () => {
|
||||
const context = createMockContext()
|
||||
|
||||
const response = context.getMockGisCheckPrizeStockResponse({
|
||||
propertyId: 'GLP',
|
||||
locationId: 'LOC013',
|
||||
prizeIdentifier: '88888888',
|
||||
prizeCode: 'mqa-gisprizec',
|
||||
prizeName: 'MQA-GISPrizeC',
|
||||
prizeCategory: 'Entertainment',
|
||||
})
|
||||
|
||||
assert.equal(response.uniqueCode, '88888888')
|
||||
assert.equal(response.quantity, 1)
|
||||
assert.equal(response.errorCode, '')
|
||||
assert.equal(response.errorMsg, '')
|
||||
})
|
||||
|
||||
test('mock gis stock response returns NotFound for unknown unique codes', () => {
|
||||
const context = createMockContext()
|
||||
|
||||
const response = context.getMockGisCheckPrizeStockResponse({
|
||||
prizeIdentifier: 'unknown-code',
|
||||
prizeCode: 'mqa-gisprizec',
|
||||
})
|
||||
|
||||
assert.equal(response.quantity, 0)
|
||||
assert.equal(response.errorCode, 'NotFound')
|
||||
assert.equal(response.errorMsg, 'Not found matching prize')
|
||||
})
|
||||
|
||||
test('check prize stock xml builder emits backend-compatible root and fields', () => {
|
||||
const xml = buildCheckPrizeStockResponseXml([
|
||||
{
|
||||
propertyId: 'GLP',
|
||||
locationId: 'LOC013',
|
||||
prizeCode: 'mqa-gisprizec',
|
||||
prizeName: 'MQA-GISPrizeC',
|
||||
prizeCategory: 'Entertainment',
|
||||
uniqueCode: '12341234',
|
||||
quantity: 1,
|
||||
errorCode: '',
|
||||
errorMsg: '',
|
||||
},
|
||||
])
|
||||
|
||||
assert.match(xml, /<ArrayOfCheckPrizeStock_response_VM>/)
|
||||
assert.match(xml, /<CheckPrizeStock_response_VM>/)
|
||||
assert.match(xml, /<uniqueCode>12341234<\/uniqueCode>/)
|
||||
assert.match(xml, /<quantity>1<\/quantity>/)
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const {
|
||||
resolveCardNumForLookup,
|
||||
resolvePlayerIdForLookup,
|
||||
TASK_15171_ACTIVE_CARD,
|
||||
TASK_15171_INACTIVE_CARD,
|
||||
TASK_15171_PLAYER_ID,
|
||||
} = require('../src/routes/ewl/edip/memberProfileCardNum')
|
||||
|
||||
test('task 15171 returns only active card for playerId lookup', () => {
|
||||
assert.deepEqual(
|
||||
resolveCardNumForLookup({
|
||||
accountNum: '',
|
||||
playerId: TASK_15171_PLAYER_ID,
|
||||
enabled: true,
|
||||
}),
|
||||
[TASK_15171_ACTIVE_CARD],
|
||||
)
|
||||
})
|
||||
|
||||
test('task 15171 returns only active card when inactive card is queried', () => {
|
||||
assert.deepEqual(
|
||||
resolveCardNumForLookup({
|
||||
accountNum: TASK_15171_INACTIVE_CARD,
|
||||
playerId: TASK_15171_PLAYER_ID,
|
||||
enabled: true,
|
||||
}),
|
||||
[TASK_15171_ACTIVE_CARD],
|
||||
)
|
||||
assert.ok(
|
||||
!resolveCardNumForLookup({
|
||||
accountNum: TASK_15171_INACTIVE_CARD,
|
||||
playerId: TASK_15171_PLAYER_ID,
|
||||
enabled: true,
|
||||
}).includes(TASK_15171_INACTIVE_CARD),
|
||||
)
|
||||
})
|
||||
|
||||
test('task 15171 resolves playerId from active or inactive card number', () => {
|
||||
assert.equal(
|
||||
resolvePlayerIdForLookup({
|
||||
accountNum: TASK_15171_INACTIVE_CARD,
|
||||
playerId: '',
|
||||
}),
|
||||
TASK_15171_PLAYER_ID,
|
||||
)
|
||||
assert.equal(
|
||||
resolvePlayerIdForLookup({
|
||||
accountNum: TASK_15171_ACTIVE_CARD,
|
||||
playerId: '',
|
||||
}),
|
||||
TASK_15171_PLAYER_ID,
|
||||
)
|
||||
})
|
||||
|
||||
test('default card_num echoes queried account when task 15171 disabled', () => {
|
||||
assert.deepEqual(
|
||||
resolveCardNumForLookup({
|
||||
accountNum: '80000001',
|
||||
playerId: '80000001',
|
||||
enabled: false,
|
||||
}),
|
||||
['80000001'],
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const express = require('express')
|
||||
|
||||
const { createMockContext } = require('../src/context/createMockContext')
|
||||
const registerMemberProfileRoute = require('../src/routes/ewl/edip/memberProfile')
|
||||
|
||||
function withEnv(overrides, fn) {
|
||||
const previousValues = new Map()
|
||||
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
previousValues.set(key, process.env[key])
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return fn()
|
||||
} finally {
|
||||
for (const [key, value] of previousValues.entries()) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startMemberProfileApp(envOverrides = {}) {
|
||||
return withEnv(envOverrides, async () => {
|
||||
const context = createMockContext()
|
||||
const app = express()
|
||||
app.use(express.json({ limit: '2mb' }))
|
||||
registerMemberProfileRoute({ app, context })
|
||||
|
||||
const server = await new Promise((resolve) => {
|
||||
const instance = app.listen(0, '127.0.0.1', () => resolve(instance))
|
||||
})
|
||||
|
||||
const address = server.address()
|
||||
return {
|
||||
baseUrl: `http://${address.address}:${address.port}`,
|
||||
close: async () =>
|
||||
new Promise((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('member profile route returns task 15171 active card_num for playerId 600000154', async () => {
|
||||
const app = await startMemberProfileApp({
|
||||
MOCK_TASK_15171_ENABLED: 'true',
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(`${app.baseUrl}/ewl/edip/v1/member/profile`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ schema: {}, data: { playerId: '600000154' } }),
|
||||
})
|
||||
const body = await response.json()
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.equal(body.status, true)
|
||||
assert.equal(body.data.playerId, '600000154')
|
||||
assert.deepEqual(body.data.card_num, ['2600000154002'])
|
||||
assert.ok(!body.data.card_num.includes('2600000154001'))
|
||||
} finally {
|
||||
await app.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('member profile route returns active card_num when inactive card is queried', async () => {
|
||||
const app = await startMemberProfileApp({
|
||||
MOCK_TASK_15171_ENABLED: 'true',
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(`${app.baseUrl}/ewl/edip/v1/member/profile`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ schema: {}, data: { accountNum: '2600000154001' } }),
|
||||
})
|
||||
const body = await response.json()
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.equal(body.data.playerId, '600000154')
|
||||
assert.deepEqual(body.data.card_num, ['2600000154002'])
|
||||
} finally {
|
||||
await app.close()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,107 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const express = require('express')
|
||||
|
||||
const { createMockContext } = require('../src/context/createMockContext')
|
||||
const registerMemberProfileRoute = require('../src/routes/ewl/edip/memberProfile')
|
||||
|
||||
function withEnv(overrides, fn) {
|
||||
const previousValues = new Map()
|
||||
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
previousValues.set(key, process.env[key])
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return fn()
|
||||
} finally {
|
||||
for (const [key, value] of previousValues.entries()) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startMemberProfileApp(envOverrides = {}) {
|
||||
return withEnv(envOverrides, async () => {
|
||||
const context = createMockContext()
|
||||
const app = express()
|
||||
app.use(express.json({ limit: '2mb' }))
|
||||
registerMemberProfileRoute({ app, context })
|
||||
|
||||
const server = await new Promise((resolve) => {
|
||||
const instance = app.listen(0, '127.0.0.1', () => resolve(instance))
|
||||
})
|
||||
|
||||
const address = server.address()
|
||||
return {
|
||||
baseUrl: `http://${address.address}:${address.port}`,
|
||||
close: async () =>
|
||||
new Promise((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('member profile route returns forced 503 when configured', async () => {
|
||||
const app = await startMemberProfileApp({
|
||||
MOCK_MEMBER_PROFILE_FORCE_STATUS_CODE: '503',
|
||||
MOCK_MEMBER_PROFILE_FORCE_ERROR_CODE: 'MOCK_503',
|
||||
MOCK_MEMBER_PROFILE_FORCE_ERROR_MSG: 'Mock member profile unavailable',
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(`${app.baseUrl}/ewl/edip/v1/member/profile`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ schema: {}, data: { accountNum: '80000001' } }),
|
||||
})
|
||||
const body = await response.json()
|
||||
|
||||
assert.equal(response.status, 503)
|
||||
assert.equal(body.errorCode, 'MOCK_503')
|
||||
assert.equal(body.message, 'Mock member profile unavailable')
|
||||
} finally {
|
||||
await app.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('member profile route delays response when configured', async () => {
|
||||
const app = await startMemberProfileApp({
|
||||
MOCK_MEMBER_PROFILE_DELAY_MS: '120',
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
})
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
const response = await fetch(`${app.baseUrl}/ewl/edip/v1/member/profile`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ schema: {}, data: { accountNum: '80000001' } }),
|
||||
})
|
||||
const elapsedMs = Date.now() - startedAt
|
||||
const body = await response.json()
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.equal(body.status, true)
|
||||
assert.ok(elapsedMs >= 100, `expected delayed response, got ${elapsedMs}ms`)
|
||||
} finally {
|
||||
await app.close()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,242 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const { createMockContext } = require('../src/context/createMockContext')
|
||||
const registerPlayerPromotionFulfillmentJourneyRoute = require('../src/routes/ewl/bal/playerPromotionFulfillmentJourney')
|
||||
const registerPlayerPromotionFulfillmentJourneyDetailRoute = require('../src/routes/ewl/bal/playerPromotionFulfillmentJourneyDetail')
|
||||
|
||||
const { buildFulfillmentJourneyResponse } =
|
||||
registerPlayerPromotionFulfillmentJourneyRoute.__internal
|
||||
const { buildFulfillmentJourneyDetailResponse } =
|
||||
registerPlayerPromotionFulfillmentJourneyDetailRoute.__internal
|
||||
|
||||
function withEnv(overrides, fn) {
|
||||
const previousValues = new Map()
|
||||
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
previousValues.set(key, process.env[key])
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return fn()
|
||||
} finally {
|
||||
for (const [key, value] of previousValues.entries()) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createRegisteredPostRoute(registerRoute, context) {
|
||||
const registrations = []
|
||||
const app = {
|
||||
post(paths, handler) {
|
||||
registrations.push({ paths, handler })
|
||||
},
|
||||
}
|
||||
|
||||
registerRoute({ app, context })
|
||||
assert.equal(registrations.length, 1)
|
||||
return registrations[0]
|
||||
}
|
||||
|
||||
function invokeHandler(handler, body) {
|
||||
const response = {
|
||||
statusCode: 0,
|
||||
payload: null,
|
||||
}
|
||||
const res = {
|
||||
status(code) {
|
||||
response.statusCode = code
|
||||
return this
|
||||
},
|
||||
json(payload) {
|
||||
response.payload = payload
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
handler({ body }, res)
|
||||
return response
|
||||
}
|
||||
|
||||
test('summary 默认返回 accountNum/ruleId,并包含 Bally 风格字段', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
},
|
||||
() => {
|
||||
const context = createMockContext()
|
||||
const response = buildFulfillmentJourneyResponse({
|
||||
context,
|
||||
requestBody: {
|
||||
data: {
|
||||
accountNum: '80010991',
|
||||
ruleId: 71768024,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(response.errorCode, '')
|
||||
assert.equal(response.errorMsg, '')
|
||||
assert.equal(response.accountNum, '80010991')
|
||||
assert.equal(response.ruleId, 71768024)
|
||||
assert.ok(Array.isArray(response.eligibleCriteriaList))
|
||||
assert.ok(Array.isArray(response.journeyList))
|
||||
assert.ok(response.eligibleCriteriaList.length > 0)
|
||||
assert.ok(response.journeyList.length > 0)
|
||||
assert.deepEqual(Object.keys(response.eligibleCriteriaList[0]).sort(), [
|
||||
'attribute',
|
||||
'operator',
|
||||
'qualify',
|
||||
'value',
|
||||
'valueArray',
|
||||
])
|
||||
assert.deepEqual(Object.keys(response.journeyList[0]).sort(), [
|
||||
'basePts',
|
||||
'memberNo',
|
||||
'periodEndTime',
|
||||
'periodStartTime',
|
||||
'progressId',
|
||||
'ruleId',
|
||||
'ruleName',
|
||||
])
|
||||
},
|
||||
))
|
||||
|
||||
test('summary empty-result 开关打开时返回空数组', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_EMPTY_RESULT: 'true',
|
||||
},
|
||||
() => {
|
||||
const context = createMockContext()
|
||||
const response = buildFulfillmentJourneyResponse({
|
||||
context,
|
||||
requestBody: {
|
||||
data: {
|
||||
accountNum: '80010991',
|
||||
ruleId: 71768024,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.deepEqual(response.eligibleCriteriaList, [])
|
||||
assert.deepEqual(response.journeyList, [])
|
||||
},
|
||||
))
|
||||
|
||||
test('detail 业务错误时保留 errorCode/errorMsg/accountNum/ruleId', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_DETAIL_ERROR_CODE: 'BIZ_1001',
|
||||
MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_DETAIL_ERROR_MSG: 'rule is not active',
|
||||
},
|
||||
() => {
|
||||
const context = createMockContext()
|
||||
const response = buildFulfillmentJourneyDetailResponse({
|
||||
context,
|
||||
requestBody: {
|
||||
data: {
|
||||
accountNum: '80010991',
|
||||
ruleId: 71768024,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(response.errorCode, 'BIZ_1001')
|
||||
assert.equal(response.errorMsg, 'rule is not active')
|
||||
assert.equal(response.accountNum, '80010991')
|
||||
assert.equal(response.ruleId, 71768024)
|
||||
assert.ok(Array.isArray(response.journeyList))
|
||||
assert.deepEqual(Object.keys(response.journeyList[0]).sort(), [
|
||||
'areaCode',
|
||||
'ballyTranId',
|
||||
'basePts',
|
||||
'casinoCode',
|
||||
'deptCode',
|
||||
'earnType',
|
||||
'gameCode',
|
||||
'gamingDt',
|
||||
'locnCode',
|
||||
'memberNo',
|
||||
'periodEndTime',
|
||||
'periodStartTime',
|
||||
'postDtm',
|
||||
'progressId',
|
||||
'ruleId',
|
||||
'ruleName',
|
||||
])
|
||||
},
|
||||
))
|
||||
|
||||
test('summary route 会注册正确路径并返回 200 JSON', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
},
|
||||
() => {
|
||||
const context = createMockContext()
|
||||
const registration = createRegisteredPostRoute(
|
||||
registerPlayerPromotionFulfillmentJourneyRoute,
|
||||
context,
|
||||
)
|
||||
|
||||
assert.deepEqual(registration.paths, [
|
||||
'/ewl/promotion/v1/playerpromotion/fulfillmentJourney',
|
||||
'/ewl/bal/v1/playerpromotion/fulfillmentJourney',
|
||||
])
|
||||
|
||||
const response = invokeHandler(registration.handler, {
|
||||
data: {
|
||||
accountNum: '80010991',
|
||||
ruleId: 71768024,
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(response.statusCode, 200)
|
||||
assert.equal(response.payload.accountNum, '80010991')
|
||||
assert.equal(response.payload.ruleId, 71768024)
|
||||
},
|
||||
))
|
||||
|
||||
test('detail route 会注册正确路径并返回 200 JSON', () =>
|
||||
withEnv(
|
||||
{
|
||||
MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED: 'false',
|
||||
MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_DETAIL_ERROR_CODE: 'BIZ_1001',
|
||||
},
|
||||
() => {
|
||||
const context = createMockContext()
|
||||
const registration = createRegisteredPostRoute(
|
||||
registerPlayerPromotionFulfillmentJourneyDetailRoute,
|
||||
context,
|
||||
)
|
||||
|
||||
assert.deepEqual(registration.paths, [
|
||||
'/ewl/promotion/v1/playerpromotion/fulfillmentJourneyDetail',
|
||||
'/ewl/bal/v1/playerpromotion/fulfillmentJourneyDetail',
|
||||
])
|
||||
|
||||
const response = invokeHandler(registration.handler, {
|
||||
data: {
|
||||
accountNum: '80010991',
|
||||
ruleId: 71768024,
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(response.statusCode, 200)
|
||||
assert.equal(response.payload.errorCode, 'BIZ_1001')
|
||||
assert.equal(response.payload.accountNum, '80010991')
|
||||
},
|
||||
))
|
||||
@@ -0,0 +1,463 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const registerPlayerPromotionRedeemRoute = require('../src/routes/ewl/bal/playerPromotionRedeem')
|
||||
|
||||
const {
|
||||
ISSUE_TARGET_TO_WALLET_HEADER,
|
||||
bridgePromotionRedeemToEwallet,
|
||||
createMockPromotionRedeemResult,
|
||||
isBackdatePromotionRedeemRequest,
|
||||
registerableRoutes,
|
||||
mergeIssueTargetToWalletOverrides,
|
||||
mergeRuntimeIssueTargetToWalletOverrides,
|
||||
resolveIssueTargetToWallet,
|
||||
} = registerPlayerPromotionRedeemRoute.__internal
|
||||
|
||||
test('registers both normal and backdate promotion redeem routes', () => {
|
||||
assert.deepEqual(registerableRoutes, [
|
||||
'/ewl/promotion/v1/playerpromotion/redeem',
|
||||
'/ewl/promotion/v1/playerpromotion/redeemBackDate',
|
||||
'/ewl/bal/v1/playerpromotion/redeem',
|
||||
'/ewl/bal/v1/playerpromotion/redeemBackDate',
|
||||
])
|
||||
})
|
||||
|
||||
test('detects backdate redeem requests from route and payload', () => {
|
||||
assert.equal(
|
||||
isBackdatePromotionRedeemRequest('/ewl/promotion/v1/playerpromotion/redeemBackDate', {
|
||||
data: {},
|
||||
}),
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
isBackdatePromotionRedeemRequest('/ewl/promotion/v1/playerpromotion/redeem', {
|
||||
data: { backdate: '2026-03-23' },
|
||||
}),
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
isBackdatePromotionRedeemRequest('/ewl/promotion/v1/playerpromotion/redeem', {
|
||||
data: {},
|
||||
}),
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('creates a local redeemed outcome for backdate requests without bridging back to backend', () => {
|
||||
const mockResult = createMockPromotionRedeemResult({
|
||||
context: {
|
||||
resolvePromotionRedeemSelection(accountNum, promotionId, promotionRuleId, outcomeIds, options) {
|
||||
assert.equal(accountNum, '800038008')
|
||||
assert.equal(promotionId, 71768004)
|
||||
assert.equal(promotionRuleId, 71768026)
|
||||
assert.deepEqual(outcomeIds, [71768072, 71768073])
|
||||
assert.deepEqual(options, { backdate: true })
|
||||
return {
|
||||
promotion: {
|
||||
code: 'LP-EE-CNYGS26',
|
||||
name: 'CNY Gift Set Earn & Get-2026',
|
||||
ruleName: 'CNYGS-188pts-Martell + LCH-Can Abalone',
|
||||
},
|
||||
selectedOutcomes: [
|
||||
{
|
||||
value: 'lp-ee-cnygs26-188pts-martellnoblige-me',
|
||||
prize: {
|
||||
prizeCode: 'lp-ee-cnygs26-188pts-martellnoblige-me',
|
||||
prizeCategory: 'PROMOTION',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'lp-ee-cnygs26-188pts-lchabalone-me',
|
||||
prize: {
|
||||
prizeCode: 'lp-ee-cnygs26-188pts-lchabalone-me',
|
||||
prizeCategory: 'PROMOTION',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
nextMockPromotionRedemptionId() {
|
||||
return 900001
|
||||
},
|
||||
generateToken(prefix) {
|
||||
return `${prefix}_001`
|
||||
},
|
||||
},
|
||||
requestBody: {
|
||||
schema: {},
|
||||
data: {
|
||||
accountNum: '800038008',
|
||||
promotionId: 71768004,
|
||||
promotionRuleId: 71768026,
|
||||
outcomeId: [71768072, 71768073],
|
||||
quantity: 1,
|
||||
backdate: '2026-03-23',
|
||||
},
|
||||
},
|
||||
accountNum: '800038008',
|
||||
backdate: true,
|
||||
})
|
||||
|
||||
assert.equal(mockResult.redeemedOutcome.redemptionId, 900001)
|
||||
assert.equal(mockResult.redeemedOutcome.promotionId, 71768004)
|
||||
assert.equal(mockResult.redeemedOutcome.prizeRuleId, 71768026)
|
||||
assert.equal(mockResult.redeemedOutcome.outcome.redeemedList.length, 2)
|
||||
assert.equal(
|
||||
mockResult.redeemedOutcome.outcome.redeemedList[0].transId,
|
||||
'mock_backdate_redemption_001',
|
||||
)
|
||||
})
|
||||
|
||||
test('bridges backdate requests to backend when promotion bridge is enabled', async (t) => {
|
||||
const originalFetch = global.fetch
|
||||
t.after(() => {
|
||||
global.fetch = originalFetch
|
||||
})
|
||||
|
||||
const fetchCalls = []
|
||||
global.fetch = async (url, options = {}) => {
|
||||
const parsedBody = options.body ? JSON.parse(options.body) : null
|
||||
fetchCalls.push({
|
||||
url,
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
body: parsedBody,
|
||||
})
|
||||
|
||||
if (url === 'http://backend.test/api/v1/auth/login') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
text: async () => JSON.stringify({ success: true, data: { accessToken: 'token-123' } }),
|
||||
}
|
||||
}
|
||||
|
||||
if (url === 'http://backend.test/api/v1/prize-redemptions') {
|
||||
assert.equal(parsedBody.customerId, '80038008')
|
||||
assert.equal(parsedBody.metadata.redemption_id, 900002)
|
||||
assert.equal(parsedBody.metadata.promotion_id, 71768004)
|
||||
assert.equal(parsedBody.targetPrizes.length, 2)
|
||||
assert.deepEqual(
|
||||
parsedBody.targetPrizes.map((targetPrize) => targetPrize.issueTargetToWallet),
|
||||
[true, false],
|
||||
)
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
text: async () =>
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
data: {
|
||||
publicId: 'backend-redemption-001',
|
||||
overallStatus: 'COMPLETED',
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${url}`)
|
||||
}
|
||||
|
||||
const bridgeResult = await bridgePromotionRedeemToEwallet({
|
||||
context: {
|
||||
resolvePromotionRedeemSelection(accountNum, promotionId, promotionRuleId, outcomeIds, options) {
|
||||
assert.equal(accountNum, '80038008')
|
||||
assert.equal(promotionId, 71768004)
|
||||
assert.equal(promotionRuleId, 71768024)
|
||||
assert.deepEqual(outcomeIds, [71768070, 71768069])
|
||||
assert.deepEqual(options, { backdate: true })
|
||||
return {
|
||||
promotion: {
|
||||
code: 'LP-EE-CNYGS26',
|
||||
name: 'CNY Gift Set Earn & Get-2026',
|
||||
ruleName: 'CNYGS-188pts-Hennessy + LCH-Scallop',
|
||||
},
|
||||
selectedOutcomes: [
|
||||
{
|
||||
key: 71768070,
|
||||
value: 'lp-ee-cnygs26-188pts-lchscallop-me',
|
||||
prize: {
|
||||
prizeCode: 'lp-ee-cnygs26-188pts-lchscallop-me',
|
||||
prizeCategory: 'PROMOTION',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 71768069,
|
||||
value: 'lp-ee-cnygs26-188pts-hennessyvsop-me',
|
||||
prize: {
|
||||
prizeCode: 'lp-ee-cnygs26-188pts-hennessyvsop-me',
|
||||
prizeCategory: 'PROMOTION',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
nextMockPromotionRedemptionId() {
|
||||
return 900002
|
||||
},
|
||||
promotionRedemptionBridgeLoginUsername: 'mock.gateway',
|
||||
promotionRedemptionBridgeLoginPassword: 'secret',
|
||||
promotionRedemptionBridgeBaseUrl: 'http://backend.test',
|
||||
promotionRedemptionBridgeTimeoutMs: 5000,
|
||||
promotionRedemptionSourceBucket: 'POINT',
|
||||
promotionRedemptionInitiateSource: 'PROMOTION',
|
||||
generateToken(prefix) {
|
||||
return `${prefix}_001`
|
||||
},
|
||||
},
|
||||
requestBody: {
|
||||
schema: {
|
||||
computerName: 'BOH-DT200244',
|
||||
ipAddress: '10.20.30.40',
|
||||
transactionId: 'mock-backdate-request-001',
|
||||
},
|
||||
data: {
|
||||
accountNum: '80038008',
|
||||
promotionId: 71768004,
|
||||
promotionRuleId: 71768024,
|
||||
outcomeId: [71768070, 71768069],
|
||||
issueTargetToWallet: [true, false],
|
||||
quantity: 1,
|
||||
loginId: 'admin',
|
||||
casinoCode: 'GLP',
|
||||
locationCode: 'GLP01',
|
||||
backdate: '2026-04-21',
|
||||
},
|
||||
},
|
||||
accountNum: '80038008',
|
||||
backdate: true,
|
||||
})
|
||||
|
||||
assert.equal(bridgeResult.redeemedOutcome.redemptionId, 900002)
|
||||
assert.equal(bridgeResult.redeemedOutcome.outcome.redeemedList.length, 2)
|
||||
assert.equal(
|
||||
bridgeResult.redeemedOutcome.outcome.redeemedList[0].transId,
|
||||
'backend-redemption-001',
|
||||
)
|
||||
assert.equal(fetchCalls.length, 2)
|
||||
assert.equal(fetchCalls[0].url, 'http://backend.test/api/v1/auth/login')
|
||||
assert.equal(fetchCalls[1].url, 'http://backend.test/api/v1/prize-redemptions')
|
||||
})
|
||||
|
||||
test('header overrides are merged into request data for backend passthrough', () => {
|
||||
const normalizedRequestBody = mergeIssueTargetToWalletOverrides(
|
||||
{
|
||||
data: {
|
||||
outcomeId: [11330011, 11330012],
|
||||
},
|
||||
},
|
||||
{
|
||||
[ISSUE_TARGET_TO_WALLET_HEADER]: '{"11330011":false,"11330012":true}',
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(normalizedRequestBody.data.issueTargetToWalletByOutcomeId, {
|
||||
'11330011': false,
|
||||
'11330012': true,
|
||||
})
|
||||
})
|
||||
|
||||
test('body overrides win over header overrides when both are present', () => {
|
||||
const normalizedRequestBody = mergeIssueTargetToWalletOverrides(
|
||||
{
|
||||
data: {
|
||||
issueTargetToWalletByOutcomeId: {
|
||||
'11330011': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[ISSUE_TARGET_TO_WALLET_HEADER]: '{"11330011":false,"11330012":true}',
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(normalizedRequestBody.data.issueTargetToWalletByOutcomeId, {
|
||||
'11330011': true,
|
||||
'11330012': true,
|
||||
})
|
||||
})
|
||||
|
||||
test('runtime overrides are merged for matching promotion outcomes', () => {
|
||||
const normalizedRequestBody = mergeRuntimeIssueTargetToWalletOverrides(
|
||||
{
|
||||
data: {
|
||||
promotionId: 11330,
|
||||
promotionRuleId: 1133001,
|
||||
outcomeId: [11330011, 11330012],
|
||||
},
|
||||
},
|
||||
{
|
||||
resolvePromotionRedeemFulfillmentOverrideByOutcomeId(input) {
|
||||
assert.deepEqual(input, {
|
||||
accountNum: '80010991',
|
||||
promotionId: 11330,
|
||||
promotionRuleId: 1133001,
|
||||
outcomeIds: [11330011, 11330012],
|
||||
})
|
||||
|
||||
return {
|
||||
'11330011': false,
|
||||
'11330012': true,
|
||||
}
|
||||
},
|
||||
},
|
||||
'80010991',
|
||||
)
|
||||
|
||||
assert.deepEqual(normalizedRequestBody.data.issueTargetToWalletByOutcomeId, {
|
||||
'11330011': false,
|
||||
'11330012': true,
|
||||
})
|
||||
})
|
||||
|
||||
test('request overrides still win over runtime overrides', () => {
|
||||
const normalizedRequestBody = mergeRuntimeIssueTargetToWalletOverrides(
|
||||
{
|
||||
data: {
|
||||
promotionId: 11330,
|
||||
promotionRuleId: 1133001,
|
||||
outcomeId: [11330011, 11330012],
|
||||
issueTargetToWalletByOutcomeId: {
|
||||
'11330011': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
resolvePromotionRedeemFulfillmentOverrideByOutcomeId() {
|
||||
return {
|
||||
'11330011': false,
|
||||
'11330012': false,
|
||||
}
|
||||
},
|
||||
},
|
||||
'80010991',
|
||||
)
|
||||
|
||||
assert.deepEqual(normalizedRequestBody.data.issueTargetToWalletByOutcomeId, {
|
||||
'11330011': true,
|
||||
'11330012': false,
|
||||
})
|
||||
})
|
||||
|
||||
test('defaults USER_CHOICE promotions to wallet when no explicit override is provided', () => {
|
||||
const resolved = resolveIssueTargetToWallet({
|
||||
requestData: {},
|
||||
outcome: {
|
||||
key: 11330011,
|
||||
prize: {
|
||||
deliveryMode: 'USER_CHOICE',
|
||||
},
|
||||
},
|
||||
index: 0,
|
||||
prizeIdentifier: null,
|
||||
})
|
||||
|
||||
assert.equal(resolved, true)
|
||||
})
|
||||
|
||||
test('uses non-empty gisPrizeIdentifier to resolve USER_CHOICE promotions as external', () => {
|
||||
const resolved = resolveIssueTargetToWallet({
|
||||
requestData: {},
|
||||
outcome: {
|
||||
key: 11330011,
|
||||
prize: {
|
||||
deliveryMode: 'USER_CHOICE',
|
||||
},
|
||||
},
|
||||
index: 0,
|
||||
prizeIdentifier: 'GIS-CODE-001',
|
||||
})
|
||||
|
||||
assert.equal(resolved, false)
|
||||
})
|
||||
|
||||
test('request outcome override takes precedence over delivery mode inference', () => {
|
||||
const resolved = resolveIssueTargetToWallet({
|
||||
requestData: {
|
||||
issueTargetToWalletByOutcomeId: {
|
||||
'11330011': false,
|
||||
},
|
||||
},
|
||||
outcome: {
|
||||
key: 11330011,
|
||||
prize: {
|
||||
deliveryMode: 'USER_CHOICE',
|
||||
},
|
||||
},
|
||||
index: 0,
|
||||
prizeIdentifier: null,
|
||||
})
|
||||
|
||||
assert.equal(resolved, false)
|
||||
})
|
||||
|
||||
test('fixture-level mockIssueTargetToWallet override is honored', () => {
|
||||
const resolved = resolveIssueTargetToWallet({
|
||||
requestData: {},
|
||||
outcome: {
|
||||
key: 11330011,
|
||||
mockIssueTargetToWallet: false,
|
||||
prize: {
|
||||
deliveryMode: 'USER_CHOICE',
|
||||
},
|
||||
},
|
||||
index: 0,
|
||||
prizeIdentifier: null,
|
||||
})
|
||||
|
||||
assert.equal(resolved, false)
|
||||
})
|
||||
|
||||
test('request array override is honored by outcome index', () => {
|
||||
const resolved = resolveIssueTargetToWallet({
|
||||
requestData: {
|
||||
issueTargetToWallet: [true, false],
|
||||
},
|
||||
outcome: {
|
||||
key: 11330012,
|
||||
prize: {
|
||||
deliveryMode: 'WALLET_ONLY',
|
||||
},
|
||||
},
|
||||
index: 1,
|
||||
prizeIdentifier: null,
|
||||
})
|
||||
|
||||
assert.equal(resolved, false)
|
||||
})
|
||||
|
||||
test('request array override can distinguish duplicate outcome ids by position', () => {
|
||||
const firstResolved = resolveIssueTargetToWallet({
|
||||
requestData: {
|
||||
issueTargetToWallet: [true, false, true],
|
||||
},
|
||||
outcome: {
|
||||
key: 11330011,
|
||||
prize: {
|
||||
deliveryMode: 'USER_CHOICE',
|
||||
},
|
||||
},
|
||||
index: 0,
|
||||
prizeIdentifier: null,
|
||||
})
|
||||
|
||||
const secondResolved = resolveIssueTargetToWallet({
|
||||
requestData: {
|
||||
issueTargetToWallet: [true, false, true],
|
||||
},
|
||||
outcome: {
|
||||
key: 11330011,
|
||||
prize: {
|
||||
deliveryMode: 'USER_CHOICE',
|
||||
},
|
||||
},
|
||||
index: 1,
|
||||
prizeIdentifier: null,
|
||||
})
|
||||
|
||||
assert.equal(firstResolved, true)
|
||||
assert.equal(secondResolved, false)
|
||||
})
|
||||
Reference in New Issue
Block a user