试试
This commit is contained in:
@@ -168,9 +168,6 @@ function createMockContext() {
|
|||||||
process.env.MOCK_PROMOTION_REDEMPTION_INITIATE_SOURCE || 'PROMOTION_MODULE'
|
process.env.MOCK_PROMOTION_REDEMPTION_INITIATE_SOURCE || 'PROMOTION_MODULE'
|
||||||
const promotionRedemptionSourceBucket =
|
const promotionRedemptionSourceBucket =
|
||||||
(process.env.MOCK_PROMOTION_REDEMPTION_SOURCE_BUCKET || 'm-comp').trim().toLowerCase()
|
(process.env.MOCK_PROMOTION_REDEMPTION_SOURCE_BUCKET || 'm-comp').trim().toLowerCase()
|
||||||
const promotionRedemptionSimulateErrorAfterBridge =
|
|
||||||
(process.env.MOCK_PROMOTION_REDEMPTION_SIMULATE_ERROR_AFTER_BRIDGE || 'false').toLowerCase() ===
|
|
||||||
'true'
|
|
||||||
const defaultPromotionRedemptionIdStart = resolveDefaultPromotionRedemptionIdStart()
|
const defaultPromotionRedemptionIdStart = resolveDefaultPromotionRedemptionIdStart()
|
||||||
const promotionRedemptionIdStartRaw =
|
const promotionRedemptionIdStartRaw =
|
||||||
Number(process.env.MOCK_PROMOTION_REDEMPTION_ID_START || defaultPromotionRedemptionIdStart)
|
Number(process.env.MOCK_PROMOTION_REDEMPTION_ID_START || defaultPromotionRedemptionIdStart)
|
||||||
@@ -209,15 +206,11 @@ function createMockContext() {
|
|||||||
process.env.MOCK_LDAP_ALLOW_ANY_USER_GROUP_CN || 'SG-APP-EWALLET-SUPER-ADMIN'
|
process.env.MOCK_LDAP_ALLOW_ANY_USER_GROUP_CN || 'SG-APP-EWALLET-SUPER-ADMIN'
|
||||||
const ldapAllowAnyUserMailDomain =
|
const ldapAllowAnyUserMailDomain =
|
||||||
process.env.MOCK_LDAP_ALLOW_ANY_USER_MAIL_DOMAIN || 'macausjm-glp.com'
|
process.env.MOCK_LDAP_ALLOW_ANY_USER_MAIL_DOMAIN || 'macausjm-glp.com'
|
||||||
const ldapForceNoSuchObject =
|
|
||||||
(process.env.MOCK_LDAP_FORCE_NO_SUCH_OBJECT || '').toLowerCase() === 'true'
|
|
||||||
const ldapForceNoSuchObjectMatch =
|
|
||||||
process.env.MOCK_LDAP_FORCE_NO_SUCH_OBJECT_MATCH || 'OU=macauin-glp,DC=com'
|
|
||||||
|
|
||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
firstName: process.env.MOCK_MEMBER_FIRST_NAME || 'Gary',
|
firstName: process.env.MOCK_MEMBER_FIRST_NAME || 'Gary',
|
||||||
lastName: process.env.MOCK_MEMBER_LAST_NAME || 'Chan',
|
lastName: process.env.MOCK_MEMBER_LAST_NAME || 'Chan',
|
||||||
nickName: process.env.MOCK_MEMBER_NICK_NAME || 'xxx',
|
nickName: process.env.MOCK_MEMBER_NICK_NAME || '陳加里',
|
||||||
tier: process.env.MOCK_MEMBER_TIER || 'Novel',
|
tier: process.env.MOCK_MEMBER_TIER || 'Novel',
|
||||||
eBonus: process.env.MOCK_MEMBER_EBONUS || '0',
|
eBonus: process.env.MOCK_MEMBER_EBONUS || '0',
|
||||||
phoneNum: process.env.MOCK_MEMBER_PHONE || '+853-68282140',
|
phoneNum: process.env.MOCK_MEMBER_PHONE || '+853-68282140',
|
||||||
@@ -309,7 +302,7 @@ function createMockContext() {
|
|||||||
firstName: 'Gary',
|
firstName: 'Gary',
|
||||||
lastName: 'Chan',
|
lastName: 'Chan',
|
||||||
middleName: 'David',
|
middleName: 'David',
|
||||||
nickName: 'xxxx',
|
nickName: '陳加里',
|
||||||
image: '',
|
image: '',
|
||||||
},
|
},
|
||||||
attribute: {
|
attribute: {
|
||||||
@@ -1138,22 +1131,6 @@ function createMockContext() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function findRecordedRedeemedPromotionOutcome(redeemedId) {
|
|
||||||
const numericRedeemedId = Number(redeemedId)
|
|
||||||
if (!Number.isFinite(numericRedeemedId)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const outcomeList of recordedRedeemedPromotionsByAccount.values()) {
|
|
||||||
const found = outcomeList.find((item) => item?.redemptionId === numericRedeemedId)
|
|
||||||
if (found) {
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task 10766: Return a deterministic redeem failure for the dedicated member so error rendering can be checked reliably.
|
// Task 10766: Return a deterministic redeem failure for the dedicated member so error rendering can be checked reliably.
|
||||||
function getPlayerPromotionRedeemMock(accountNum) {
|
function getPlayerPromotionRedeemMock(accountNum) {
|
||||||
if (isTask10766Account(accountNum)) {
|
if (isTask10766Account(accountNum)) {
|
||||||
@@ -1245,7 +1222,6 @@ function createMockContext() {
|
|||||||
promotionRedemptionBridgeTimeoutMs,
|
promotionRedemptionBridgeTimeoutMs,
|
||||||
promotionRedemptionInitiateSource,
|
promotionRedemptionInitiateSource,
|
||||||
promotionRedemptionSourceBucket,
|
promotionRedemptionSourceBucket,
|
||||||
promotionRedemptionSimulateErrorAfterBridge,
|
|
||||||
redeemedPromotionVoidErrorCode,
|
redeemedPromotionVoidErrorCode,
|
||||||
redeemedPromotionVoidErrorMsg,
|
redeemedPromotionVoidErrorMsg,
|
||||||
redeemedPromotionVoidResult,
|
redeemedPromotionVoidResult,
|
||||||
@@ -1263,8 +1239,6 @@ function createMockContext() {
|
|||||||
ldapAllowAnyUserOu,
|
ldapAllowAnyUserOu,
|
||||||
ldapAllowAnyUserGroupCn,
|
ldapAllowAnyUserGroupCn,
|
||||||
ldapAllowAnyUserMailDomain,
|
ldapAllowAnyUserMailDomain,
|
||||||
ldapForceNoSuchObject,
|
|
||||||
ldapForceNoSuchObjectMatch,
|
|
||||||
},
|
},
|
||||||
edipForceErrorCode: process.env.MOCK_EDIP_FORCE_ERROR_CODE || '',
|
edipForceErrorCode: process.env.MOCK_EDIP_FORCE_ERROR_CODE || '',
|
||||||
edipForceErrorMsg: process.env.MOCK_EDIP_FORCE_ERROR_MSG || 'Mock EDIP token error',
|
edipForceErrorMsg: process.env.MOCK_EDIP_FORCE_ERROR_MSG || 'Mock EDIP token error',
|
||||||
@@ -1301,7 +1275,6 @@ function createMockContext() {
|
|||||||
recordRedeemedPromotionOutcome,
|
recordRedeemedPromotionOutcome,
|
||||||
getRecordedRedeemedPromotionOutcomes,
|
getRecordedRedeemedPromotionOutcomes,
|
||||||
voidRecordedRedeemedPromotionOutcome,
|
voidRecordedRedeemedPromotionOutcome,
|
||||||
findRecordedRedeemedPromotionOutcome,
|
|
||||||
getAccountSpecificRedeemedPromotionsFixture,
|
getAccountSpecificRedeemedPromotionsFixture,
|
||||||
cloneJsonValue,
|
cloneJsonValue,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ function createMockLdapService({ ldapConfig }) {
|
|||||||
ldapAllowAnyUserOu,
|
ldapAllowAnyUserOu,
|
||||||
ldapAllowAnyUserGroupCn,
|
ldapAllowAnyUserGroupCn,
|
||||||
ldapAllowAnyUserMailDomain,
|
ldapAllowAnyUserMailDomain,
|
||||||
ldapForceNoSuchObject,
|
|
||||||
ldapForceNoSuchObjectMatch,
|
|
||||||
} = ldapConfig
|
} = ldapConfig
|
||||||
|
|
||||||
function normalizeDn(value) {
|
function normalizeDn(value) {
|
||||||
@@ -457,15 +455,6 @@ function createMockLdapService({ ldapConfig }) {
|
|||||||
const searchBase = resolveSearchBase(req.dn?.toString())
|
const searchBase = resolveSearchBase(req.dn?.toString())
|
||||||
const scope = req.scope || 'sub'
|
const scope = req.scope || 'sub'
|
||||||
|
|
||||||
if (ldapForceNoSuchObject) {
|
|
||||||
// Reproduce QA's "LDAP error code 32 - NO_OBJECT" from real AD.
|
|
||||||
// The "best match of" suffix mirrors the AD DSID-0310028D / NameErr output seen on QA.
|
|
||||||
const match = ldapForceNoSuchObjectMatch || 'OU=macauin-glp,DC=com'
|
|
||||||
const error = new ldap.NoSuchObjectError()
|
|
||||||
error.message = `NO_OBJECT: best match of: '${match}'\n000001`
|
|
||||||
return next(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ldapAllowAnyUserMode) {
|
if (ldapAllowAnyUserMode) {
|
||||||
let hasAnyModeResult = false
|
let hasAnyModeResult = false
|
||||||
const usernames = dedupeStringValues(extractEqFilterValues(req.filter, 'sAMAccountName'))
|
const usernames = dedupeStringValues(extractEqFilterValues(req.filter, 'sAMAccountName'))
|
||||||
|
|||||||
@@ -268,10 +268,15 @@ function resolveTargetPrizeValue(prize, sourceBucket, promotion) {
|
|||||||
return bucketPrice
|
return bucketPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the source bucket price is absent, leave the value unspecified (null)
|
if (Number.isFinite(prize?.prizeValue)) {
|
||||||
// so the redemption payload sends valueUsed: null and eWallet computes it
|
return prize.prizeValue
|
||||||
// from its own configuration (instead of falling back to prizeValue).
|
}
|
||||||
return null
|
|
||||||
|
if (Number.isFinite(promotion?.toEarn1Unit)) {
|
||||||
|
return promotion.toEarn1Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPromotionMetadata(requestData, promotion, redemptionId) {
|
function buildPromotionMetadata(requestData, promotion, redemptionId) {
|
||||||
@@ -443,11 +448,7 @@ async function bridgePromotionRedeemToEwallet({
|
|||||||
accountNum,
|
accountNum,
|
||||||
backdate = false,
|
backdate = false,
|
||||||
}) {
|
}) {
|
||||||
const { log } = require('../../../logger')
|
|
||||||
const requestData = requestBody?.data || {}
|
const requestData = requestBody?.data || {}
|
||||||
log(
|
|
||||||
`[BRIDGE] start account=${accountNum} promotionId=${requestData.promotionId} outcomeId=${requestData.outcomeId} backdate=${backdate}`,
|
|
||||||
)
|
|
||||||
const selection = context.resolvePromotionRedeemSelection(
|
const selection = context.resolvePromotionRedeemSelection(
|
||||||
accountNum,
|
accountNum,
|
||||||
requestData.promotionId,
|
requestData.promotionId,
|
||||||
@@ -501,7 +502,6 @@ async function bridgePromotionRedeemToEwallet({
|
|||||||
throw new Error('Promotion bridge login did not return an access token')
|
throw new Error('Promotion bridge login did not return an access token')
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`[BRIDGE] calling eWallet POST ${redemptionUrl} (sync)`)
|
|
||||||
const redemptionResponseBody = await requestJson(
|
const redemptionResponseBody = await requestJson(
|
||||||
redemptionUrl,
|
redemptionUrl,
|
||||||
createRequestOptions(
|
createRequestOptions(
|
||||||
@@ -518,12 +518,10 @@ async function bridgePromotionRedeemToEwallet({
|
|||||||
context.promotionRedemptionBridgeTimeoutMs,
|
context.promotionRedemptionBridgeTimeoutMs,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
log(`[BRIDGE][SYNC-RAW] ${JSON.stringify(redemptionResponseBody)}`)
|
|
||||||
const redemptionResponseData = unwrapApiResponseBody(
|
const redemptionResponseData = unwrapApiResponseBody(
|
||||||
redemptionResponseBody,
|
redemptionResponseBody,
|
||||||
'Promotion bridge prize redemption',
|
'Promotion bridge prize redemption',
|
||||||
)
|
)
|
||||||
log(`[BRIDGE][SYNC-DATA] ${JSON.stringify(redemptionResponseData)}`)
|
|
||||||
|
|
||||||
if (!redemptionResponseData?.publicId) {
|
if (!redemptionResponseData?.publicId) {
|
||||||
throw new Error('Promotion bridge prize redemption did not return a publicId')
|
throw new Error('Promotion bridge prize redemption did not return a publicId')
|
||||||
@@ -557,10 +555,6 @@ function registerPlayerPromotionRedeemRoute({ app, context }) {
|
|||||||
]
|
]
|
||||||
|
|
||||||
routes.forEach((route) => app.post(route, async (req, res) => {
|
routes.forEach((route) => app.post(route, async (req, res) => {
|
||||||
const { log } = require('../../../logger')
|
|
||||||
log(
|
|
||||||
`[REDEEM-RECV] ${route} account=${context.resolveAccountNum(req.body)} promotionId=${req.body?.data?.promotionId} outcomeId=${req.body?.data?.outcomeId}`,
|
|
||||||
)
|
|
||||||
const headerMergedRequestBody = mergeIssueTargetToWalletOverrides(req.body, req.headers)
|
const headerMergedRequestBody = mergeIssueTargetToWalletOverrides(req.body, req.headers)
|
||||||
const accountNum = context.resolveAccountNum(headerMergedRequestBody)
|
const accountNum = context.resolveAccountNum(headerMergedRequestBody)
|
||||||
const normalizedRequestBody = mergeRuntimeIssueTargetToWalletOverrides(
|
const normalizedRequestBody = mergeRuntimeIssueTargetToWalletOverrides(
|
||||||
@@ -574,7 +568,6 @@ function registerPlayerPromotionRedeemRoute({ app, context }) {
|
|||||||
let errorMsg = redeemMock.errorMsg
|
let errorMsg = redeemMock.errorMsg
|
||||||
let result = redeemMock.result
|
let result = redeemMock.result
|
||||||
|
|
||||||
let simulatePublicId = null
|
|
||||||
if (result && !errorCode) {
|
if (result && !errorCode) {
|
||||||
try {
|
try {
|
||||||
if (context.promotionRedemptionBridgeEnabled) {
|
if (context.promotionRedemptionBridgeEnabled) {
|
||||||
@@ -584,18 +577,7 @@ function registerPlayerPromotionRedeemRoute({ app, context }) {
|
|||||||
accountNum,
|
accountNum,
|
||||||
backdate: backdateRedeemRequest,
|
backdate: backdateRedeemRequest,
|
||||||
})
|
})
|
||||||
if (context.promotionRedemptionSimulateErrorAfterBridge) {
|
|
||||||
// Plan C: simulate the promotion error AFTER the bridge created the
|
|
||||||
// eWallet redemption, but do NOT record a redeemed promotion in the
|
|
||||||
// Mock store so the history won't show a non-voided "success".
|
|
||||||
simulatePublicId =
|
|
||||||
bridgeResult.redeemedOutcome?.outcome?.redeemedList?.[0]?.transId || null
|
|
||||||
errorCode = 'SIMULATED_PROMO_REDEEM_ERROR'
|
|
||||||
errorMsg = `Simulated promotion redeem failure after eWallet sync created publicId=${simulatePublicId}`
|
|
||||||
result = false
|
|
||||||
} else {
|
|
||||||
context.recordRedeemedPromotionOutcome(accountNum, bridgeResult.redeemedOutcome)
|
context.recordRedeemedPromotionOutcome(accountNum, bridgeResult.redeemedOutcome)
|
||||||
}
|
|
||||||
} else if (backdateRedeemRequest) {
|
} else if (backdateRedeemRequest) {
|
||||||
const mockResult = createMockPromotionRedeemResult({
|
const mockResult = createMockPromotionRedeemResult({
|
||||||
context,
|
context,
|
||||||
@@ -617,7 +599,6 @@ function registerPlayerPromotionRedeemRoute({ app, context }) {
|
|||||||
errorMsg,
|
errorMsg,
|
||||||
accountNum,
|
accountNum,
|
||||||
result,
|
result,
|
||||||
...(simulatePublicId ? { redemptionPublicId: simulatePublicId } : {}),
|
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,198 +1,15 @@
|
|||||||
const { log } = require('../../../logger')
|
|
||||||
|
|
||||||
function joinUrl(baseUrl, pathname) {
|
|
||||||
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`
|
|
||||||
return new URL(pathname.replace(/^\//, ''), normalizedBaseUrl).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
function createRequestOptions(method, body, headers, timeoutMs) {
|
|
||||||
return {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: body === undefined ? undefined : JSON.stringify(body),
|
|
||||||
signal: AbortSignal.timeout(timeoutMs),
|
|
||||||
timeoutMs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestJson(url, options) {
|
|
||||||
let response
|
|
||||||
try {
|
|
||||||
response = await fetch(url, options)
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
|
||||||
if (error?.name === 'TimeoutError' || /aborted due to timeout/i.test(message)) {
|
|
||||||
throw new Error(
|
|
||||||
`Timeout after ${options.timeoutMs}ms on ${options.method || 'GET'} request for "${url}"`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
const rawBody = await response.text()
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`HTTP ${response.status} ${response.statusText} on ${options.method || 'GET'} request for "${url}"${
|
|
||||||
rawBody ? `: ${rawBody}` : ''
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!rawBody) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(rawBody)
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid JSON response on ${options.method || 'GET'} request for "${url}": "${rawBody}"`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrapApiResponseBody(responseBody, requestLabel) {
|
|
||||||
if (!responseBody || typeof responseBody !== 'object') {
|
|
||||||
throw new Error(`${requestLabel} returned an empty response body`)
|
|
||||||
}
|
|
||||||
if (Object.prototype.hasOwnProperty.call(responseBody, 'success')) {
|
|
||||||
if (responseBody.success === false) {
|
|
||||||
const detail =
|
|
||||||
responseBody.error?.detail || responseBody.message || `${requestLabel} returned an error`
|
|
||||||
throw new Error(detail)
|
|
||||||
}
|
|
||||||
return responseBody.data
|
|
||||||
}
|
|
||||||
return responseBody
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginToEwallet(context) {
|
|
||||||
const baseUrl = context.promotionRedemptionBridgeBaseUrl
|
|
||||||
if (!baseUrl) {
|
|
||||||
throw new Error('promotionRedemptionBridgeBaseUrl is not configured; cannot bridge void to eWallet')
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginUsername = context.promotionRedemptionBridgeLoginUsername || 'mock.gateway'
|
|
||||||
const authUrl = joinUrl(baseUrl, '/api/v1/auth/login')
|
|
||||||
const authResp = await requestJson(
|
|
||||||
authUrl,
|
|
||||||
createRequestOptions(
|
|
||||||
'POST',
|
|
||||||
{
|
|
||||||
username: loginUsername,
|
|
||||||
password: context.promotionRedemptionBridgeLoginPassword,
|
|
||||||
},
|
|
||||||
{ 'Content-Type': 'application/json' },
|
|
||||||
context.promotionRedemptionBridgeTimeoutMs,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const authData = unwrapApiResponseBody(authResp, 'Promotion void bridge login')
|
|
||||||
const accessToken = authData?.accessToken
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('Promotion void bridge login did not return an access token')
|
|
||||||
}
|
|
||||||
return accessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirror playerPromotionRedeem.bridgePromotionRedeemToEwallet: in production the promotion /
|
|
||||||
// Bally void first calls eWallet's own prize-redemption void, and only after that succeeds does
|
|
||||||
// Bally mark its promotion record voided. This Mock simulates that cascade so the eWallet side
|
|
||||||
// (prize_redemption_transactions / prize_redemption_void_transactions) is voided before the
|
|
||||||
// recorded promotion file flips isVoided.
|
|
||||||
//
|
|
||||||
// A promotion outcome may contain multiple redeemed prizes, each with its own eWallet
|
|
||||||
// prize-redemption publicId (transId) nested under outcome.redeemedList. Void every one of them.
|
|
||||||
async function voidPrizeRedemptionInEwallet(context, { accessToken, transId, correlationId, voidReason, initiateType, initiateSource, schema, data }) {
|
|
||||||
const baseUrl = context.promotionRedemptionBridgeBaseUrl
|
|
||||||
const voidUrl = joinUrl(baseUrl, `/api/v1/prize-redemptions/${transId}/sync`)
|
|
||||||
log(`[BRIDGE][VOID] calling eWallet DELETE ${voidUrl} (correlationId=${correlationId}, transId=${transId})`)
|
|
||||||
const voidResp = await requestJson(
|
|
||||||
voidUrl,
|
|
||||||
createRequestOptions(
|
|
||||||
'DELETE',
|
|
||||||
{
|
|
||||||
correlationId,
|
|
||||||
voidReason,
|
|
||||||
initiateType,
|
|
||||||
initiateSource,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
'Client-Machine-Id': schema?.computerName || 'mock-gateway',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Location-Code': data?.locationCode || 'MOCK',
|
|
||||||
'Property-Code': data?.casinoCode || 'GLP',
|
|
||||||
'X-Real-IP': schema?.ipAddress || '',
|
|
||||||
},
|
|
||||||
context.promotionRedemptionBridgeTimeoutMs,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const voidData = unwrapApiResponseBody(voidResp, 'Promotion void bridge prize redemption void')
|
|
||||||
log(`[BRIDGE][VOID] eWallet void result overallStatus=${voidData?.overallStatus} (transId=${transId})`)
|
|
||||||
// eWallet treats FAILED / COMPENSATED / PENDING / PROCESSING as errors; a successful void
|
|
||||||
// comes back as VOIDED (or COMPLETED). Anything else is treated as success here.
|
|
||||||
const failedStatuses = ['FAILED', 'COMPENSATED', 'PENDING', 'PROCESSING']
|
|
||||||
if (voidData?.overallStatus && failedStatuses.includes(voidData.overallStatus)) {
|
|
||||||
throw new Error(`eWallet prize redemption void finished with status ${voidData.overallStatus} (transId=${transId})`)
|
|
||||||
}
|
|
||||||
return voidData
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerRedeemedPromotionVoidRoute({ app, context }) {
|
function registerRedeemedPromotionVoidRoute({ app, context }) {
|
||||||
const routes = [
|
const routes = [
|
||||||
'/ewl/promotion/v1/redeemedpromotion/void',
|
'/ewl/promotion/v1/redeemedpromotion/void',
|
||||||
'/ewl/bal/v1/redeemedpromotion/void',
|
'/ewl/bal/v1/redeemedpromotion/void',
|
||||||
]
|
]
|
||||||
|
|
||||||
routes.forEach((route) =>
|
routes.forEach((route) => app.post(route, (req, res) => {
|
||||||
app.post(
|
|
||||||
route,
|
|
||||||
async (req, res) => {
|
|
||||||
const schema = req.body?.schema || {}
|
|
||||||
const data = req.body?.data || {}
|
|
||||||
const redeemedId = data.redeemedId
|
|
||||||
const loginId = data.loginId || ''
|
|
||||||
|
|
||||||
// eWallet void payload. The Bally promotion void request does not carry these, so derive them.
|
|
||||||
const correlationId = `VOID_PROMO_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
|
|
||||||
const voidReason = data?.remark || 'PROMOTION_VOID'
|
|
||||||
const initiateType = data?.initiateType || 'SYSTEM'
|
|
||||||
const initiateSource = data?.initiateSource || 'WEB_APP'
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Resolve the eWallet prize-redemption publicId(s) (transId) tied to this promotion
|
|
||||||
// redemption. They are nested under outcome.redeemedList[].transId in the recorded
|
|
||||||
// promotion (NOT at the top level of the recorded outcome entry). A promotion outcome
|
|
||||||
// may contain multiple redeemed prizes, each with its own transId.
|
|
||||||
const recordedOutcome = context.findRecordedRedeemedPromotionOutcome
|
|
||||||
? context.findRecordedRedeemedPromotionOutcome(redeemedId)
|
|
||||||
: null
|
|
||||||
|
|
||||||
const transIds = recordedOutcome?.outcome?.redeemedList
|
|
||||||
? recordedOutcome.outcome.redeemedList.map((r) => r.transId).filter(Boolean)
|
|
||||||
: []
|
|
||||||
|
|
||||||
if (transIds.length > 0 && context.promotionRedemptionBridgeBaseUrl) {
|
|
||||||
const accessToken = await loginToEwallet(context)
|
|
||||||
for (const transId of transIds) {
|
|
||||||
await voidPrizeRedemptionInEwallet(context, {
|
|
||||||
accessToken,
|
|
||||||
transId,
|
|
||||||
correlationId,
|
|
||||||
voidReason,
|
|
||||||
initiateType,
|
|
||||||
initiateSource,
|
|
||||||
schema,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
log(`[BRIDGE][VOID] eWallet prize redemption void succeeded for transIds=${transIds.join(',')}`)
|
|
||||||
} else {
|
|
||||||
log(
|
|
||||||
`[BRIDGE][VOID] skipped eWallet void bridge (transIds=${JSON.stringify(transIds)}, baseUrl=${context.promotionRedemptionBridgeBaseUrl})`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only mark the recorded promotion as voided after the eWallet void succeeded.
|
|
||||||
if (context.redeemedPromotionVoidResult) {
|
if (context.redeemedPromotionVoidResult) {
|
||||||
context.voidRecordedRedeemedPromotionOutcome(redeemedId, loginId)
|
context.voidRecordedRedeemedPromotionOutcome(
|
||||||
|
req.body?.data?.redeemedId,
|
||||||
|
req.body?.data?.loginId || '',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -200,17 +17,7 @@ function registerRedeemedPromotionVoidRoute({ app, context }) {
|
|||||||
errorMsg: context.redeemedPromotionVoidErrorMsg,
|
errorMsg: context.redeemedPromotionVoidErrorMsg,
|
||||||
result: context.redeemedPromotionVoidResult,
|
result: context.redeemedPromotionVoidResult,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
}))
|
||||||
log(`[BRIDGE][VOID] eWallet void bridge failed: ${error.message}`)
|
|
||||||
return res.status(200).json({
|
|
||||||
errorCode: context.redeemedPromotionVoidErrorCode || 'EWALLET_VOID_FAILED',
|
|
||||||
errorMsg: context.redeemedPromotionVoidErrorMsg || error.message,
|
|
||||||
result: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = registerRedeemedPromotionVoidRoute
|
module.exports = registerRedeemedPromotionVoidRoute
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Message Gateway 内容发送接口 Mock:POST /ewl/msgw/v1/messages/content/send
|
||||||
|
// 后端短信已从旧 SMS Hub(/SMSToken + /SMSRequest/Post)切换到该接口,认证复用 EDIP Token。
|
||||||
|
// 响应严格按真实网关信封:HTTP 200 + status/errorCode,成功时 smsid 为全小写字段、errorCode 为空字符串。
|
||||||
|
function registerMessageGatewayContentSendRoute({ app }) {
|
||||||
|
app.post('/ewl/msgw/v1/messages/content/send', (req, res) => {
|
||||||
|
const data = req.body?.data || {}
|
||||||
|
const recipients = Array.isArray(data.recipients) ? data.recipients : []
|
||||||
|
|
||||||
|
// 错误注入:MOCK_SMS_FORCE_ERROR_CODE=8923 模拟供应商暂不可用(后端应重试),
|
||||||
|
// MOCK_SMS_FORCE_ERROR_CODE=8907 模拟号码非法(后端不应重试)
|
||||||
|
const forcedErrorCode = process.env.MOCK_SMS_FORCE_ERROR_CODE || ''
|
||||||
|
if (forcedErrorCode) {
|
||||||
|
return res.status(200).json({
|
||||||
|
status: false,
|
||||||
|
errorCode: forcedErrorCode,
|
||||||
|
message: process.env.MOCK_SMS_FORCE_ERROR_MSG || 'Mock message gateway error',
|
||||||
|
data: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
errorCode: '',
|
||||||
|
message: '',
|
||||||
|
data: {
|
||||||
|
smsid: `mock-sms-${Date.now()}`,
|
||||||
|
smsStatus: recipients.map((r) => ({
|
||||||
|
recipient: r?.phone || '',
|
||||||
|
status: 'QUEUED',
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerMessageGatewayContentSendRoute
|
||||||
@@ -21,6 +21,7 @@ const registerPlayerPromotionFulfillmentJourneyDetailRoute = require('./ewl/bal/
|
|||||||
const registerPrizeRedemptionsSyncRoute = require('./api/v1/prizeRedemptionsSync')
|
const registerPrizeRedemptionsSyncRoute = require('./api/v1/prizeRedemptionsSync')
|
||||||
const registerPromotionRedeemFulfillmentOverridesRoute = require('./mockApi/promotionRedeemFulfillmentOverrides')
|
const registerPromotionRedeemFulfillmentOverridesRoute = require('./mockApi/promotionRedeemFulfillmentOverrides')
|
||||||
const registerSmsPortalRoutes = require('./sms/smsPortal')
|
const registerSmsPortalRoutes = require('./sms/smsPortal')
|
||||||
|
const registerMessageGatewayContentSendRoute = require('./ewl/msgw/contentSend')
|
||||||
|
|
||||||
function registerRoutes({ app, context, ldapService }) {
|
function registerRoutes({ app, context, ldapService }) {
|
||||||
registerHealthzRoute({ app, ldapService })
|
registerHealthzRoute({ app, ldapService })
|
||||||
@@ -46,6 +47,7 @@ function registerRoutes({ app, context, ldapService }) {
|
|||||||
registerPrizeRedemptionsSyncRoute({ app, context })
|
registerPrizeRedemptionsSyncRoute({ app, context })
|
||||||
registerPromotionRedeemFulfillmentOverridesRoute({ app, context })
|
registerPromotionRedeemFulfillmentOverridesRoute({ app, context })
|
||||||
registerSmsPortalRoutes({ app })
|
registerSmsPortalRoutes({ app })
|
||||||
|
registerMessageGatewayContentSendRoute({ app })
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { registerRoutes }
|
module.exports = { registerRoutes }
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ const { registerRoutes } = require('./routes/registerRoutes')
|
|||||||
const app = express()
|
const app = express()
|
||||||
app.use(express.json({ limit: '2mb' }))
|
app.use(express.json({ limit: '2mb' }))
|
||||||
|
|
||||||
const { requestLogger, log } = require('./logger')
|
|
||||||
app.use(requestLogger)
|
|
||||||
|
|
||||||
const context = createMockContext()
|
const context = createMockContext()
|
||||||
const ldapService = createMockLdapService({ ldapConfig: context.ldap })
|
const ldapService = createMockLdapService({ ldapConfig: context.ldap })
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user