初始化

This commit is contained in:
DESKTOP-U5J7A7B\Willtech007
2026-10-12 17:55:59 +08:00
parent 54b4d8f8c5
commit 633690b0c4
52 changed files with 15478 additions and 0 deletions
+199
View File
@@ -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')
})
+57
View File
@@ -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>/)
})
+67
View File
@@ -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'],
)
})
+103
View File
@@ -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()
}
})
+107
View File
@@ -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')
},
))
+463
View File
@@ -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)
})