初始化
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Get redeemed promotions successfully.",
|
||||||
|
"data": {
|
||||||
|
"customerId": "800049052",
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"gamingDate": "2026-02-13",
|
||||||
|
"normalPromo": true,
|
||||||
|
"postedBy": "ballytest",
|
||||||
|
"postedDtm": "2026-01-14T16:49:57.305069",
|
||||||
|
"casinoCode": "GLP",
|
||||||
|
"locationCode": "rs",
|
||||||
|
"computerName": "QVTERAPP10",
|
||||||
|
"ipAddress": "10.51.23.199",
|
||||||
|
"prizeRuleCode": "WEP15pts",
|
||||||
|
"prizeRuleId": 506,
|
||||||
|
"promotionCode": "QAWEP01",
|
||||||
|
"promotionId": 7,
|
||||||
|
"redeemedQty": 1,
|
||||||
|
"redemptionId": 18219,
|
||||||
|
"remark": "",
|
||||||
|
"outcome": {
|
||||||
|
"redeemedList": [
|
||||||
|
{
|
||||||
|
"outcomeRemark": "mqa-openprizeb",
|
||||||
|
"outcomeType": "PRIZE",
|
||||||
|
"prizeCategory": null,
|
||||||
|
"transId": "f4928051-43ff-4bc5-a1c8-8978c3f4748e",
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "mqa-openprizeb",
|
||||||
|
"behaviorProfileCode": "RETAIL_OPEN_ITEM",
|
||||||
|
"name": "MQA-OpenPrizeB",
|
||||||
|
"description": "MQA-OpenPrizeB",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 7.00,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"prizeCategory": "Retail",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-11-10T12:35:22+08:00",
|
||||||
|
"createdBy": "qa-issaclo",
|
||||||
|
"updatedAt": "2026-02-06T10:24:57+08:00",
|
||||||
|
"updatedBy": "qa-issaclo",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "RETAIL_OPEN_ITEM",
|
||||||
|
"profileName": "Retail (Open Item)",
|
||||||
|
"prizeCategory": "Retail",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "Retail target-only item, user choice delivery",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": {},
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outcomeAward": null,
|
||||||
|
"voidedBy": "v-wt-mark",
|
||||||
|
"voidedDtm": "2026-02-13T23:17:54.082765",
|
||||||
|
"voidedGamingDate": "",
|
||||||
|
"voidedRemark": "mark test",
|
||||||
|
"displayOnlyPromo": false,
|
||||||
|
"isVoided": true,
|
||||||
|
"isDirectOffer": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"status": true,
|
||||||
|
"errorCode": "",
|
||||||
|
"message": "Mock member profile",
|
||||||
|
"data": {
|
||||||
|
"accountNum": "",
|
||||||
|
"playerId": "",
|
||||||
|
"card_num": [],
|
||||||
|
"isActive": true,
|
||||||
|
"isDeactive": false,
|
||||||
|
"isBanned": false,
|
||||||
|
"currentTier": "Novel",
|
||||||
|
"onDayTierPoint": 150,
|
||||||
|
"lastLocn": "Table 22 (VIP)",
|
||||||
|
"eBonus": "0",
|
||||||
|
"clubState": 1,
|
||||||
|
"primaryHost": "MOCK HOST",
|
||||||
|
"primaryHostID": 1000001,
|
||||||
|
"secondaryHost": "MOCK SECOND HOST",
|
||||||
|
"secondaryHostID": 1000002,
|
||||||
|
"phoneList": [
|
||||||
|
{
|
||||||
|
"phoneNum": "+853-68282140",
|
||||||
|
"phoneType": "Mobile1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"player": {
|
||||||
|
"title": "Mr",
|
||||||
|
"firstName": "Gary",
|
||||||
|
"lastName": "Chan",
|
||||||
|
"middleName": "David",
|
||||||
|
"nickName": "xxxx",
|
||||||
|
"image": ""
|
||||||
|
},
|
||||||
|
"attribute": {
|
||||||
|
"birthday": "1990-01-11",
|
||||||
|
"gender": "Male",
|
||||||
|
"height": "",
|
||||||
|
"weight": "",
|
||||||
|
"smoker": false,
|
||||||
|
"languageCode": "ENG"
|
||||||
|
},
|
||||||
|
"addressList": [],
|
||||||
|
"identityList": [],
|
||||||
|
"emailList": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Get backdate promotions successfully.",
|
||||||
|
"data": {
|
||||||
|
"customerId": "80038008",
|
||||||
|
"entitledPromotionList": [
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 1,
|
||||||
|
"awardsEntitled": 1,
|
||||||
|
"awardsRedeemLimit": 1,
|
||||||
|
"earnedPoints": 1,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": true,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-01T00:00",
|
||||||
|
"endDate": "2036-12-31T23:59",
|
||||||
|
"id": 38008,
|
||||||
|
"code": "PROMO-BIRTHDAY-2026",
|
||||||
|
"name": "Birthday Promotion",
|
||||||
|
"ruleId": 3800801,
|
||||||
|
"ruleName": "Birthday Redeem - choose 1 gift",
|
||||||
|
"ruleNameDesc": "Birthday Redeem - choose 1 gift",
|
||||||
|
"toEarn1Unit": 1,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 38008011,
|
||||||
|
"value": "it-test-fb-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"prizeCode": "it-test-fb-$1",
|
||||||
|
"name": "Birthday FnB Gift",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"prizeValue": 1,
|
||||||
|
"prizeCategory": "FnB",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"deliveryMode": "USER_CHOICE"
|
||||||
|
},
|
||||||
|
"bucketPrices": {
|
||||||
|
"d-comp": 1,
|
||||||
|
"m-comp": 1
|
||||||
|
},
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 38008012,
|
||||||
|
"value": "it-test-nnpc-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"prizeCode": "it-test-nnpc-$1",
|
||||||
|
"name": "Birthday NNPC Gift",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"prizeValue": 1,
|
||||||
|
"prizeCategory": "NNPC",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"deliveryMode": "USER_CHOICE"
|
||||||
|
},
|
||||||
|
"bucketPrices": {
|
||||||
|
"d-comp": 1,
|
||||||
|
"m-comp": 1
|
||||||
|
},
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 38008013,
|
||||||
|
"value": "it-test-ebo-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"prizeCode": "it-test-ebo-$1",
|
||||||
|
"name": "Birthday eBonus Gift",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"prizeValue": 1,
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"deliveryMode": "USER_CHOICE"
|
||||||
|
},
|
||||||
|
"bucketPrices": {
|
||||||
|
"d-comp": 1,
|
||||||
|
"m-comp": 1
|
||||||
|
},
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"eligiblePromotionList": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Get all promotions successfully.",
|
||||||
|
"data": {
|
||||||
|
"customerId": "80010766",
|
||||||
|
"eligiblePromotionList": [
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 1,
|
||||||
|
"awardsRedeemLimit": 1,
|
||||||
|
"earnedPoints": 8,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": true,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 10766001,
|
||||||
|
"code": "LP-MOCK-10766",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 10766024,
|
||||||
|
"ruleName": "GS-8pts-LCH Gift Set Abalone + Scallop",
|
||||||
|
"ruleNameDesc": "GS-8pts-LCH Gift Set Abalone + Scallop",
|
||||||
|
"toEarn1Unit": 8,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 10766069,
|
||||||
|
"value": "mock-10766-lch-gift-set-abalone-scallop",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entitledPromotionList": []
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,771 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Get all promotions successfully.",
|
||||||
|
"data": {
|
||||||
|
"customerId": "800038008",
|
||||||
|
"eligiblePromotionList": [
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 1,
|
||||||
|
"awardsEntitled": 1,
|
||||||
|
"awardsRedeemLimit": 1,
|
||||||
|
"earnedPoints": 3860,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": true,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-30T06:00",
|
||||||
|
"endDate": "2026-04-27T05:59",
|
||||||
|
"id": 71768001,
|
||||||
|
"code": "LP-EE-CNYIPENG",
|
||||||
|
"name": "CNY iPhone Earn & Get-2026",
|
||||||
|
"ruleId": 71768001,
|
||||||
|
"ruleName": "17-19Feb-CNY iPhone Earn & Get-1,388Pts",
|
||||||
|
"ruleNameDesc": "17-19Feb-CNY iPhone Earn & Get-1,388Pts",
|
||||||
|
"toEarn1Unit": 1388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 1,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 44,
|
||||||
|
"awardsEntitled": 43,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 3948,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": true,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768035,
|
||||||
|
"ruleName": "CNYGS-88pts-LCH Gift Set LCH-Bird's Nest Indonesia",
|
||||||
|
"ruleNameDesc": "CNYGS-88pts-LCH Gift Set LCH-Bird's Nest Indonesia",
|
||||||
|
"toEarn1Unit": 88,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768091,
|
||||||
|
"value": "lp-ee-cnygs26-88pts-birdsnest-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "lp-ee-cnygs26-88pts-birdsnest-me",
|
||||||
|
"behaviorProfileCode": "MERCH_AUTOSETTLE",
|
||||||
|
"name": "CNY26 88pts Birds Nest",
|
||||||
|
"description": null,
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 350.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"prizeCategory": "Merchandise",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "EXTERNAL_ONLY",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2026-02-16T12:47:41.824397+08:00",
|
||||||
|
"createdBy": "qa-adelinotou",
|
||||||
|
"createdByEmployeeId": null,
|
||||||
|
"updatedAt": "2026-02-16T12:47:41.824397+08:00",
|
||||||
|
"updatedBy": "qa-adelinotou",
|
||||||
|
"updatedByEmployeeId": null,
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "MERCH_AUTOSETTLE",
|
||||||
|
"profileName": "Merchandise (AutoSettle)",
|
||||||
|
"prizeCategory": "Merchandise",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "EXTERNAL_ONLY",
|
||||||
|
"description": "Merchandise target-only item auto-settled externally",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": "2026-02-16T12:47:41.694+08:00",
|
||||||
|
"effectiveTo": "2036-02-16T12:47:41.694+08:00",
|
||||||
|
"bucketPrices": {
|
||||||
|
"d-comp": 350,
|
||||||
|
"m-comp": 350
|
||||||
|
},
|
||||||
|
"compValue": 350,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 2,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 11,
|
||||||
|
"awardsEntitled": 9,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 4636,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": true,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768029,
|
||||||
|
"ruleName": "CNYGS-388pts-Moutai 200ML + $1,000 NP/EB",
|
||||||
|
"ruleNameDesc": "CNYGS-388pts-Moutai 200ML + $1,000 NP/EB",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768077,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-moutai200ml-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "lp-ee-cnygs26-388pts-moutai200ml-me",
|
||||||
|
"behaviorProfileCode": "MERCH_AUTOSETTLE",
|
||||||
|
"name": "CNY26 388pts Moutai 200 ML",
|
||||||
|
"description": null,
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1000.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"prizeCategory": "Merchandise",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "EXTERNAL_ONLY",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2026-02-16T12:39:46.397008+08:00",
|
||||||
|
"createdBy": "qa-adelinotou",
|
||||||
|
"createdByEmployeeId": null,
|
||||||
|
"updatedAt": "2026-02-16T12:39:46.397008+08:00",
|
||||||
|
"updatedBy": "qa-adelinotou",
|
||||||
|
"updatedByEmployeeId": null,
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "MERCH_AUTOSETTLE",
|
||||||
|
"profileName": "Merchandise (AutoSettle)",
|
||||||
|
"prizeCategory": "Merchandise",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "EXTERNAL_ONLY",
|
||||||
|
"description": "Merchandise target-only item auto-settled externally",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": "2026-02-16T12:39:46.271+08:00",
|
||||||
|
"effectiveTo": "2036-02-16T12:39:46.271+08:00",
|
||||||
|
"bucketPrices": {
|
||||||
|
"d-comp": 1000,
|
||||||
|
"m-comp": 1000
|
||||||
|
},
|
||||||
|
"compValue": 1000,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768078,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-1000-pc",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "lp-ee-cnygs26-388pts-1000-pc",
|
||||||
|
"behaviorProfileCode": "NNPC_OPEN_ITEM",
|
||||||
|
"name": "CNY26 388pts 1000 PC",
|
||||||
|
"description": null,
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1000.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"prizeCategory": "NNPC",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2026-02-16T12:33:30.522397+08:00",
|
||||||
|
"createdBy": "qa-adelinotou",
|
||||||
|
"createdByEmployeeId": null,
|
||||||
|
"updatedAt": "2026-02-16T12:33:30.522398+08:00",
|
||||||
|
"updatedBy": "qa-adelinotou",
|
||||||
|
"updatedByEmployeeId": null,
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "NNPC_OPEN_ITEM",
|
||||||
|
"profileName": "NNPC (Open Item)",
|
||||||
|
"prizeCategory": "NNPC",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "NNPC target-only item, user chooses wallet or external",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": "2026-02-16T12:34:51.704+08:00",
|
||||||
|
"effectiveTo": "2036-02-16T12:34:51.704+08:00",
|
||||||
|
"bucketPrices": {
|
||||||
|
"d-comp": 1000,
|
||||||
|
"m-comp": 1000
|
||||||
|
},
|
||||||
|
"compValue": 1000,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768079,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-1000-eb",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "lp-ee-cnygs26-388pts-1000-eb",
|
||||||
|
"behaviorProfileCode": "EBONUS_AUTOSETTLE",
|
||||||
|
"name": "CNY26 388pts 1000 EB",
|
||||||
|
"description": null,
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1000.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "EXTERNAL_ONLY",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2026-02-16T12:34:53.967027+08:00",
|
||||||
|
"createdBy": "qa-adelinotou",
|
||||||
|
"createdByEmployeeId": null,
|
||||||
|
"updatedAt": "2026-02-16T12:34:53.967027+08:00",
|
||||||
|
"updatedBy": "qa-adelinotou",
|
||||||
|
"updatedByEmployeeId": null,
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "EBONUS_AUTOSETTLE",
|
||||||
|
"profileName": "eBonus (AutoSettle)",
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "EXTERNAL_ONLY",
|
||||||
|
"description": "eBonus target-only item auto-settled externally",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": true
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": "2026-02-16T12:36:15.15+08:00",
|
||||||
|
"effectiveTo": "2036-02-16T12:36:15.15+08:00",
|
||||||
|
"bucketPrices": {
|
||||||
|
"d-comp": 1000,
|
||||||
|
"m-comp": 1000
|
||||||
|
},
|
||||||
|
"compValue": 1000,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 5,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 3,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-03-16T06:00",
|
||||||
|
"endDate": "2026-03-17T06:00",
|
||||||
|
"id": 1,
|
||||||
|
"code": "PROMO1001",
|
||||||
|
"name": "Promotion 1",
|
||||||
|
"ruleId": 17,
|
||||||
|
"ruleName": "Rule_13pts",
|
||||||
|
"ruleNameDesc": "daily 13pts",
|
||||||
|
"toEarn1Unit": 13,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 3579,
|
||||||
|
"value": "it-test-fb-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "it-test-fb-$1",
|
||||||
|
"behaviorProfileCode": "FNB_OPEN_ITEM",
|
||||||
|
"name": "IT-TEST-FB-$1",
|
||||||
|
"description": "IT-TEST-FB-$1",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P3D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": true
|
||||||
|
},
|
||||||
|
"prizeCategory": "FnB",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-11-04T11:00:53.232361+08:00",
|
||||||
|
"createdBy": "qa-nicolefeng",
|
||||||
|
"createdByEmployeeId": "173094",
|
||||||
|
"updatedAt": "2026-02-04T18:04:07.911273+08:00",
|
||||||
|
"updatedBy": "qa-randyng",
|
||||||
|
"updatedByEmployeeId": "171853",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "FNB_OPEN_ITEM",
|
||||||
|
"profileName": "FnB (Open Item)",
|
||||||
|
"prizeCategory": "FnB",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "Food and Beverage target-only item, user choice delivery",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": {},
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 3580,
|
||||||
|
"value": "it-test-nnpc-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "it-test-nnpc-$1",
|
||||||
|
"behaviorProfileCode": "NNPC_OPEN_ITEM",
|
||||||
|
"name": "it-test-nnpc-$1",
|
||||||
|
"description": "test nnpc",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": true
|
||||||
|
},
|
||||||
|
"prizeCategory": "NNPC",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-12-01T16:33:18.420102+08:00",
|
||||||
|
"createdBy": "qa-henrylai",
|
||||||
|
"createdByEmployeeId": "165114",
|
||||||
|
"updatedAt": "2025-12-18T17:56:51.783392+08:00",
|
||||||
|
"updatedBy": "qa-henrylai",
|
||||||
|
"updatedByEmployeeId": "165114",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "NNPC_OPEN_ITEM",
|
||||||
|
"profileName": "NNPC (Open Item)",
|
||||||
|
"prizeCategory": "NNPC",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "NNPC target-only item, user chooses wallet or external",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": null,
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 3581,
|
||||||
|
"value": "it-test-ebo-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "it-test-ebo-$1",
|
||||||
|
"behaviorProfileCode": "EBONUS_OPEN_ITEM",
|
||||||
|
"name": "IT-TEST-EBO-$1",
|
||||||
|
"description": "IT-TEST-EBO-$1",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P7D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": true
|
||||||
|
},
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-11-04T09:48:33.139099+08:00",
|
||||||
|
"createdBy": "qa-nicolefeng",
|
||||||
|
"createdByEmployeeId": "173094",
|
||||||
|
"updatedAt": "2025-12-26T15:20:31.57588+08:00",
|
||||||
|
"updatedBy": "qa-issaclo",
|
||||||
|
"updatedByEmployeeId": "165148",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "EBONUS_OPEN_ITEM",
|
||||||
|
"profileName": "eBonus (Open Item)",
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "eBonus target-only item, user chooses wallet or external",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": true
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": null,
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 10,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-03-16T06:00",
|
||||||
|
"endDate": "2026-03-23T05:59:59.999",
|
||||||
|
"id": 8,
|
||||||
|
"code": "EARN_N_GET",
|
||||||
|
"name": "Daily & Weeklyl Earn&Get",
|
||||||
|
"ruleId": 512,
|
||||||
|
"ruleName": "Weekly_100pts",
|
||||||
|
"ruleNameDesc": "Weekly_100pts",
|
||||||
|
"toEarn1Unit": 100,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 5061,
|
||||||
|
"value": "it-test-ebo-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "it-test-ebo-$1",
|
||||||
|
"behaviorProfileCode": "EBONUS_OPEN_ITEM",
|
||||||
|
"name": "IT-TEST-EBO-$1",
|
||||||
|
"description": "IT-TEST-EBO-$1",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P7D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": true
|
||||||
|
},
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-11-04T09:48:33.139099+08:00",
|
||||||
|
"createdBy": "qa-nicolefeng",
|
||||||
|
"createdByEmployeeId": "173094",
|
||||||
|
"updatedAt": "2025-12-26T15:20:31.57588+08:00",
|
||||||
|
"updatedBy": "qa-issaclo",
|
||||||
|
"updatedByEmployeeId": "165148",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "EBONUS_OPEN_ITEM",
|
||||||
|
"profileName": "eBonus (Open Item)",
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "eBonus target-only item, user chooses wallet or external",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": true
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": null,
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 5062,
|
||||||
|
"value": "it-test-fb-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "it-test-fb-$1",
|
||||||
|
"behaviorProfileCode": "FNB_OPEN_ITEM",
|
||||||
|
"name": "IT-TEST-FB-$1",
|
||||||
|
"description": "IT-TEST-FB-$1",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P3D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": true
|
||||||
|
},
|
||||||
|
"prizeCategory": "FnB",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-11-04T11:00:53.232361+08:00",
|
||||||
|
"createdBy": "qa-nicolefeng",
|
||||||
|
"createdByEmployeeId": "173094",
|
||||||
|
"updatedAt": "2026-02-04T18:04:07.911273+08:00",
|
||||||
|
"updatedBy": "qa-randyng",
|
||||||
|
"updatedByEmployeeId": "171853",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "FNB_OPEN_ITEM",
|
||||||
|
"profileName": "FnB (Open Item)",
|
||||||
|
"prizeCategory": "FnB",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "Food and Beverage target-only item, user choice delivery",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": {},
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 5063,
|
||||||
|
"value": "it-test-nnpc-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "it-test-nnpc-$1",
|
||||||
|
"behaviorProfileCode": "NNPC_OPEN_ITEM",
|
||||||
|
"name": "it-test-nnpc-$1",
|
||||||
|
"description": "test nnpc",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": true
|
||||||
|
},
|
||||||
|
"prizeCategory": "NNPC",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-12-01T16:33:18.420102+08:00",
|
||||||
|
"createdBy": "qa-henrylai",
|
||||||
|
"createdByEmployeeId": "165114",
|
||||||
|
"updatedAt": "2025-12-18T17:56:51.783392+08:00",
|
||||||
|
"updatedBy": "qa-henrylai",
|
||||||
|
"updatedByEmployeeId": "165114",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "NNPC_OPEN_ITEM",
|
||||||
|
"profileName": "NNPC (Open Item)",
|
||||||
|
"prizeCategory": "NNPC",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "NNPC target-only item, user chooses wallet or external",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": null,
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entitledPromotionList": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,401 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Get all promotions successfully.",
|
||||||
|
"data": {
|
||||||
|
"customerId": "800048718",
|
||||||
|
"eligiblePromotionList": [
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768024,
|
||||||
|
"ruleName": "CNYGS-188pts-Hennessy VSOP + LCH Dried Scallop",
|
||||||
|
"ruleNameDesc": "CNYGS-188pts-Hennessy VSOP + LCH Dried Scallop",
|
||||||
|
"toEarn1Unit": 188,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768069,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-hennessyvsop-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768070,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-lchscallop-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768025,
|
||||||
|
"ruleName": "CNYGS-188pts-LCH Gift Set LCH-Fish Maw & Scallop",
|
||||||
|
"ruleNameDesc": "CNYGS-188pts-LCH Gift Set LCH-Fish Maw & Scallop",
|
||||||
|
"toEarn1Unit": 188,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768071,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-lchgiftset-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768026,
|
||||||
|
"ruleName": "CNYGS-188pts-Martell + LCH-Can Abalone",
|
||||||
|
"ruleNameDesc": "CNYGS-188pts-Martell + LCH-Can Abalone",
|
||||||
|
"toEarn1Unit": 188,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768072,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-martellnoblige-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768073,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-lchabalone-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768027,
|
||||||
|
"ruleName": "CNYGS-388pts-LCH-Gift Set Fish Maw + Scallop",
|
||||||
|
"ruleNameDesc": "CNYGS-388pts-LCH-Gift Set Fish Maw + Scallop",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768074,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-lchdriedseafood-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768028,
|
||||||
|
"ruleName": "CNYGS-388pts-Martell + LCH Abalone",
|
||||||
|
"ruleNameDesc": "CNYGS-388pts-Martell + LCH Abalone",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768075,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-martellcny-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768076,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-lchabalone-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768029,
|
||||||
|
"ruleName": "CNYGS-388pts-Moutai 200ML + $1,000 NP/EB",
|
||||||
|
"ruleNameDesc": "CNYGS-388pts-Moutai 200ML + $1,000 NP/EB",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768077,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-moutai200ml-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768078,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-1000-pc",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768079,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-1000-eb",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 688,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768030,
|
||||||
|
"ruleName": "CNYGS-688pts-Hennessy XO + $2,200 NP/EB",
|
||||||
|
"ruleNameDesc": "CNYGS-688pts-Hennessy XO + $2,200 NP/EB",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768080,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-hennessyxocny-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768081,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-2200-pc",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768082,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-2200-eb",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 688,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768031,
|
||||||
|
"ruleName": "CNYGS-688pts-Moutai 500 ML + LCH Gift Set +Abalone",
|
||||||
|
"ruleNameDesc": "CNYGS-688pts-Moutai 500 ML + LCH Gift Set +Abalone",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768083,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-moutai500ml-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768084,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-lchmushroom-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768085,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-lchabalone-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 688,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768032,
|
||||||
|
"ruleName": "CNYGS-688pts-Whisky Live + $2,000 NP/EB",
|
||||||
|
"ruleNameDesc": "CNYGS-688pts-Whisky Live + $2,000 NP/EB",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768086,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-whiskylive-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768087,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-2000-pc",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768088,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-2000-eb",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Get all promotions successfully.",
|
||||||
|
"data": {
|
||||||
|
"customerId": "80010991",
|
||||||
|
"eligiblePromotionList": [
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 1,
|
||||||
|
"awardsEntitled": 1,
|
||||||
|
"awardsRedeemLimit": 1,
|
||||||
|
"earnedPoints": 5,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": true,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-01T00:00",
|
||||||
|
"endDate": "2036-01-01T00:00",
|
||||||
|
"id": 10991,
|
||||||
|
"code": "LP-MOCK-REPRINT-CHAIN",
|
||||||
|
"name": "Mock Promotion Reprint Chain",
|
||||||
|
"ruleId": 1099101,
|
||||||
|
"ruleName": "Mock-5pts-$20 GLP F&B voucher",
|
||||||
|
"ruleNameDesc": "Mock-5pts-$20 GLP F&B voucher",
|
||||||
|
"toEarn1Unit": 5,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 10991011,
|
||||||
|
"value": "lp-ee-nsuwr-5pts-20-fb",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "lp-ee-nsuwr-5pts-20-fb",
|
||||||
|
"behaviorProfileCode": "FNB_OPEN_ITEM",
|
||||||
|
"name": "NSUWR-5PTS-$20 GLP F&B voucher",
|
||||||
|
"description": "NSUWR-5PTS-$20 GLP F&B voucher",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 20.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"prizeCategory": "FnB",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2026-03-21T12:40:00+08:00",
|
||||||
|
"createdBy": "mock-gateway",
|
||||||
|
"createdByEmployeeId": null,
|
||||||
|
"updatedAt": "2026-03-21T12:40:00+08:00",
|
||||||
|
"updatedBy": "mock-gateway",
|
||||||
|
"updatedByEmployeeId": null,
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "FNB_OPEN_ITEM",
|
||||||
|
"profileName": "FnB (Open Item)",
|
||||||
|
"prizeCategory": "FnB",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "Food and Beverage target-only item, user choice delivery",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": {
|
||||||
|
"d-comp": 20.0,
|
||||||
|
"m-comp": 20.0
|
||||||
|
},
|
||||||
|
"compValue": 20.0,
|
||||||
|
"note": "Dedicated mock fixture for promotion redeem -> history -> reprint chain",
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 1,
|
||||||
|
"awardsEntitled": 1,
|
||||||
|
"awardsRedeemLimit": 1,
|
||||||
|
"earnedPoints": 1,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": true,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-01T00:00",
|
||||||
|
"endDate": "2036-01-01T00:00",
|
||||||
|
"id": 11225,
|
||||||
|
"code": "LP-MOCK-AUTOSETTLE-CHECK",
|
||||||
|
"name": "Mock Promotion AutoSettle Check",
|
||||||
|
"ruleId": 1122502,
|
||||||
|
"ruleName": "Mock-1pt-qa-ebonus-auto",
|
||||||
|
"ruleNameDesc": "Mock-1pt-qa-ebonus-auto",
|
||||||
|
"toEarn1Unit": 1,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 11225021,
|
||||||
|
"value": "qa-ebonus-auto",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "qa-ebonus-auto",
|
||||||
|
"behaviorProfileCode": "EBONUS_AUTOSETTLE",
|
||||||
|
"name": "qa-ebonus-auto",
|
||||||
|
"description": "for automation testing",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": false
|
||||||
|
},
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "EXTERNAL_ONLY",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2026-01-16T11:32:27.761213+08:00",
|
||||||
|
"createdBy": "qa-henrylai",
|
||||||
|
"createdByEmployeeId": null,
|
||||||
|
"updatedAt": "2026-01-16T11:32:27.761213+08:00",
|
||||||
|
"updatedBy": "qa-henrylai",
|
||||||
|
"updatedByEmployeeId": null,
|
||||||
|
"tags": [],
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "EBONUS_AUTOSETTLE",
|
||||||
|
"profileName": "eBonus (AutoSettle)",
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "EXTERNAL_ONLY",
|
||||||
|
"description": "eBonus target-only item auto-settled externally",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": true
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": false,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": "2026-01-16T11:32:27.555+08:00",
|
||||||
|
"effectiveTo": "2036-01-16T11:32:27.562+08:00",
|
||||||
|
"bucketPrices": {},
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 3,
|
||||||
|
"awardsEntitled": 3,
|
||||||
|
"awardsRedeemLimit": 3,
|
||||||
|
"earnedPoints": 100,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": true,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 3,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-01T00:00",
|
||||||
|
"endDate": "2036-01-01T00:00",
|
||||||
|
"id": 11330,
|
||||||
|
"code": "LP-MOCK-USER-CHOICE-3-OUTCOMES",
|
||||||
|
"name": "Mock Promotion 3 User Choice Outcomes",
|
||||||
|
"ruleId": 1133001,
|
||||||
|
"ruleName": "Mock-3-user-choice-prizes",
|
||||||
|
"ruleNameDesc": "Mock-3-user-choice-prizes",
|
||||||
|
"toEarn1Unit": 1,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 11330011,
|
||||||
|
"value": "it-test-fb-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "it-test-fb-$1",
|
||||||
|
"behaviorProfileCode": "FNB_OPEN_ITEM",
|
||||||
|
"name": "IT-TEST-FB-$1",
|
||||||
|
"description": "IT-TEST-FB-$1",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P3D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": true
|
||||||
|
},
|
||||||
|
"prizeCategory": "FnB",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-11-04T11:00:53.232361+08:00",
|
||||||
|
"createdBy": "qa-nicolefeng",
|
||||||
|
"createdByEmployeeId": "173094",
|
||||||
|
"updatedAt": "2026-02-04T18:04:07.911273+08:00",
|
||||||
|
"updatedBy": "qa-randyng",
|
||||||
|
"updatedByEmployeeId": "171853",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "FNB_OPEN_ITEM",
|
||||||
|
"profileName": "FnB (Open Item)",
|
||||||
|
"prizeCategory": "FnB",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "Food and Beverage target-only item, user choice delivery",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": {},
|
||||||
|
"compValue": null,
|
||||||
|
"note": "Dedicated mock fixture for 3 USER_CHOICE promotion redeem testing",
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 11330012,
|
||||||
|
"value": "it-test-nnpc-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "it-test-nnpc-$1",
|
||||||
|
"behaviorProfileCode": "NNPC_OPEN_ITEM",
|
||||||
|
"name": "it-test-nnpc-$1",
|
||||||
|
"description": "test nnpc",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P90D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": true
|
||||||
|
},
|
||||||
|
"prizeCategory": "NNPC",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-12-01T16:33:18.420102+08:00",
|
||||||
|
"createdBy": "qa-henrylai",
|
||||||
|
"createdByEmployeeId": "165114",
|
||||||
|
"updatedAt": "2025-12-18T17:56:51.783392+08:00",
|
||||||
|
"updatedBy": "qa-henrylai",
|
||||||
|
"updatedByEmployeeId": "165114",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "NNPC_OPEN_ITEM",
|
||||||
|
"profileName": "NNPC (Open Item)",
|
||||||
|
"prizeCategory": "NNPC",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "NNPC target-only item, user chooses wallet or external",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": false
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": null,
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 11330013,
|
||||||
|
"value": "it-test-ebo-$1",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": {
|
||||||
|
"correlationId": null,
|
||||||
|
"prizeCode": "it-test-ebo-$1",
|
||||||
|
"behaviorProfileCode": "EBONUS_OPEN_ITEM",
|
||||||
|
"name": "IT-TEST-EBO-$1",
|
||||||
|
"description": "IT-TEST-EBO-$1",
|
||||||
|
"source": "INTERNAL",
|
||||||
|
"isVoidable": true,
|
||||||
|
"isEnabled": true,
|
||||||
|
"imageUrl": null,
|
||||||
|
"prizeValue": 1.0,
|
||||||
|
"expiryConfig": {
|
||||||
|
"strategy": "MIN",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "ROLLING_DURATION",
|
||||||
|
"value": "P7D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isEnabled": true
|
||||||
|
},
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"createdAt": "2025-11-04T09:48:33.139099+08:00",
|
||||||
|
"createdBy": "qa-nicolefeng",
|
||||||
|
"createdByEmployeeId": "173094",
|
||||||
|
"updatedAt": "2025-12-26T15:20:31.57588+08:00",
|
||||||
|
"updatedBy": "qa-issaclo",
|
||||||
|
"updatedByEmployeeId": "165148",
|
||||||
|
"tags": null,
|
||||||
|
"behaviorProfile": {
|
||||||
|
"profileCode": "EBONUS_OPEN_ITEM",
|
||||||
|
"profileName": "eBonus (Open Item)",
|
||||||
|
"prizeCategory": "eBonus",
|
||||||
|
"usageBehavior": "TARGET_ONLY",
|
||||||
|
"deliveryMode": "USER_CHOICE",
|
||||||
|
"description": "eBonus target-only item, user chooses wallet or external",
|
||||||
|
"isActive": true,
|
||||||
|
"allowNegativeBalance": false,
|
||||||
|
"isBucket": false,
|
||||||
|
"isEbonus": true
|
||||||
|
},
|
||||||
|
"localizations": null,
|
||||||
|
"messages": null,
|
||||||
|
"isPrintVoucher": null,
|
||||||
|
"isExcludeGlobalMessages": null,
|
||||||
|
"effectiveFrom": null,
|
||||||
|
"effectiveTo": null,
|
||||||
|
"bucketPrices": null,
|
||||||
|
"compValue": null,
|
||||||
|
"note": null,
|
||||||
|
"uniqueCodeRequirement": "NONE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entitledPromotionList": []
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,402 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Get all promotions successfully.",
|
||||||
|
"data": {
|
||||||
|
"customerId": "800048718",
|
||||||
|
"eligiblePromotionList": [
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768024,
|
||||||
|
"ruleName": "CNYGS-188pts-Hennessy VSOP + LCH Dried Scallop",
|
||||||
|
"ruleNameDesc": "CNYGS-188pts-Hennessy VSOP + LCH Dried Scallop",
|
||||||
|
"toEarn1Unit": 188,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768069,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-hennessyvsop-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768070,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-lchscallop-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768025,
|
||||||
|
"ruleName": "CNYGS-188pts-LCH Gift Set LCH-Fish Maw & Scallop",
|
||||||
|
"ruleNameDesc": "CNYGS-188pts-LCH Gift Set LCH-Fish Maw & Scallop",
|
||||||
|
"toEarn1Unit": 188,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768071,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-lchgiftset-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768026,
|
||||||
|
"ruleName": "CNYGS-188pts-Martell + LCH-Can Abalone",
|
||||||
|
"ruleNameDesc": "CNYGS-188pts-Martell + LCH-Can Abalone",
|
||||||
|
"toEarn1Unit": 188,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768072,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-martellnoblige-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768073,
|
||||||
|
"value": "lp-ee-cnygs26-188pts-lchabalone-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 1,
|
||||||
|
"minOutcome": 1,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768027,
|
||||||
|
"ruleName": "CNYGS-388pts-LCH-Gift Set Fish Maw + Scallop",
|
||||||
|
"ruleNameDesc": "CNYGS-388pts-LCH-Gift Set Fish Maw + Scallop",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768074,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-lchdriedseafood-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768028,
|
||||||
|
"ruleName": "CNYGS-388pts-Martell + LCH Abalone",
|
||||||
|
"ruleNameDesc": "CNYGS-388pts-Martell + LCH Abalone",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768075,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-martellcny-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768076,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-lchabalone-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 999,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768029,
|
||||||
|
"ruleName": "CNYGS-388pts-Moutai 200ML + $1,000 NP/EB",
|
||||||
|
"ruleNameDesc": "CNYGS-388pts-Moutai 200ML + $1,000 NP/EB",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768077,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-moutai200ml-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768078,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-1000-pc",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768079,
|
||||||
|
"value": "lp-ee-cnygs26-388pts-1000-eb",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 688,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768030,
|
||||||
|
"ruleName": "CNYGS-688pts-Hennessy XO + $2,200 NP/EB",
|
||||||
|
"ruleNameDesc": "CNYGS-688pts-Hennessy XO + $2,200 NP/EB",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768080,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-hennessyxocny-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768081,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-2200-pc",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768082,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-2200-eb",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 688,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768031,
|
||||||
|
"ruleName": "CNYGS-688pts-Moutai 500 ML + LCH Gift Set +Abalone",
|
||||||
|
"ruleNameDesc": "CNYGS-688pts-Moutai 500 ML + LCH Gift Set +Abalone",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768083,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-moutai500ml-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768084,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-lchmushroom-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768085,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-lchabalone-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awardUsedLimit": 0,
|
||||||
|
"awardsCount": 0,
|
||||||
|
"awardsEligible": 0,
|
||||||
|
"awardsEntitled": 0,
|
||||||
|
"awardsRedeemLimit": 688,
|
||||||
|
"earnedPoints": 0,
|
||||||
|
"isDirectOffer": false,
|
||||||
|
"isEligible": true,
|
||||||
|
"isEntitled": false,
|
||||||
|
"isOptIn": false,
|
||||||
|
"isOptedInByPlayer": false,
|
||||||
|
"isRedeemCompleted": false,
|
||||||
|
"maxOutcome": 2,
|
||||||
|
"minOutcome": 2,
|
||||||
|
"startDate": "2026-01-27T06:00",
|
||||||
|
"endDate": "2026-04-30T06:00",
|
||||||
|
"id": 71768004,
|
||||||
|
"code": "LP-EE-CNYGS26",
|
||||||
|
"name": "CNY Gift Set Earn & Get-2026",
|
||||||
|
"ruleId": 71768032,
|
||||||
|
"ruleName": "CNYGS-688pts-Whisky Live + $2,000 NP/EB",
|
||||||
|
"ruleNameDesc": "CNYGS-688pts-Whisky Live + $2,000 NP/EB",
|
||||||
|
"toEarn1Unit": 388,
|
||||||
|
"isRedeemAll": false,
|
||||||
|
"outcomeList": [
|
||||||
|
{
|
||||||
|
"key": 71768086,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-whiskylive-me",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768087,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-2000-pc",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 71768088,
|
||||||
|
"value": "lp-ee-cnygs26-688pts-2000-eb",
|
||||||
|
"isInventoryCheck": null,
|
||||||
|
"qtyOnHand": null,
|
||||||
|
"prize": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entitledPromotionList": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"errorCode": "",
|
||||||
|
"errorMsg": "",
|
||||||
|
"accessToken": "",
|
||||||
|
"refreshToken": "",
|
||||||
|
"period": 3600
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,603 @@
|
|||||||
|
const ldap = require('ldapjs')
|
||||||
|
|
||||||
|
function createMockLdapService({ ldapConfig }) {
|
||||||
|
const {
|
||||||
|
ldapEnabled,
|
||||||
|
ldapHost,
|
||||||
|
ldapPort,
|
||||||
|
ldapBaseDn,
|
||||||
|
ldapManagerDn,
|
||||||
|
ldapGroupBaseDn,
|
||||||
|
ldapManagerPassword,
|
||||||
|
ldapAllowAnyUserMode,
|
||||||
|
ldapAllowAnyUserOu,
|
||||||
|
ldapAllowAnyUserGroupCn,
|
||||||
|
ldapAllowAnyUserMailDomain,
|
||||||
|
ldapForceNoSuchObject,
|
||||||
|
ldapForceNoSuchObjectMatch,
|
||||||
|
} = ldapConfig
|
||||||
|
|
||||||
|
function normalizeDn(value) {
|
||||||
|
const raw = `${value || ''}`.trim()
|
||||||
|
if (!raw) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ldap.parseDN(raw).toString().toLowerCase()
|
||||||
|
} catch (error) {
|
||||||
|
return raw.toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitDnList(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map((entry) => `${entry || ''}`.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.split(';')
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeDnValues(values) {
|
||||||
|
const seen = new Set()
|
||||||
|
const result = []
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
const normalized = normalizeDn(value)
|
||||||
|
if (!normalized || seen.has(normalized)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen.add(normalized)
|
||||||
|
result.push(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCnFromDn(dn) {
|
||||||
|
const input = `${dn || ''}`.trim()
|
||||||
|
if (!input) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstPart = input.split(',')[0] || ''
|
||||||
|
const match = /^cn=([^,]+)$/i.exec(firstPart.trim())
|
||||||
|
return match ? match[1] : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultLdapUser() {
|
||||||
|
const username =
|
||||||
|
process.env.MOCK_LDAP_USER_USERNAME ||
|
||||||
|
process.env.MOCK_LDAP_DEFAULT_USER_SAMACCOUNTNAME ||
|
||||||
|
'mock.user'
|
||||||
|
const dn =
|
||||||
|
process.env.MOCK_LDAP_USER_DN ||
|
||||||
|
process.env.MOCK_LDAP_DEFAULT_USER_DN ||
|
||||||
|
`cn=${username},${ldapAllowAnyUserOu},${ldapBaseDn}`
|
||||||
|
const memberOfRaw =
|
||||||
|
process.env.MOCK_LDAP_USER_MEMBER_OF ||
|
||||||
|
process.env.MOCK_LDAP_DEFAULT_USER_MEMBER_OF ||
|
||||||
|
process.env.MOCK_LDAP_USER_GROUPS ||
|
||||||
|
`cn=${ldapAllowAnyUserGroupCn},${ldapGroupBaseDn}`
|
||||||
|
const memberOf = dedupeDnValues(splitDnList(memberOfRaw))
|
||||||
|
|
||||||
|
return {
|
||||||
|
dn,
|
||||||
|
sAMAccountName: username,
|
||||||
|
cn:
|
||||||
|
process.env.MOCK_LDAP_USER_CN ||
|
||||||
|
process.env.MOCK_LDAP_DEFAULT_USER_CN ||
|
||||||
|
'Mock User',
|
||||||
|
mail:
|
||||||
|
process.env.MOCK_LDAP_USER_MAIL ||
|
||||||
|
process.env.MOCK_LDAP_DEFAULT_USER_MAIL ||
|
||||||
|
'mock.user@macausjm-glp.com',
|
||||||
|
password:
|
||||||
|
process.env.MOCK_LDAP_USER_PASSWORD ||
|
||||||
|
process.env.MOCK_LDAP_DEFAULT_USER_PASSWORD ||
|
||||||
|
'MockUser@123',
|
||||||
|
memberOf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLdapUser(item, fallbackUser) {
|
||||||
|
const username = `${item?.sAMAccountName || item?.username || ''}`.trim()
|
||||||
|
if (!username) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const dn = `${item?.dn || `cn=${username},${ldapAllowAnyUserOu},${ldapBaseDn}`}`.trim()
|
||||||
|
if (!dn) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberOfRaw = item?.memberOf || item?.groups || fallbackUser.memberOf
|
||||||
|
const memberOf = dedupeDnValues(splitDnList(memberOfRaw))
|
||||||
|
|
||||||
|
return {
|
||||||
|
dn,
|
||||||
|
sAMAccountName: username,
|
||||||
|
cn: `${item?.cn || username}`.trim(),
|
||||||
|
mail: `${item?.mail || `${username}@macausjm-glp.com`}`.trim(),
|
||||||
|
password: `${item?.password || fallbackUser.password}`,
|
||||||
|
memberOf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLdapUsers() {
|
||||||
|
const fallbackUser = buildDefaultLdapUser()
|
||||||
|
const rawUsersJson = process.env.MOCK_LDAP_USERS_JSON
|
||||||
|
|
||||||
|
if (!rawUsersJson) {
|
||||||
|
return [fallbackUser]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawUsersJson)
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return [fallbackUser]
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = parsed
|
||||||
|
.map((item) => normalizeLdapUser(item, fallbackUser))
|
||||||
|
.filter((item) => Boolean(item))
|
||||||
|
|
||||||
|
return users.length > 0 ? users : [fallbackUser]
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to parse MOCK_LDAP_USERS_JSON: ${error.message}`)
|
||||||
|
return [fallbackUser]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGroupEntry(entry) {
|
||||||
|
if (!entry || typeof entry !== 'object') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const dn = `${entry.dn || ''}`.trim()
|
||||||
|
if (!dn) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = dedupeDnValues(splitDnList(entry.member || []))
|
||||||
|
const cn = `${entry.cn || extractCnFromDn(dn) || 'UNKNOWN'}`.trim()
|
||||||
|
|
||||||
|
return {
|
||||||
|
dn,
|
||||||
|
cn,
|
||||||
|
member,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLdapGroups(users) {
|
||||||
|
const groupMap = new Map()
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
for (const groupDn of user.memberOf) {
|
||||||
|
const normalizedGroupDn = normalizeDn(groupDn)
|
||||||
|
if (!normalizedGroupDn) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupMap.has(normalizedGroupDn)) {
|
||||||
|
groupMap.set(normalizedGroupDn, {
|
||||||
|
dn: groupDn,
|
||||||
|
cn: extractCnFromDn(groupDn) || 'UNKNOWN',
|
||||||
|
member: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groupMap.get(normalizedGroupDn)
|
||||||
|
if (!group.member.some((value) => normalizeDn(value) === normalizeDn(user.dn))) {
|
||||||
|
group.member.push(user.dn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawGroupsJson = process.env.MOCK_LDAP_GROUPS_JSON
|
||||||
|
if (rawGroupsJson) {
|
||||||
|
try {
|
||||||
|
const parsedGroups = JSON.parse(rawGroupsJson)
|
||||||
|
if (Array.isArray(parsedGroups)) {
|
||||||
|
for (const item of parsedGroups) {
|
||||||
|
const group = normalizeGroupEntry(item)
|
||||||
|
if (!group) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedDn = normalizeDn(group.dn)
|
||||||
|
if (!groupMap.has(normalizedDn)) {
|
||||||
|
groupMap.set(normalizedDn, group)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = groupMap.get(normalizedDn)
|
||||||
|
existing.cn = group.cn || existing.cn
|
||||||
|
existing.member = dedupeDnValues([...existing.member, ...group.member])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to parse MOCK_LDAP_GROUPS_JSON: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...groupMap.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ldapUsers = buildLdapUsers()
|
||||||
|
const ldapGroups = buildLdapGroups(ldapUsers)
|
||||||
|
const ldapUsersByDn = new Map(ldapUsers.map((user) => [normalizeDn(user.dn), user]))
|
||||||
|
const normalizedManagerDn = normalizeDn(ldapManagerDn)
|
||||||
|
|
||||||
|
function getDnDepth(value) {
|
||||||
|
const normalized = normalizeDn(value)
|
||||||
|
if (!normalized) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ldap.parseDN(normalized).rdns.length
|
||||||
|
} catch (error) {
|
||||||
|
return normalized.split(',').length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDnInScope(entryDn, baseDn, scope) {
|
||||||
|
const normalizedEntryDn = normalizeDn(entryDn)
|
||||||
|
const normalizedBaseDn = normalizeDn(baseDn)
|
||||||
|
|
||||||
|
if (!normalizedEntryDn || !normalizedBaseDn) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === 'base') {
|
||||||
|
return normalizedEntryDn === normalizedBaseDn
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === 'one') {
|
||||||
|
if (!normalizedEntryDn.endsWith(`,${normalizedBaseDn}`)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDnDepth(normalizedEntryDn) === getDnDepth(normalizedBaseDn) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
normalizedEntryDn === normalizedBaseDn ||
|
||||||
|
normalizedEntryDn.endsWith(`,${normalizedBaseDn}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSearchBase(searchBase) {
|
||||||
|
const raw = `${searchBase || ''}`.trim()
|
||||||
|
if (!raw) {
|
||||||
|
return ldapBaseDn
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedRaw = normalizeDn(raw)
|
||||||
|
const normalizedBase = normalizeDn(ldapBaseDn)
|
||||||
|
|
||||||
|
if (!normalizedRaw) {
|
||||||
|
return ldapBaseDn
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedRaw === normalizedBase || normalizedRaw.endsWith(`,${normalizedBase}`)) {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/dc=/i.test(raw)) {
|
||||||
|
return `${raw},${ldapBaseDn}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUserAttributes(user) {
|
||||||
|
return {
|
||||||
|
objectClass: ['top', 'person', 'organizationalPerson', 'user'],
|
||||||
|
distinguishedName: user.dn,
|
||||||
|
sAMAccountName: user.sAMAccountName,
|
||||||
|
employeeId: '123456789',
|
||||||
|
cn: user.cn,
|
||||||
|
mail: user.mail,
|
||||||
|
memberOf: user.memberOf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGroupAttributes(group) {
|
||||||
|
return {
|
||||||
|
objectClass: ['top', 'group'],
|
||||||
|
distinguishedName: group.dn,
|
||||||
|
cn: group.cn,
|
||||||
|
member: group.member,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilter(filter, attributes) {
|
||||||
|
try {
|
||||||
|
return filter.matches(attributes)
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeStringValues(values) {
|
||||||
|
const seen = new Set()
|
||||||
|
const result = []
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
const normalized = `${value || ''}`.trim().toLowerCase()
|
||||||
|
if (!normalized || seen.has(normalized)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen.add(normalized)
|
||||||
|
result.push(`${value || ''}`.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractEqFilterValues(filter, attributeName, result = []) {
|
||||||
|
if (!filter || typeof filter !== 'object') {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const attr = `${filter.attribute || ''}`.trim().toLowerCase()
|
||||||
|
if (attr === `${attributeName}`.toLowerCase()) {
|
||||||
|
const rawValue = filter.value
|
||||||
|
const value = Buffer.isBuffer(rawValue)
|
||||||
|
? rawValue.toString('utf8').trim()
|
||||||
|
: `${rawValue || ''}`.trim()
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
result.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter.filters)) {
|
||||||
|
for (const childFilter of filter.filters) {
|
||||||
|
extractEqFilterValues(childFilter, attributeName, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeSamAccountName(value) {
|
||||||
|
return `${value || ''}`
|
||||||
|
.trim()
|
||||||
|
.replace(/[\0-\x1f\x7f,=+<>#;"\\]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAnyModeUser(username) {
|
||||||
|
const normalizedUsername = sanitizeSamAccountName(username)
|
||||||
|
if (!normalizedUsername) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultGroupDn = `cn=${ldapAllowAnyUserGroupCn},${ldapGroupBaseDn}`
|
||||||
|
const configuredGroupDns = ldapGroups.map((group) => group.dn)
|
||||||
|
const memberOf = dedupeDnValues(
|
||||||
|
configuredGroupDns.length > 0 ? configuredGroupDns : [defaultGroupDn],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
dn: `cn=${normalizedUsername},${ldapAllowAnyUserOu},${ldapBaseDn}`,
|
||||||
|
sAMAccountName: normalizedUsername,
|
||||||
|
cn: normalizedUsername,
|
||||||
|
mail: `${normalizedUsername}@${ldapAllowAnyUserMailDomain}`,
|
||||||
|
memberOf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAnyModeGroups(memberDn) {
|
||||||
|
const normalizedMemberDn = `${memberDn || ''}`.trim()
|
||||||
|
if (!normalizedMemberDn) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ldapGroups.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
dn: `cn=${ldapAllowAnyUserGroupCn},${ldapGroupBaseDn}`,
|
||||||
|
cn: ldapAllowAnyUserGroupCn,
|
||||||
|
member: [normalizedMemberDn],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ldapGroups.map((group) => ({
|
||||||
|
dn: group.dn,
|
||||||
|
cn: group.cn,
|
||||||
|
member: [normalizedMemberDn],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLdapBind(req, res, next) {
|
||||||
|
const bindDn = req.dn?.toString() || ''
|
||||||
|
const normalizedBindDn = normalizeDn(bindDn)
|
||||||
|
const credentials = `${req.credentials || ''}`
|
||||||
|
|
||||||
|
if (!normalizedBindDn) {
|
||||||
|
return next(new ldap.InvalidCredentialsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ldapAllowAnyUserMode) {
|
||||||
|
res.end()
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedBindDn === normalizedManagerDn) {
|
||||||
|
if (!ldapManagerPassword || credentials === ldapManagerPassword) {
|
||||||
|
res.end()
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
return next(new ldap.InvalidCredentialsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = ldapUsersByDn.get(normalizedBindDn)
|
||||||
|
if (!user || credentials !== `${user.password}`) {
|
||||||
|
return next(new ldap.InvalidCredentialsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end()
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLdapSearch(req, res, next) {
|
||||||
|
const searchBase = resolveSearchBase(req.dn?.toString())
|
||||||
|
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) {
|
||||||
|
let hasAnyModeResult = false
|
||||||
|
const usernames = dedupeStringValues(extractEqFilterValues(req.filter, 'sAMAccountName'))
|
||||||
|
const memberDns = dedupeDnValues(extractEqFilterValues(req.filter, 'member'))
|
||||||
|
|
||||||
|
for (const username of usernames) {
|
||||||
|
const anyModeUser = buildAnyModeUser(username)
|
||||||
|
if (!anyModeUser || !isDnInScope(anyModeUser.dn, searchBase, scope)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = buildUserAttributes(anyModeUser)
|
||||||
|
if (!matchesFilter(req.filter, attributes)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAnyModeResult = true
|
||||||
|
res.send({
|
||||||
|
dn: anyModeUser.dn,
|
||||||
|
attributes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const memberDn of memberDns) {
|
||||||
|
for (const anyModeGroup of buildAnyModeGroups(memberDn)) {
|
||||||
|
if (!isDnInScope(anyModeGroup.dn, searchBase, scope)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = buildGroupAttributes(anyModeGroup)
|
||||||
|
if (!matchesFilter(req.filter, attributes)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAnyModeResult = true
|
||||||
|
res.send({
|
||||||
|
dn: anyModeGroup.dn,
|
||||||
|
attributes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAnyModeResult) {
|
||||||
|
res.end()
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const user of ldapUsers) {
|
||||||
|
if (!isDnInScope(user.dn, searchBase, scope)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = buildUserAttributes(user)
|
||||||
|
if (matchesFilter(req.filter, attributes)) {
|
||||||
|
res.send({
|
||||||
|
dn: user.dn,
|
||||||
|
attributes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const group of ldapGroups) {
|
||||||
|
if (!isDnInScope(group.dn, searchBase, scope)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = buildGroupAttributes(group)
|
||||||
|
if (matchesFilter(req.filter, attributes)) {
|
||||||
|
res.send({
|
||||||
|
dn: group.dn,
|
||||||
|
attributes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end()
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
let ldapServer = null
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (!ldapEnabled) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ldapServer) {
|
||||||
|
return ldapServer
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapServer = ldap.createServer()
|
||||||
|
ldapServer.bind(ldapBaseDn, handleLdapBind)
|
||||||
|
ldapServer.bind('', handleLdapBind)
|
||||||
|
ldapServer.search(ldapBaseDn, handleLdapSearch)
|
||||||
|
ldapServer.search('', handleLdapSearch)
|
||||||
|
|
||||||
|
ldapServer.on('error', (error) => {
|
||||||
|
console.error(`LDAP server error: ${error.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
ldapServer.listen(ldapPort, ldapHost, () => {
|
||||||
|
console.log(
|
||||||
|
`Mock LDAP listening on ldap://${ldapHost}:${ldapPort} (base=${ldapBaseDn}, users=${ldapUsers.length}, groups=${ldapGroups.length})`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return ldapServer
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHealthInfo() {
|
||||||
|
return {
|
||||||
|
enabled: ldapEnabled,
|
||||||
|
allowAnyUserMode: ldapAllowAnyUserMode,
|
||||||
|
url: `ldap://${ldapHost}:${ldapPort}`,
|
||||||
|
managerDn: ldapManagerDn,
|
||||||
|
baseDn: ldapBaseDn,
|
||||||
|
users: ldapUsers.map((user) => ({
|
||||||
|
sAMAccountName: user.sAMAccountName,
|
||||||
|
dn: user.dn,
|
||||||
|
})),
|
||||||
|
groups: ldapGroups.map((group) => ({
|
||||||
|
cn: group.cn,
|
||||||
|
dn: group.dn,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
getHealthInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createMockLdapService }
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const LOG_FILE = path.join(__dirname, '..', 'mock_gateway.log')
|
||||||
|
|
||||||
|
function ts() {
|
||||||
|
return new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeToFile(line) {
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(LOG_FILE, line + '\n', 'utf8')
|
||||||
|
} catch (_) {
|
||||||
|
// ignore file write errors so logging never breaks the gateway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified request logger. Prints to console and appends to mock_gateway.log.
|
||||||
|
*/
|
||||||
|
function log(...args) {
|
||||||
|
const line = `[${ts()}] ${args
|
||||||
|
.map((a) => (typeof a === 'string' ? a : JSON.stringify(a)))
|
||||||
|
.join(' ')}`
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(line)
|
||||||
|
writeToFile(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express middleware that logs every incoming request (method, path, query, body size).
|
||||||
|
*/
|
||||||
|
function requestLogger(req, res, next) {
|
||||||
|
const start = Date.now()
|
||||||
|
const accountNum =
|
||||||
|
req.body?.data?.accountNum || req.body?.accountNum || req.query?.accountNum || '-'
|
||||||
|
log(
|
||||||
|
`[REQ] ${req.method} ${req.path} account=${accountNum} bodyBytes=${req.headers['content-length'] || 0}`,
|
||||||
|
)
|
||||||
|
res.on('finish', () => {
|
||||||
|
log(
|
||||||
|
`[RES] ${req.method} ${req.path} status=${res.statusCode} tookMs=${Date.now() - start}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { log, requestLogger, LOG_FILE }
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
function registerPrizeRedemptionsSyncRoute({ app, context }) {
|
||||||
|
app.post('/api/v1/prize-redemptions/sync', (req, res) => {
|
||||||
|
const correlationId =
|
||||||
|
req.body?.data?.correlationId || req.body?.correlationId || req.body?.schema?.correlationId || ''
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: context.prizeRedemptionsSyncSuccess,
|
||||||
|
message: context.prizeRedemptionsSyncMessage,
|
||||||
|
data: {
|
||||||
|
correlationId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPrizeRedemptionsSyncRoute
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
function registerEdipTokenGetRoute({ app, context }) {
|
||||||
|
app.post('/edip/v1/token/get', (req, res) => {
|
||||||
|
if (context.edipForceErrorCode) {
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: context.edipForceErrorCode,
|
||||||
|
errorMsg: context.edipForceErrorMsg,
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
period: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json(context.tokenSuccessResponse())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerEdipTokenGetRoute
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
function registerEdipTokenRefreshRoute({ app, context }) {
|
||||||
|
app.post('/edip/v1/token/refresh', (req, res) => {
|
||||||
|
const refreshToken = req.body?.data?.refreshToken
|
||||||
|
if (!refreshToken) {
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: 'MISSING_REFRESH_TOKEN',
|
||||||
|
errorMsg: 'refreshToken is required',
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
period: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.edipRefreshTokenMustExist && !context.issuedRefreshTokens.has(refreshToken)) {
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: 'INVALID_REFRESH_TOKEN',
|
||||||
|
errorMsg: 'refreshToken not recognized by mock gateway',
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
period: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json(context.tokenSuccessResponse())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerEdipTokenRefreshRoute
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
function registerCardpinValidateRoute({ app, context }) {
|
||||||
|
app.post('/ewl/bal/v1/cardpin/validate', (req, res) => {
|
||||||
|
const accountNum = context.resolveAccountNum(req.body)
|
||||||
|
const pin = `${req.body?.data?.pin || ''}`
|
||||||
|
|
||||||
|
if (context.failPins.has(pin)) {
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: 'INVALID_PIN',
|
||||||
|
errorMsg: 'Mock validation failed',
|
||||||
|
accountNum,
|
||||||
|
result: 'FAIL',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: '',
|
||||||
|
errorMsg: '',
|
||||||
|
accountNum,
|
||||||
|
result: context.defaultValidateResult,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerCardpinValidateRoute
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
function registerPlayerEarnRoute({ app, context }) {
|
||||||
|
app.post('/ewl/bal/v1/player/earn', (req, res) => {
|
||||||
|
const accountNum = context.resolveAccountNum(req.body)
|
||||||
|
const casinoCode = req.body?.data?.casinoCode || 'GLP'
|
||||||
|
const points = Number(process.env.MOCK_PLAYER_EARN_POINTS || 0)
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: '',
|
||||||
|
errorMsg: '',
|
||||||
|
accountNum,
|
||||||
|
playerEarn: [
|
||||||
|
{
|
||||||
|
casinoCode,
|
||||||
|
points,
|
||||||
|
comps: 0,
|
||||||
|
ebonus: 0,
|
||||||
|
stubs: 0,
|
||||||
|
tierPoints: 0,
|
||||||
|
mgmtComp: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ratingSum: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPlayerEarnRoute
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
function registerPlayerImageRoute({ app, context }) {
|
||||||
|
app.post('/ewl/bal/v1/player/image', (req, res) => {
|
||||||
|
const accountNum = context.resolveAccountNum(req.body)
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: '',
|
||||||
|
errorMsg: '',
|
||||||
|
accountNum,
|
||||||
|
image: context.mockProfile.image,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPlayerImageRoute
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const {
|
||||||
|
toIntegerOrDefault,
|
||||||
|
resolveAccountNum,
|
||||||
|
buildPlayerProfile,
|
||||||
|
} = require('./playerPromotionFulfillmentJourneyShared')
|
||||||
|
|
||||||
|
function buildFulfillmentJourneyResponse({ context, requestBody = {} }) {
|
||||||
|
const requestData = requestBody?.data || {}
|
||||||
|
const accountNum = resolveAccountNum(context, requestData, requestBody)
|
||||||
|
const ruleId = toIntegerOrDefault(requestData.ruleId, 71768024)
|
||||||
|
const ruleName = requestData.ruleName || 'Super Flash 2026'
|
||||||
|
const isEmptyResult = context.playerPromotionFulfillmentJourneyEmptyResult
|
||||||
|
|
||||||
|
return {
|
||||||
|
errorCode: context.playerPromotionFulfillmentJourneyErrorCode,
|
||||||
|
errorMsg: context.playerPromotionFulfillmentJourneyErrorMsg,
|
||||||
|
accountNum,
|
||||||
|
ruleId,
|
||||||
|
playerProfile: buildPlayerProfile(context, accountNum),
|
||||||
|
eligibleCriteriaList: isEmptyResult
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
attribute: 'Tag',
|
||||||
|
operator: 'IN',
|
||||||
|
value: 'Tag 01',
|
||||||
|
valueArray: ['Tag 01'],
|
||||||
|
qualify: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attribute: 'Tag',
|
||||||
|
operator: 'NOT IN',
|
||||||
|
value: 'Tag 02',
|
||||||
|
valueArray: ['Tag 02'],
|
||||||
|
qualify: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
journeyList: isEmptyResult
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
progressId: 1,
|
||||||
|
memberNo: accountNum,
|
||||||
|
ruleId,
|
||||||
|
ruleName,
|
||||||
|
periodStartTime: '2026-04-17T00:00:00+08:00',
|
||||||
|
periodEndTime: '2026-04-18T23:59:59+08:00',
|
||||||
|
basePts: 3000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
progressId: 2,
|
||||||
|
memberNo: accountNum,
|
||||||
|
ruleId: 695,
|
||||||
|
ruleName: 'QA-TESTING',
|
||||||
|
periodStartTime: '2026-04-17T00:00:00+08:00',
|
||||||
|
periodEndTime: '2026-04-18T23:59:59+08:00',
|
||||||
|
basePts: -5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
progressId: 3,
|
||||||
|
memberNo: accountNum,
|
||||||
|
ruleId: 701,
|
||||||
|
ruleName: 'QA-REGRESSION-TEST',
|
||||||
|
periodStartTime: '2026-04-17T00:00:00+08:00',
|
||||||
|
periodEndTime: '2026-04-18T23:59:59+08:00',
|
||||||
|
basePts: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerPlayerPromotionFulfillmentJourneyRoute({ app, context }) {
|
||||||
|
app.post(
|
||||||
|
[
|
||||||
|
'/ewl/promotion/v1/playerpromotion/fulfillmentJourney',
|
||||||
|
'/ewl/bal/v1/playerpromotion/fulfillmentJourney',
|
||||||
|
],
|
||||||
|
(req, res) =>
|
||||||
|
res.status(200).json(
|
||||||
|
buildFulfillmentJourneyResponse({
|
||||||
|
context,
|
||||||
|
requestBody: req.body,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPlayerPromotionFulfillmentJourneyRoute
|
||||||
|
module.exports.__internal = {
|
||||||
|
buildFulfillmentJourneyResponse,
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
const {
|
||||||
|
toIntegerOrDefault,
|
||||||
|
resolveAccountNum,
|
||||||
|
buildPlayerProfile,
|
||||||
|
} = require('./playerPromotionFulfillmentJourneyShared')
|
||||||
|
|
||||||
|
function buildFulfillmentJourneyDetailResponse({ context, requestBody = {} }) {
|
||||||
|
const requestData = requestBody?.data || {}
|
||||||
|
const accountNum = resolveAccountNum(context, requestData, requestBody)
|
||||||
|
const ruleId = toIntegerOrDefault(requestData.ruleId, 71768024)
|
||||||
|
const isEmptyResult = context.playerPromotionFulfillmentJourneyDetailEmptyResult
|
||||||
|
|
||||||
|
return {
|
||||||
|
errorCode: context.playerPromotionFulfillmentJourneyDetailErrorCode,
|
||||||
|
errorMsg: context.playerPromotionFulfillmentJourneyDetailErrorMsg,
|
||||||
|
accountNum,
|
||||||
|
ruleId,
|
||||||
|
playerProfile: buildPlayerProfile(context, accountNum),
|
||||||
|
journeyList: isEmptyResult
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
progressId: 10,
|
||||||
|
memberNo: accountNum,
|
||||||
|
postDtm: '2026-04-17T18:40:48+08:00',
|
||||||
|
gamingDt: '2026-04-17',
|
||||||
|
earnType: 'EARN',
|
||||||
|
ballyTranId: 'earn-001',
|
||||||
|
ruleId,
|
||||||
|
ruleName: 'Super Flash 2026',
|
||||||
|
periodStartTime: '2026-04-17T00:00:00+08:00',
|
||||||
|
periodEndTime: '2026-04-18T23:59:59+08:00',
|
||||||
|
basePts: 251,
|
||||||
|
deptCode: 'SLOT',
|
||||||
|
locnCode: 'P00001',
|
||||||
|
gameCode: 'GLPSlot',
|
||||||
|
areaCode: 'VID',
|
||||||
|
casinoCode: 'GLP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
progressId: 11,
|
||||||
|
memberNo: accountNum,
|
||||||
|
postDtm: '2026-04-17T20:26:13+08:00',
|
||||||
|
gamingDt: '2026-04-17',
|
||||||
|
earnType: 'EARN',
|
||||||
|
ballyTranId: 'earn-002',
|
||||||
|
ruleId,
|
||||||
|
ruleName: 'Super Flash 2026',
|
||||||
|
periodStartTime: '2026-04-17T00:00:00+08:00',
|
||||||
|
periodEndTime: '2026-04-18T23:59:59+08:00',
|
||||||
|
basePts: 149,
|
||||||
|
deptCode: 'SLOT',
|
||||||
|
locnCode: 'P00001',
|
||||||
|
gameCode: 'GLPSlot',
|
||||||
|
areaCode: 'VID',
|
||||||
|
casinoCode: 'GLP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
progressId: 12,
|
||||||
|
memberNo: accountNum,
|
||||||
|
postDtm: '2026-04-17T22:19:47+08:00',
|
||||||
|
gamingDt: '2026-04-17',
|
||||||
|
earnType: 'EARN',
|
||||||
|
ballyTranId: 'earn-003',
|
||||||
|
ruleId,
|
||||||
|
ruleName: 'Super Flash 2026',
|
||||||
|
periodStartTime: '2026-04-17T00:00:00+08:00',
|
||||||
|
periodEndTime: '2026-04-18T23:59:59+08:00',
|
||||||
|
basePts: 144,
|
||||||
|
deptCode: 'SLOT',
|
||||||
|
locnCode: 'P00001',
|
||||||
|
gameCode: 'GLPSlot',
|
||||||
|
areaCode: 'VID',
|
||||||
|
casinoCode: 'GLP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
progressId: 13,
|
||||||
|
memberNo: accountNum,
|
||||||
|
postDtm: '2026-04-18T01:38:00+08:00',
|
||||||
|
gamingDt: '2026-04-18',
|
||||||
|
earnType: 'BURN',
|
||||||
|
ballyTranId: 'burn-001',
|
||||||
|
ruleId,
|
||||||
|
ruleName: 'Super Flash 2026',
|
||||||
|
periodStartTime: '2026-04-17T00:00:00+08:00',
|
||||||
|
periodEndTime: '2026-04-18T23:59:59+08:00',
|
||||||
|
basePts: -60,
|
||||||
|
deptCode: 'SLOT',
|
||||||
|
locnCode: 'P00001',
|
||||||
|
gameCode: 'GLPSlot',
|
||||||
|
areaCode: 'VID',
|
||||||
|
casinoCode: 'GLP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
progressId: 14,
|
||||||
|
memberNo: accountNum,
|
||||||
|
postDtm: '2026-04-18T01:38:12+08:00',
|
||||||
|
gamingDt: '2026-04-18',
|
||||||
|
earnType: 'BURN',
|
||||||
|
ballyTranId: 'burn-002',
|
||||||
|
ruleId,
|
||||||
|
ruleName: 'Super Flash 2026',
|
||||||
|
periodStartTime: '2026-04-17T00:00:00+08:00',
|
||||||
|
periodEndTime: '2026-04-18T23:59:59+08:00',
|
||||||
|
basePts: -144,
|
||||||
|
deptCode: 'SLOT',
|
||||||
|
locnCode: 'P00001',
|
||||||
|
gameCode: 'GLPSlot',
|
||||||
|
areaCode: 'VID',
|
||||||
|
casinoCode: 'GLP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
progressId: 15,
|
||||||
|
memberNo: accountNum,
|
||||||
|
postDtm: '2026-04-18T01:38:20+08:00',
|
||||||
|
gamingDt: '2026-04-18',
|
||||||
|
earnType: 'BURN',
|
||||||
|
ballyTranId: 'burn-003',
|
||||||
|
ruleId,
|
||||||
|
ruleName: 'WEG 10 Pts',
|
||||||
|
periodStartTime: '2026-04-17T00:00:00+08:00',
|
||||||
|
periodEndTime: '2026-04-18T23:59:59+08:00',
|
||||||
|
basePts: -35,
|
||||||
|
deptCode: 'SLOT',
|
||||||
|
locnCode: 'P00001',
|
||||||
|
gameCode: 'GLPSlot',
|
||||||
|
areaCode: 'VID',
|
||||||
|
casinoCode: 'GLP',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerPlayerPromotionFulfillmentJourneyDetailRoute({ app, context }) {
|
||||||
|
app.post(
|
||||||
|
[
|
||||||
|
'/ewl/promotion/v1/playerpromotion/fulfillmentJourneyDetail',
|
||||||
|
'/ewl/bal/v1/playerpromotion/fulfillmentJourneyDetail',
|
||||||
|
],
|
||||||
|
(req, res) =>
|
||||||
|
res.status(200).json(
|
||||||
|
buildFulfillmentJourneyDetailResponse({
|
||||||
|
context,
|
||||||
|
requestBody: req.body,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPlayerPromotionFulfillmentJourneyDetailRoute
|
||||||
|
module.exports.__internal = {
|
||||||
|
buildFulfillmentJourneyDetailResponse,
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
function toIntegerOrDefault(value, fallbackValue) {
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isInteger(parsed) ? parsed : fallbackValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAccountNum(context, requestData, requestBody) {
|
||||||
|
const resolvedByContext =
|
||||||
|
typeof context.resolveAccountNum === 'function' ? context.resolveAccountNum(requestBody) : ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
requestData.accountNum ||
|
||||||
|
requestData.playerId ||
|
||||||
|
resolvedByContext ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPlayerProfile(context, accountNum) {
|
||||||
|
const profile = context.getMockProfileForAccount(accountNum)
|
||||||
|
|
||||||
|
return {
|
||||||
|
clubState: 'ACTIVE',
|
||||||
|
clubLevelName: profile?.tier || 'MOCK',
|
||||||
|
setupCasinoCode: 'GLP',
|
||||||
|
signUpDate: '2024-02-16',
|
||||||
|
significanceDate: '2025-01-01',
|
||||||
|
countryCode: 'MO',
|
||||||
|
tagArray: ['IN Tag 01', 'NOT IN Tag 02'],
|
||||||
|
xdaysSignOn: 180,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
toIntegerOrDefault,
|
||||||
|
resolveAccountNum,
|
||||||
|
buildPlayerProfile,
|
||||||
|
}
|
||||||
@@ -0,0 +1,643 @@
|
|||||||
|
const EWALLET_HTTP_ERROR = 'EWALLET_HTTP_ERROR'
|
||||||
|
const ISSUE_TARGET_TO_WALLET_HEADER = 'x-ewallet-mock-issue-target-to-wallet-by-outcome-id'
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHttpErrorMessage(method, url, status, statusText, rawBody) {
|
||||||
|
const responseText = rawBody ? `: "${rawBody}"` : ''
|
||||||
|
return `${status} ${statusText} on ${method} request for "${url}"${responseText}`
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
buildHttpErrorMessage(options.method || 'GET', url, response.status, response.statusText, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNullableString(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = String(value).trim()
|
||||||
|
return text ? text : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNullableBoolean(value) {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalizedValue = value.trim().toLowerCase()
|
||||||
|
if (['true', '1', 'yes', 'y'].includes(normalizedValue)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (['false', '0', 'no', 'n'].includes(normalizedValue)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIssueTargetToWalletHeader(headerValue) {
|
||||||
|
const normalizedHeaderValue = toNullableString(headerValue)
|
||||||
|
if (!normalizedHeaderValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedHeaderValue
|
||||||
|
try {
|
||||||
|
parsedHeaderValue = JSON.parse(normalizedHeaderValue)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid ${ISSUE_TARGET_TO_WALLET_HEADER} header JSON: ${normalizedHeaderValue}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedHeaderValue || typeof parsedHeaderValue !== 'object' || Array.isArray(parsedHeaderValue)) {
|
||||||
|
throw new Error(`Invalid ${ISSUE_TARGET_TO_WALLET_HEADER} header payload`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedOverrides = {}
|
||||||
|
for (const [outcomeId, rawValue] of Object.entries(parsedHeaderValue)) {
|
||||||
|
const normalizedValue = toNullableBoolean(rawValue)
|
||||||
|
if (normalizedValue === null) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid ${ISSUE_TARGET_TO_WALLET_HEADER} header value for outcomeId=${outcomeId}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
normalizedOverrides[outcomeId] = normalizedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedOverrides
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeIssueTargetToWalletOverrides(requestBody, headers) {
|
||||||
|
const requestData = requestBody?.data || {}
|
||||||
|
const headerOverrides = parseIssueTargetToWalletHeader(headers?.[ISSUE_TARGET_TO_WALLET_HEADER])
|
||||||
|
|
||||||
|
if (!headerOverrides) {
|
||||||
|
return requestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOverrides =
|
||||||
|
requestData.issueTargetToWalletByOutcomeId &&
|
||||||
|
typeof requestData.issueTargetToWalletByOutcomeId === 'object'
|
||||||
|
? requestData.issueTargetToWalletByOutcomeId
|
||||||
|
: {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(requestBody || {}),
|
||||||
|
data: {
|
||||||
|
...requestData,
|
||||||
|
issueTargetToWalletByOutcomeId: {
|
||||||
|
...headerOverrides,
|
||||||
|
...requestOverrides,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeRuntimeIssueTargetToWalletOverrides(requestBody, context, accountNum) {
|
||||||
|
const requestData = requestBody?.data || {}
|
||||||
|
const runtimeOverrides = context.resolvePromotionRedeemFulfillmentOverrideByOutcomeId({
|
||||||
|
accountNum,
|
||||||
|
promotionId: requestData.promotionId,
|
||||||
|
promotionRuleId: requestData.promotionRuleId,
|
||||||
|
outcomeIds: requestData.outcomeId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!runtimeOverrides || Object.keys(runtimeOverrides).length === 0) {
|
||||||
|
return requestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOverrides =
|
||||||
|
requestData.issueTargetToWalletByOutcomeId &&
|
||||||
|
typeof requestData.issueTargetToWalletByOutcomeId === 'object'
|
||||||
|
? requestData.issueTargetToWalletByOutcomeId
|
||||||
|
: {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(requestBody || {}),
|
||||||
|
data: {
|
||||||
|
...requestData,
|
||||||
|
issueTargetToWalletByOutcomeId: {
|
||||||
|
...runtimeOverrides,
|
||||||
|
...requestOverrides,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveIssueTargetToWalletOverride(requestData, outcome, index) {
|
||||||
|
const requestIssueTargetToWalletByOutcomeId = requestData?.issueTargetToWalletByOutcomeId
|
||||||
|
const outcomeId = outcome?.key
|
||||||
|
|
||||||
|
if (
|
||||||
|
requestIssueTargetToWalletByOutcomeId &&
|
||||||
|
typeof requestIssueTargetToWalletByOutcomeId === 'object' &&
|
||||||
|
outcomeId !== undefined &&
|
||||||
|
outcomeId !== null
|
||||||
|
) {
|
||||||
|
const overrideByOutcomeId = toNullableBoolean(requestIssueTargetToWalletByOutcomeId[String(outcomeId)])
|
||||||
|
if (overrideByOutcomeId !== null) {
|
||||||
|
return overrideByOutcomeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(requestData?.issueTargetToWallet) && requestData.issueTargetToWallet.length > index) {
|
||||||
|
const overrideByIndex = toNullableBoolean(requestData.issueTargetToWallet[index])
|
||||||
|
if (overrideByIndex !== null) {
|
||||||
|
return overrideByIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outcomeOverride = toNullableBoolean(outcome?.issueTargetToWallet)
|
||||||
|
if (outcomeOverride !== null) {
|
||||||
|
return outcomeOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockOutcomeOverride = toNullableBoolean(outcome?.mockIssueTargetToWallet)
|
||||||
|
if (mockOutcomeOverride !== null) {
|
||||||
|
return mockOutcomeOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
const prizeOverride = toNullableBoolean(outcome?.prize?.issueTargetToWallet)
|
||||||
|
if (prizeOverride !== null) {
|
||||||
|
return prizeOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockPrizeOverride = toNullableBoolean(outcome?.prize?.mockIssueTargetToWallet)
|
||||||
|
if (mockPrizeOverride !== null) {
|
||||||
|
return mockPrizeOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveIssueTargetToWallet({ requestData, outcome, index, prizeIdentifier }) {
|
||||||
|
const explicitOverride = resolveIssueTargetToWalletOverride(requestData, outcome, index)
|
||||||
|
if (explicitOverride !== null) {
|
||||||
|
return explicitOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toNullableString(prizeIdentifier)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const prize = outcome?.prize || {}
|
||||||
|
const deliveryMode = prize?.behaviorProfile?.deliveryMode || prize?.deliveryMode || ''
|
||||||
|
|
||||||
|
if (deliveryMode === 'EXTERNAL_ONLY') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deliveryMode === 'WALLET_ONLY') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTargetPrizeValue(prize, sourceBucket, promotion) {
|
||||||
|
const bucketPrice = prize?.bucketPrices?.[sourceBucket]
|
||||||
|
if (Number.isFinite(bucketPrice)) {
|
||||||
|
return bucketPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the source bucket price is absent, leave the value unspecified (null)
|
||||||
|
// so the redemption payload sends valueUsed: null and eWallet computes it
|
||||||
|
// from its own configuration (instead of falling back to prizeValue).
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPromotionMetadata(requestData, promotion, redemptionId) {
|
||||||
|
return {
|
||||||
|
rule_id: requestData.promotionRuleId,
|
||||||
|
promotion_type: requestData.isDirectOffer ? 'direct_offer' : 'promotion',
|
||||||
|
promotion_name: requestData.promotionName || promotion?.name || '',
|
||||||
|
promotion_code: promotion?.code || '',
|
||||||
|
rule_code: promotion?.ruleName || '',
|
||||||
|
promotion_id: requestData.promotionId,
|
||||||
|
redemption_id: redemptionId,
|
||||||
|
__policy_segments__: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRedemptionPayload({
|
||||||
|
context,
|
||||||
|
requestBody,
|
||||||
|
accountNum,
|
||||||
|
promotion,
|
||||||
|
selectedOutcomes,
|
||||||
|
promotionRedemptionId,
|
||||||
|
}) {
|
||||||
|
const requestData = requestBody?.data || {}
|
||||||
|
const requestSchema = requestBody?.schema || {}
|
||||||
|
const quantity = Number.isFinite(Number(requestData.quantity)) && Number(requestData.quantity) > 0
|
||||||
|
? Number(requestData.quantity)
|
||||||
|
: 1
|
||||||
|
const sourceBucket = context.promotionRedemptionSourceBucket
|
||||||
|
|
||||||
|
const targetPrizes = selectedOutcomes.map((outcome, index) => {
|
||||||
|
const prize = outcome?.prize || {}
|
||||||
|
const prizeIdentifier =
|
||||||
|
Array.isArray(requestData.gisPrizeIdentifier) && requestData.gisPrizeIdentifier.length > index
|
||||||
|
? requestData.gisPrizeIdentifier[index]
|
||||||
|
: null
|
||||||
|
|
||||||
|
return {
|
||||||
|
targetPrizeCode: prize?.prizeCode || outcome?.value || '',
|
||||||
|
amount: quantity,
|
||||||
|
prizeIdentifier: toNullableString(prizeIdentifier),
|
||||||
|
issueTargetToWallet: resolveIssueTargetToWallet({
|
||||||
|
requestData,
|
||||||
|
outcome,
|
||||||
|
index,
|
||||||
|
prizeIdentifier,
|
||||||
|
}),
|
||||||
|
visibleFrom: null,
|
||||||
|
validFrom: null,
|
||||||
|
expiryDate: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalTargetValue = targetPrizes.reduce((sum, targetPrize, index) => {
|
||||||
|
const prize = selectedOutcomes[index]?.prize || {}
|
||||||
|
return sum + resolveTargetPrizeValue(prize, sourceBucket, promotion) * targetPrize.amount
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
correlationId: requestSchema.transactionId || context.generateToken('mock_promotion_redeem'),
|
||||||
|
redemptionType: 'PRIZE_EXCHANGE',
|
||||||
|
metadata: buildPromotionMetadata(requestData, promotion, promotionRedemptionId),
|
||||||
|
customerId: accountNum,
|
||||||
|
customerType: 'MEMBER',
|
||||||
|
targetPrizes,
|
||||||
|
initiateType: 'API',
|
||||||
|
initiateSource: context.promotionRedemptionInitiateSource,
|
||||||
|
initiateIdentifier: requestData.loginId || '',
|
||||||
|
note: requestData.remark || null,
|
||||||
|
sourceWalletItems: [
|
||||||
|
{
|
||||||
|
sourceWalletItemBucket: sourceBucket,
|
||||||
|
valueUsed: totalTargetValue > 0 ? totalTargetValue : null,
|
||||||
|
redemptionRule: 'FEFO',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRecordedRedeemedOutcome({
|
||||||
|
requestBody,
|
||||||
|
promotion,
|
||||||
|
selectedOutcomes,
|
||||||
|
promotionRedemptionId,
|
||||||
|
redemptionPublicId,
|
||||||
|
}) {
|
||||||
|
const requestData = requestBody?.data || {}
|
||||||
|
const requestSchema = requestBody?.schema || {}
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
const gamingDate = now.slice(0, 10)
|
||||||
|
|
||||||
|
return {
|
||||||
|
gamingDate,
|
||||||
|
normalPromo: !Boolean(requestData.isDirectOffer),
|
||||||
|
postedBy: requestData.loginId || 'mock.user',
|
||||||
|
postedDtm: now,
|
||||||
|
casinoCode: requestData.casinoCode || 'GLP',
|
||||||
|
locationCode: requestData.locationCode || 'MOCK',
|
||||||
|
computerName: requestSchema.computerName || 'mock-gateway',
|
||||||
|
ipAddress: requestSchema.ipAddress || '',
|
||||||
|
prizeRuleCode: promotion?.ruleName || '',
|
||||||
|
prizeRuleId: requestData.promotionRuleId,
|
||||||
|
promotionCode: promotion?.code || '',
|
||||||
|
promotionId: requestData.promotionId,
|
||||||
|
redeemedQty:
|
||||||
|
Number.isFinite(Number(requestData.quantity)) && Number(requestData.quantity) > 0
|
||||||
|
? Number(requestData.quantity)
|
||||||
|
: 1,
|
||||||
|
redemptionId: promotionRedemptionId,
|
||||||
|
remark: requestData.remark || 'Mock redeemed promotion detail',
|
||||||
|
outcome: {
|
||||||
|
redeemedList: selectedOutcomes.map((outcome) => ({
|
||||||
|
outcomeRemark: outcome?.prize?.prizeCode || outcome?.value || '',
|
||||||
|
outcomeType: 'PRIZE',
|
||||||
|
prizeCategory: outcome?.prize?.prizeCategory || 'PROMOTION',
|
||||||
|
transId: redemptionPublicId,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
outcomeAward: null,
|
||||||
|
voidedBy: '',
|
||||||
|
voidedDtm: '',
|
||||||
|
voidedGamingDate: '',
|
||||||
|
voidedRemark: '',
|
||||||
|
displayOnlyPromo: false,
|
||||||
|
isVoided: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBackdatePromotionRedeemRequest(route, requestBody) {
|
||||||
|
if (typeof route === 'string' && /redeembackdate/i.test(route)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const backdate = requestBody?.data?.backdate
|
||||||
|
return typeof backdate === 'string' && backdate.trim().length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockPromotionRedeemResult({ context, requestBody, accountNum, backdate = false }) {
|
||||||
|
const requestData = requestBody?.data || {}
|
||||||
|
const selection = context.resolvePromotionRedeemSelection(
|
||||||
|
accountNum,
|
||||||
|
requestData.promotionId,
|
||||||
|
requestData.promotionRuleId,
|
||||||
|
requestData.outcomeId,
|
||||||
|
{ backdate },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!selection || !selection.promotion || selection.selectedOutcomes.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Promotion fixture not found for account ${accountNum}, promotionId=${requestData.promotionId}, promotionRuleId=${requestData.promotionRuleId}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promotionRedemptionId = context.nextMockPromotionRedemptionId()
|
||||||
|
return {
|
||||||
|
redeemedOutcome: buildRecordedRedeemedOutcome({
|
||||||
|
requestBody,
|
||||||
|
promotion: selection.promotion,
|
||||||
|
selectedOutcomes: selection.selectedOutcomes,
|
||||||
|
promotionRedemptionId,
|
||||||
|
redemptionPublicId: context.generateToken(backdate ? 'mock_backdate_redemption' : 'mock_redemption'),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bridgePromotionRedeemToEwallet({
|
||||||
|
context,
|
||||||
|
requestBody,
|
||||||
|
accountNum,
|
||||||
|
backdate = false,
|
||||||
|
}) {
|
||||||
|
const { log } = require('../../../logger')
|
||||||
|
const requestData = requestBody?.data || {}
|
||||||
|
log(
|
||||||
|
`[BRIDGE] start account=${accountNum} promotionId=${requestData.promotionId} outcomeId=${requestData.outcomeId} backdate=${backdate}`,
|
||||||
|
)
|
||||||
|
const selection = context.resolvePromotionRedeemSelection(
|
||||||
|
accountNum,
|
||||||
|
requestData.promotionId,
|
||||||
|
requestData.promotionRuleId,
|
||||||
|
requestData.outcomeId,
|
||||||
|
{ backdate },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!selection || !selection.promotion || selection.selectedOutcomes.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Promotion fixture not found for account ${accountNum}, promotionId=${requestData.promotionId}, promotionRuleId=${requestData.promotionRuleId}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promotionRedemptionId = context.nextMockPromotionRedemptionId()
|
||||||
|
const redemptionPayload = buildRedemptionPayload({
|
||||||
|
context,
|
||||||
|
requestBody,
|
||||||
|
accountNum,
|
||||||
|
promotion: selection.promotion,
|
||||||
|
selectedOutcomes: selection.selectedOutcomes,
|
||||||
|
promotionRedemptionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const loginUsername =
|
||||||
|
context.promotionRedemptionBridgeLoginUsername || requestData.loginId || 'mock.gateway'
|
||||||
|
const authUrl = joinUrl(context.promotionRedemptionBridgeBaseUrl, '/api/v1/auth/login')
|
||||||
|
const redemptionUrl = joinUrl(
|
||||||
|
context.promotionRedemptionBridgeBaseUrl,
|
||||||
|
'/api/v1/prize-redemptions',
|
||||||
|
)
|
||||||
|
|
||||||
|
const authResponseBody = await requestJson(
|
||||||
|
authUrl,
|
||||||
|
createRequestOptions(
|
||||||
|
'POST',
|
||||||
|
{
|
||||||
|
username: loginUsername,
|
||||||
|
password: context.promotionRedemptionBridgeLoginPassword,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
context.promotionRedemptionBridgeTimeoutMs,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const authResponseData = unwrapApiResponseBody(authResponseBody, 'Promotion bridge login')
|
||||||
|
const accessToken = authResponseData?.accessToken
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('Promotion bridge login did not return an access token')
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`[BRIDGE] calling eWallet POST ${redemptionUrl} (sync)`)
|
||||||
|
const redemptionResponseBody = await requestJson(
|
||||||
|
redemptionUrl,
|
||||||
|
createRequestOptions(
|
||||||
|
'POST',
|
||||||
|
redemptionPayload,
|
||||||
|
{
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Client-Machine-Id': requestBody?.schema?.computerName || 'mock-gateway',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Location-Code': requestData.locationCode || 'MOCK',
|
||||||
|
'Property-Code': requestData.casinoCode || 'GLP',
|
||||||
|
'X-Real-IP': requestBody?.schema?.ipAddress || '',
|
||||||
|
},
|
||||||
|
context.promotionRedemptionBridgeTimeoutMs,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
log(`[BRIDGE][SYNC-RAW] ${JSON.stringify(redemptionResponseBody)}`)
|
||||||
|
const redemptionResponseData = unwrapApiResponseBody(
|
||||||
|
redemptionResponseBody,
|
||||||
|
'Promotion bridge prize redemption',
|
||||||
|
)
|
||||||
|
log(`[BRIDGE][SYNC-DATA] ${JSON.stringify(redemptionResponseData)}`)
|
||||||
|
|
||||||
|
if (!redemptionResponseData?.publicId) {
|
||||||
|
throw new Error('Promotion bridge prize redemption did not return a publicId')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redemptionResponseData.overallStatus && redemptionResponseData.overallStatus !== 'COMPLETED') {
|
||||||
|
const errorMessage =
|
||||||
|
redemptionResponseData.finalErrorMessage ||
|
||||||
|
redemptionResponseData.finalStatusReason ||
|
||||||
|
`Prize redemption finished with status ${redemptionResponseData.overallStatus}`
|
||||||
|
throw new Error(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
redeemedOutcome: buildRecordedRedeemedOutcome({
|
||||||
|
requestBody,
|
||||||
|
promotion: selection.promotion,
|
||||||
|
selectedOutcomes: selection.selectedOutcomes,
|
||||||
|
promotionRedemptionId,
|
||||||
|
redemptionPublicId: redemptionResponseData.publicId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerPlayerPromotionRedeemRoute({ app, context }) {
|
||||||
|
const routes = [
|
||||||
|
'/ewl/promotion/v1/playerpromotion/redeem',
|
||||||
|
'/ewl/promotion/v1/playerpromotion/redeemBackDate',
|
||||||
|
'/ewl/bal/v1/playerpromotion/redeem',
|
||||||
|
'/ewl/bal/v1/playerpromotion/redeemBackDate',
|
||||||
|
]
|
||||||
|
|
||||||
|
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 accountNum = context.resolveAccountNum(headerMergedRequestBody)
|
||||||
|
const normalizedRequestBody = mergeRuntimeIssueTargetToWalletOverrides(
|
||||||
|
headerMergedRequestBody,
|
||||||
|
context,
|
||||||
|
accountNum,
|
||||||
|
)
|
||||||
|
const backdateRedeemRequest = isBackdatePromotionRedeemRequest(route, normalizedRequestBody)
|
||||||
|
const redeemMock = context.getPlayerPromotionRedeemMock(accountNum)
|
||||||
|
let errorCode = redeemMock.errorCode
|
||||||
|
let errorMsg = redeemMock.errorMsg
|
||||||
|
let result = redeemMock.result
|
||||||
|
|
||||||
|
let simulatePublicId = null
|
||||||
|
if (result && !errorCode) {
|
||||||
|
try {
|
||||||
|
if (context.promotionRedemptionBridgeEnabled) {
|
||||||
|
const bridgeResult = await bridgePromotionRedeemToEwallet({
|
||||||
|
context,
|
||||||
|
requestBody: normalizedRequestBody,
|
||||||
|
accountNum,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
} else if (backdateRedeemRequest) {
|
||||||
|
const mockResult = createMockPromotionRedeemResult({
|
||||||
|
context,
|
||||||
|
requestBody: normalizedRequestBody,
|
||||||
|
accountNum,
|
||||||
|
backdate: true,
|
||||||
|
})
|
||||||
|
context.recordRedeemedPromotionOutcome(accountNum, mockResult.redeemedOutcome)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorCode = EWALLET_HTTP_ERROR
|
||||||
|
errorMsg = error instanceof Error ? error.message : String(error)
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode,
|
||||||
|
errorMsg,
|
||||||
|
accountNum,
|
||||||
|
result,
|
||||||
|
...(simulatePublicId ? { redemptionPublicId: simulatePublicId } : {}),
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPlayerPromotionRedeemRoute
|
||||||
|
module.exports.__internal = {
|
||||||
|
ISSUE_TARGET_TO_WALLET_HEADER,
|
||||||
|
createMockPromotionRedeemResult,
|
||||||
|
isBackdatePromotionRedeemRequest,
|
||||||
|
bridgePromotionRedeemToEwallet,
|
||||||
|
registerableRoutes: [
|
||||||
|
'/ewl/promotion/v1/playerpromotion/redeem',
|
||||||
|
'/ewl/promotion/v1/playerpromotion/redeemBackDate',
|
||||||
|
'/ewl/bal/v1/playerpromotion/redeem',
|
||||||
|
'/ewl/bal/v1/playerpromotion/redeemBackDate',
|
||||||
|
],
|
||||||
|
mergeIssueTargetToWalletOverrides,
|
||||||
|
mergeRuntimeIssueTargetToWalletOverrides,
|
||||||
|
parseIssueTargetToWalletHeader,
|
||||||
|
resolveIssueTargetToWallet,
|
||||||
|
resolveIssueTargetToWalletOverride,
|
||||||
|
toNullableBoolean,
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
function registerPointVoidRoute({ app, context }) {
|
||||||
|
app.post('/ewl/bal/v1/point/void', (req, res) => {
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: context.pointVoidErrorCode,
|
||||||
|
errorMsg: context.pointVoidErrorMsg,
|
||||||
|
result: context.pointVoidResult,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPointVoidRoute
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
function registerPrizeRedeemRoute({ app, context }) {
|
||||||
|
app.post('/ewl/bal/v1/prize/redeem', (req, res) => {
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: context.prizeRedeemErrorCode,
|
||||||
|
errorMsg: context.prizeRedeemErrorMsg,
|
||||||
|
redeemPrize: context.prizeRedeemValue || context.generateToken('mock_redeem_prize'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPrizeRedeemRoute
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
function registerPromotionAllRoute({ app, context }) {
|
||||||
|
const fixedBackdateAccountNum = '80038008'
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/ewl/promotion/v1/promotion/all',
|
||||||
|
resolvePromotionLists: (accountNum) => context.getPromotionLists(accountNum),
|
||||||
|
requireAccountNum: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ewl/bal/v1/promotion/all',
|
||||||
|
resolvePromotionLists: (accountNum) => context.getPromotionLists(accountNum),
|
||||||
|
requireAccountNum: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ewl/promotion/v1/promotion/entitledBackDate',
|
||||||
|
resolvePromotionLists: () => context.getBackdatePromotionLists(fixedBackdateAccountNum),
|
||||||
|
requireAccountNum: false,
|
||||||
|
responseAccountNum: fixedBackdateAccountNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ewl/promotion/v1/promotion/entitledBackDate/all',
|
||||||
|
resolvePromotionLists: () => context.getBackdatePromotionLists(fixedBackdateAccountNum),
|
||||||
|
requireAccountNum: false,
|
||||||
|
responseAccountNum: fixedBackdateAccountNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ewl/bal/v1/promotion/entitledBackDate/all',
|
||||||
|
resolvePromotionLists: () => context.getBackdatePromotionLists(fixedBackdateAccountNum),
|
||||||
|
requireAccountNum: false,
|
||||||
|
responseAccountNum: fixedBackdateAccountNum,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
routes.forEach(({ path, resolvePromotionLists, requireAccountNum, responseAccountNum }) =>
|
||||||
|
app.post(path, (req, res) => {
|
||||||
|
const accountNum = requireAccountNum ? context.resolveAccountNum(req.body) : responseAccountNum
|
||||||
|
|
||||||
|
if (requireAccountNum && !accountNum) {
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: 'MISSING_ACCOUNT_NUM',
|
||||||
|
errorMsg: 'accountNum or playerId is required',
|
||||||
|
accountNum: '',
|
||||||
|
eligiblePromotionList: [],
|
||||||
|
entitledPromotionList: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { eligiblePromotionList, entitledPromotionList } = resolvePromotionLists(accountNum)
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: '',
|
||||||
|
errorMsg: '',
|
||||||
|
accountNum,
|
||||||
|
eligiblePromotionList,
|
||||||
|
entitledPromotionList,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPromotionAllRoute
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
function registerRedeemedPromotionDetailRoute({ app, context }) {
|
||||||
|
app.post(
|
||||||
|
[
|
||||||
|
'/ewl/promotion/v1/redeemedpromotion/detail',
|
||||||
|
'/ewl/bal/v1/redeemedpromotion/detail',
|
||||||
|
],
|
||||||
|
(req, res) => {
|
||||||
|
const fixtureCustomerId =
|
||||||
|
typeof context.promotionsRedeemedFixture?.data?.customerId === 'string'
|
||||||
|
? context.promotionsRedeemedFixture.data.customerId
|
||||||
|
: ''
|
||||||
|
const fixtureOutcomeList = Array.isArray(context.promotionsRedeemedFixture?.data?.outcomeList)
|
||||||
|
? context.cloneJsonValue(context.promotionsRedeemedFixture.data.outcomeList)
|
||||||
|
: []
|
||||||
|
const accountNum = context.resolveAccountNum(req.body) || fixtureCustomerId
|
||||||
|
const recordedOutcomeList = context.getRecordedRedeemedPromotionOutcomes(accountNum)
|
||||||
|
const fallbackOutcomeList = [context.buildMockRedeemedPromotionOutcome(req.body)]
|
||||||
|
let outcomeList = recordedOutcomeList
|
||||||
|
|
||||||
|
if (outcomeList.length === 0) {
|
||||||
|
const accountFixture = context.getAccountSpecificRedeemedPromotionsFixture(accountNum)
|
||||||
|
const accountFixtureOutcomeList = Array.isArray(accountFixture?.data?.outcomeList)
|
||||||
|
? context.cloneJsonValue(accountFixture.data.outcomeList)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (accountFixtureOutcomeList !== null) {
|
||||||
|
outcomeList = accountFixtureOutcomeList
|
||||||
|
} else {
|
||||||
|
outcomeList = context.redeemedPromotionDetailIncludeDefaultRecord
|
||||||
|
? fixtureOutcomeList.length > 0
|
||||||
|
? fixtureOutcomeList
|
||||||
|
: fallbackOutcomeList
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: context.redeemedPromotionDetailErrorCode,
|
||||||
|
errorMsg: context.redeemedPromotionDetailErrorMsg,
|
||||||
|
accountNum,
|
||||||
|
outcomeList,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerRedeemedPromotionDetailRoute
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
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 }) {
|
||||||
|
const routes = [
|
||||||
|
'/ewl/promotion/v1/redeemedpromotion/void',
|
||||||
|
'/ewl/bal/v1/redeemedpromotion/void',
|
||||||
|
]
|
||||||
|
|
||||||
|
routes.forEach((route) =>
|
||||||
|
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) {
|
||||||
|
context.voidRecordedRedeemedPromotionOutcome(redeemedId, loginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: context.redeemedPromotionVoidErrorCode,
|
||||||
|
errorMsg: context.redeemedPromotionVoidErrorMsg,
|
||||||
|
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
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
const {
|
||||||
|
resolveCardNumForLookup,
|
||||||
|
resolvePlayerIdForLookup,
|
||||||
|
} = require('./memberProfileCardNum')
|
||||||
|
|
||||||
|
function registerMemberProfileRoute({ app, context }) {
|
||||||
|
app.post('/ewl/edip/v1/member/profile', (req, res) => {
|
||||||
|
const sendResponse = () => {
|
||||||
|
const accountNum = context.resolveAccountNum(req.body)
|
||||||
|
const playerId = resolvePlayerIdForLookup({
|
||||||
|
accountNum,
|
||||||
|
playerId: req.body?.data?.playerId,
|
||||||
|
})
|
||||||
|
const cardNum = resolveCardNumForLookup({
|
||||||
|
accountNum,
|
||||||
|
playerId,
|
||||||
|
enabled: context.task15171Enabled,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!accountNum) {
|
||||||
|
return res.status(200).json({
|
||||||
|
status: false,
|
||||||
|
errorCode: 'NOT_FOUND',
|
||||||
|
message: 'accountNum or playerId is required',
|
||||||
|
data: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const mockProfile = context.getMockProfileForAccount(accountNum)
|
||||||
|
|
||||||
|
const memberProfileTemplate = context.cloneJsonValue(context.memberProfileResponseFixture)
|
||||||
|
const memberProfileDataTemplate =
|
||||||
|
memberProfileTemplate?.data && typeof memberProfileTemplate.data === 'object'
|
||||||
|
? memberProfileTemplate.data
|
||||||
|
: {}
|
||||||
|
const playerTemplate =
|
||||||
|
memberProfileDataTemplate?.player && typeof memberProfileDataTemplate.player === 'object'
|
||||||
|
? memberProfileDataTemplate.player
|
||||||
|
: {}
|
||||||
|
const attributeTemplate =
|
||||||
|
memberProfileDataTemplate?.attribute && typeof memberProfileDataTemplate.attribute === 'object'
|
||||||
|
? memberProfileDataTemplate.attribute
|
||||||
|
: {}
|
||||||
|
const phoneListTemplate = Array.isArray(memberProfileDataTemplate.phoneList)
|
||||||
|
? memberProfileDataTemplate.phoneList
|
||||||
|
: []
|
||||||
|
const firstPhoneTemplate =
|
||||||
|
phoneListTemplate.length > 0 &&
|
||||||
|
phoneListTemplate[0] &&
|
||||||
|
typeof phoneListTemplate[0] === 'object'
|
||||||
|
? phoneListTemplate[0]
|
||||||
|
: {}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
...memberProfileTemplate,
|
||||||
|
status: memberProfileTemplate?.status ?? true,
|
||||||
|
errorCode: `${memberProfileTemplate?.errorCode || ''}`,
|
||||||
|
message: memberProfileTemplate?.message || 'Mock member profile',
|
||||||
|
data: {
|
||||||
|
...memberProfileDataTemplate,
|
||||||
|
accountNum,
|
||||||
|
playerId,
|
||||||
|
card_num: cardNum,
|
||||||
|
currentTier: mockProfile.tier,
|
||||||
|
onDayTierPoint: mockProfile.onDayTierPoint,
|
||||||
|
lastLocn: mockProfile.lastLocn,
|
||||||
|
eBonus: mockProfile.eBonus,
|
||||||
|
phoneList: [
|
||||||
|
{
|
||||||
|
...firstPhoneTemplate,
|
||||||
|
phoneNum: mockProfile.phoneNum,
|
||||||
|
phoneType: firstPhoneTemplate.phoneType || 'Mobile1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
player: {
|
||||||
|
...playerTemplate,
|
||||||
|
firstName: mockProfile.firstName,
|
||||||
|
lastName: mockProfile.lastName,
|
||||||
|
nickName: mockProfile.nickName,
|
||||||
|
image: mockProfile.image,
|
||||||
|
},
|
||||||
|
attribute: {
|
||||||
|
...attributeTemplate,
|
||||||
|
birthday: mockProfile.birthday,
|
||||||
|
gender: mockProfile.gender,
|
||||||
|
},
|
||||||
|
addressList: Array.isArray(memberProfileDataTemplate.addressList)
|
||||||
|
? memberProfileDataTemplate.addressList
|
||||||
|
: [],
|
||||||
|
identityList: Array.isArray(memberProfileDataTemplate.identityList)
|
||||||
|
? memberProfileDataTemplate.identityList
|
||||||
|
: [],
|
||||||
|
emailList: Array.isArray(memberProfileDataTemplate.emailList)
|
||||||
|
? memberProfileDataTemplate.emailList
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.memberProfileForceStatusCode) {
|
||||||
|
const sendForcedError = () =>
|
||||||
|
res.status(context.memberProfileForceStatusCode).json({
|
||||||
|
status: false,
|
||||||
|
errorCode: context.memberProfileForceErrorCode,
|
||||||
|
message: context.memberProfileForceErrorMsg,
|
||||||
|
data: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (context.memberProfileDelayMs > 0) {
|
||||||
|
return setTimeout(sendForcedError, context.memberProfileDelayMs)
|
||||||
|
}
|
||||||
|
return sendForcedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.memberProfileDelayMs > 0) {
|
||||||
|
return setTimeout(sendResponse, context.memberProfileDelayMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendResponse()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerMemberProfileRoute
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
const TASK_15171_PLAYER_ID = '600000154'
|
||||||
|
const TASK_15171_INACTIVE_CARD = '2600000154001'
|
||||||
|
const TASK_15171_ACTIVE_CARD = '2600000154002'
|
||||||
|
|
||||||
|
function normalizeLookupValue(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTask15171Scenario({ accountNum, playerId }) {
|
||||||
|
const normalizedAccountNum = normalizeLookupValue(accountNum)
|
||||||
|
const normalizedPlayerId = normalizeLookupValue(playerId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
normalizedPlayerId === TASK_15171_PLAYER_ID ||
|
||||||
|
normalizedAccountNum === TASK_15171_ACTIVE_CARD ||
|
||||||
|
normalizedAccountNum === TASK_15171_INACTIVE_CARD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCardNumForLookup({ accountNum, playerId, enabled }) {
|
||||||
|
if (!enabled) {
|
||||||
|
const normalizedAccountNum = normalizeLookupValue(accountNum)
|
||||||
|
return normalizedAccountNum ? [normalizedAccountNum] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTask15171Scenario({ accountNum, playerId })) {
|
||||||
|
return [TASK_15171_ACTIVE_CARD]
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedAccountNum = normalizeLookupValue(accountNum)
|
||||||
|
return normalizedAccountNum ? [normalizedAccountNum] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePlayerIdForLookup({ accountNum, playerId }) {
|
||||||
|
const normalizedAccountNum = normalizeLookupValue(accountNum)
|
||||||
|
const normalizedPlayerId = normalizeLookupValue(playerId)
|
||||||
|
|
||||||
|
if (normalizedPlayerId) {
|
||||||
|
return normalizedPlayerId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedAccountNum === TASK_15171_ACTIVE_CARD ||
|
||||||
|
normalizedAccountNum === TASK_15171_INACTIVE_CARD
|
||||||
|
) {
|
||||||
|
return TASK_15171_PLAYER_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedAccountNum
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
TASK_15171_PLAYER_ID,
|
||||||
|
TASK_15171_INACTIVE_CARD,
|
||||||
|
TASK_15171_ACTIVE_CARD,
|
||||||
|
resolveCardNumForLookup,
|
||||||
|
resolvePlayerIdForLookup,
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
function registerTeamHierarchyRoute({ app, context }) {
|
||||||
|
app.post('/ewl/mkt-rm/v1/team/hierarchy', (req, res) => {
|
||||||
|
const requestedUserName = `${req.body?.data?.userName || 'mock.user'}`.trim() || 'mock.user'
|
||||||
|
const requestedStaffId = `${req.body?.data?.staffId || 'M000001'}`.trim() || 'M000001'
|
||||||
|
const resolvedTags = context.teamHierarchyTags.length > 0 ? context.teamHierarchyTags : ['MOCK']
|
||||||
|
const teamMember = {
|
||||||
|
name: requestedUserName,
|
||||||
|
userName: requestedUserName,
|
||||||
|
mail: `${requestedUserName}@macausjm-glp.com`,
|
||||||
|
description: 'Mock team member',
|
||||||
|
staffId: requestedStaffId,
|
||||||
|
teamRole: context.teamHierarchyRole,
|
||||||
|
tags: resolvedTags,
|
||||||
|
}
|
||||||
|
const supportTeam = {
|
||||||
|
teamName: 'Mock Support Team',
|
||||||
|
description: 'Mock support team',
|
||||||
|
department: context.teamHierarchyDepartment,
|
||||||
|
hierarchy: [
|
||||||
|
{
|
||||||
|
name: 'Support',
|
||||||
|
priority: 1,
|
||||||
|
members: [teamMember],
|
||||||
|
tags: resolvedTags,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
supportTeams: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
status: context.teamHierarchyStatus,
|
||||||
|
errorCode: context.teamHierarchyErrorCode,
|
||||||
|
message: context.teamHierarchyMessage,
|
||||||
|
data: {
|
||||||
|
userName: requestedUserName,
|
||||||
|
staffId: requestedStaffId,
|
||||||
|
name: requestedUserName,
|
||||||
|
mail: `${requestedUserName}@macausjm-glp.com`,
|
||||||
|
teamRole: context.teamHierarchyRole,
|
||||||
|
tags: resolvedTags,
|
||||||
|
teams: [
|
||||||
|
{
|
||||||
|
teamName: 'Mock Team',
|
||||||
|
description: 'Mock team hierarchy',
|
||||||
|
department: context.teamHierarchyDepartment,
|
||||||
|
hierarchy: [
|
||||||
|
{
|
||||||
|
name: 'Main Team',
|
||||||
|
priority: 1,
|
||||||
|
members: [teamMember],
|
||||||
|
tags: resolvedTags,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
supportTeams: [supportTeam],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerTeamHierarchyRoute
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const { buildCheckPrizeStockResponseXml } = require('./xmlResponse')
|
||||||
|
|
||||||
|
function registerGisCheckPrizeStockRoute({ app, context }) {
|
||||||
|
app.get('/checkprizestock', (req, res) => {
|
||||||
|
const requestBody = req.body && typeof req.body === 'object' ? req.body : {}
|
||||||
|
const stockResponse = context.getMockGisCheckPrizeStockResponse(requestBody)
|
||||||
|
|
||||||
|
return res
|
||||||
|
.status(200)
|
||||||
|
.type('application/xml')
|
||||||
|
.send(buildCheckPrizeStockResponseXml([stockResponse]))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerGisCheckPrizeStockRoute
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
const { buildPrizeResponseXml } = require('./xmlResponse')
|
||||||
|
|
||||||
|
function registerGisPrizeRoute({ app, context }) {
|
||||||
|
app.post('/prize', (req, res) => {
|
||||||
|
return res.status(200).type('application/xml').send(
|
||||||
|
buildPrizeResponseXml({
|
||||||
|
errorCode: context.prizeRedeemErrorCode,
|
||||||
|
errorMsg: context.prizeRedeemErrorMsg,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerGisPrizeRoute
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const { buildTokenResponseXml } = require('./xmlResponse')
|
||||||
|
|
||||||
|
function registerGisTokenGetRoute({ app, context }) {
|
||||||
|
app.post('/token/get', (req, res) => {
|
||||||
|
if (context.edipForceErrorCode) {
|
||||||
|
return res.status(200).type('application/xml').send(
|
||||||
|
buildTokenResponseXml({
|
||||||
|
errorCode: context.edipForceErrorCode,
|
||||||
|
errorMsg: context.edipForceErrorMsg,
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
period: 0,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = context.tokenSuccessResponse()
|
||||||
|
return res.status(200).type('application/xml').send(
|
||||||
|
buildTokenResponseXml({
|
||||||
|
errorCode: token.errorCode,
|
||||||
|
errorMsg: token.errorMsg,
|
||||||
|
accessToken: token.accessToken,
|
||||||
|
refreshToken: token.refreshToken,
|
||||||
|
period: token.period,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerGisTokenGetRoute
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
const { buildTokenResponseXml } = require('./xmlResponse')
|
||||||
|
|
||||||
|
function resolveRefreshToken(body) {
|
||||||
|
return body?.refreshToken || body?.data?.refreshToken || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerGisTokenRefreshRoute({ app, context }) {
|
||||||
|
app.post('/token/refresh', (req, res) => {
|
||||||
|
const refreshToken = resolveRefreshToken(req.body)
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
return res.status(200).type('application/xml').send(
|
||||||
|
buildTokenResponseXml({
|
||||||
|
errorCode: 'MISSING_REFRESH_TOKEN',
|
||||||
|
errorMsg: 'refreshToken is required',
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
period: 0,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.edipRefreshTokenMustExist && !context.issuedRefreshTokens.has(refreshToken)) {
|
||||||
|
return res.status(200).type('application/xml').send(
|
||||||
|
buildTokenResponseXml({
|
||||||
|
errorCode: 'INVALID_REFRESH_TOKEN',
|
||||||
|
errorMsg: 'refreshToken not recognized by mock gateway',
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
period: 0,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = context.tokenSuccessResponse()
|
||||||
|
return res.status(200).type('application/xml').send(
|
||||||
|
buildTokenResponseXml({
|
||||||
|
errorCode: token.errorCode,
|
||||||
|
errorMsg: token.errorMsg,
|
||||||
|
accessToken: token.accessToken,
|
||||||
|
refreshToken: token.refreshToken,
|
||||||
|
period: token.period,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerGisTokenRefreshRoute
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
function escapeXml(value) {
|
||||||
|
return String(value ?? '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPositiveInteger(value, fallback = 0) {
|
||||||
|
const number = Number(value)
|
||||||
|
if (!Number.isFinite(number) || number < 0) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return Math.floor(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTokenResponseXml({
|
||||||
|
errorCode = '',
|
||||||
|
errorMsg = '',
|
||||||
|
accessToken = '',
|
||||||
|
refreshToken = '',
|
||||||
|
period = 0,
|
||||||
|
} = {}) {
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<GisTokenResponse>
|
||||||
|
<errorCode>${escapeXml(errorCode)}</errorCode>
|
||||||
|
<errorMsg>${escapeXml(errorMsg)}</errorMsg>
|
||||||
|
<accessToken>${escapeXml(accessToken)}</accessToken>
|
||||||
|
<refreshToken>${escapeXml(refreshToken)}</refreshToken>
|
||||||
|
<period>${toPositiveInteger(period, 0)}</period>
|
||||||
|
</GisTokenResponse>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPrizeResponseXml({ errorCode = '', errorMsg = '' } = {}) {
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<GisResponse>
|
||||||
|
<errorCode>${escapeXml(errorCode)}</errorCode>
|
||||||
|
<errorMsg>${escapeXml(errorMsg)}</errorMsg>
|
||||||
|
</GisResponse>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCheckPrizeStockResponseEntryXml({
|
||||||
|
propertyId = '',
|
||||||
|
locationId = '',
|
||||||
|
prizeCode = '',
|
||||||
|
prizeName = '',
|
||||||
|
prizeCategory = '',
|
||||||
|
supplierID = '',
|
||||||
|
uniqueCode = '',
|
||||||
|
sapItemCode = '',
|
||||||
|
luxuryFlag = '',
|
||||||
|
receivingID = '',
|
||||||
|
quantity = 0,
|
||||||
|
errorCode = '',
|
||||||
|
errorMsg = '',
|
||||||
|
} = {}) {
|
||||||
|
return ` <CheckPrizeStock_response_VM>
|
||||||
|
<propertyId>${escapeXml(propertyId)}</propertyId>
|
||||||
|
<locationId>${escapeXml(locationId)}</locationId>
|
||||||
|
<prizeCode>${escapeXml(prizeCode)}</prizeCode>
|
||||||
|
<prizeName>${escapeXml(prizeName)}</prizeName>
|
||||||
|
<prizeCategory>${escapeXml(prizeCategory)}</prizeCategory>
|
||||||
|
<supplierID>${escapeXml(supplierID)}</supplierID>
|
||||||
|
<uniqueCode>${escapeXml(uniqueCode)}</uniqueCode>
|
||||||
|
<sap_ItemCode>${escapeXml(sapItemCode)}</sap_ItemCode>
|
||||||
|
<luxuryFlag>${escapeXml(luxuryFlag)}</luxuryFlag>
|
||||||
|
<receivingID>${escapeXml(receivingID)}</receivingID>
|
||||||
|
<quantity>${toPositiveInteger(quantity, 0)}</quantity>
|
||||||
|
<errorCode>${escapeXml(errorCode)}</errorCode>
|
||||||
|
<errorMsg>${escapeXml(errorMsg)}</errorMsg>
|
||||||
|
</CheckPrizeStock_response_VM>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCheckPrizeStockResponseXml(entries = []) {
|
||||||
|
const normalizedEntries = Array.isArray(entries) ? entries : []
|
||||||
|
const responseEntries =
|
||||||
|
normalizedEntries.length > 0
|
||||||
|
? normalizedEntries.map((entry) => buildCheckPrizeStockResponseEntryXml(entry)).join('\n')
|
||||||
|
: buildCheckPrizeStockResponseEntryXml()
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ArrayOfCheckPrizeStock_response_VM>
|
||||||
|
${responseEntries}
|
||||||
|
</ArrayOfCheckPrizeStock_response_VM>`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildTokenResponseXml,
|
||||||
|
buildPrizeResponseXml,
|
||||||
|
buildCheckPrizeStockResponseXml,
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
function registerHealthzRoute({ app, ldapService }) {
|
||||||
|
app.get('/healthz', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
service: 'ewallet-mock-gateway',
|
||||||
|
now: new Date().toISOString(),
|
||||||
|
ldap: ldapService.getHealthInfo(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerHealthzRoute
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
function buildFilterFromRequest(req) {
|
||||||
|
const query = req.query || {}
|
||||||
|
const body = req.body && typeof req.body === 'object' ? req.body : {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accountNum: body.accountNum ?? query.accountNum,
|
||||||
|
promotionId: body.promotionId ?? query.promotionId,
|
||||||
|
promotionRuleId: body.promotionRuleId ?? query.promotionRuleId,
|
||||||
|
outcomeId: body.outcomeId ?? query.outcomeId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerPromotionRedeemFulfillmentOverridesRoute({ app, context }) {
|
||||||
|
const route = '/mock-api/promotion-redeem/fulfillment-overrides'
|
||||||
|
|
||||||
|
app.get(route, (req, res) => {
|
||||||
|
const overrides = context.getPromotionRedeemFulfillmentOverrides(buildFilterFromRequest(req))
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Promotion redeem fulfillment overrides retrieved successfully.',
|
||||||
|
data: overrides,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(route, (req, res) => {
|
||||||
|
try {
|
||||||
|
const override = context.setPromotionRedeemFulfillmentOverride(req.body)
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Promotion redeem fulfillment override saved successfully.',
|
||||||
|
data: override,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
error: {
|
||||||
|
code: 'INVALID_REQUEST',
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete(route, (req, res) => {
|
||||||
|
const removed = context.clearPromotionRedeemFulfillmentOverrides(buildFilterFromRequest(req))
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Promotion redeem fulfillment overrides cleared successfully.',
|
||||||
|
data: {
|
||||||
|
removed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerPromotionRedeemFulfillmentOverridesRoute
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
const registerHealthzRoute = require('./healthz')
|
||||||
|
const registerEdipTokenGetRoute = require('./edip/tokenGet')
|
||||||
|
const registerEdipTokenRefreshRoute = require('./edip/tokenRefresh')
|
||||||
|
const registerGisTokenGetRoute = require('./gis/tokenGet')
|
||||||
|
const registerGisTokenRefreshRoute = require('./gis/tokenRefresh')
|
||||||
|
const registerGisPrizeRoute = require('./gis/prize')
|
||||||
|
const registerGisCheckPrizeStockRoute = require('./gis/checkPrizeStock')
|
||||||
|
const registerCardpinValidateRoute = require('./ewl/bal/cardpinValidate')
|
||||||
|
const registerTeamHierarchyRoute = require('./ewl/mktRm/teamHierarchy')
|
||||||
|
const registerMemberProfileRoute = require('./ewl/edip/memberProfile')
|
||||||
|
const registerPromotionAllRoute = require('./ewl/bal/promotionAll')
|
||||||
|
const registerPlayerImageRoute = require('./ewl/bal/playerImage')
|
||||||
|
const registerPlayerEarnRoute = require('./ewl/bal/playerEarn')
|
||||||
|
const registerPrizeRedeemRoute = require('./ewl/bal/prizeRedeem')
|
||||||
|
const registerPointVoidRoute = require('./ewl/bal/pointVoid')
|
||||||
|
const registerRedeemedPromotionDetailRoute = require('./ewl/bal/redeemedPromotionDetail')
|
||||||
|
const registerPlayerPromotionRedeemRoute = require('./ewl/bal/playerPromotionRedeem')
|
||||||
|
const registerRedeemedPromotionVoidRoute = require('./ewl/bal/redeemedPromotionVoid')
|
||||||
|
const registerPlayerPromotionFulfillmentJourneyRoute = require('./ewl/bal/playerPromotionFulfillmentJourney')
|
||||||
|
const registerPlayerPromotionFulfillmentJourneyDetailRoute = require('./ewl/bal/playerPromotionFulfillmentJourneyDetail')
|
||||||
|
const registerPrizeRedemptionsSyncRoute = require('./api/v1/prizeRedemptionsSync')
|
||||||
|
const registerPromotionRedeemFulfillmentOverridesRoute = require('./mockApi/promotionRedeemFulfillmentOverrides')
|
||||||
|
const registerSmsPortalRoutes = require('./sms/smsPortal')
|
||||||
|
|
||||||
|
function registerRoutes({ app, context, ldapService }) {
|
||||||
|
registerHealthzRoute({ app, ldapService })
|
||||||
|
registerEdipTokenGetRoute({ app, context })
|
||||||
|
registerEdipTokenRefreshRoute({ app, context })
|
||||||
|
registerGisTokenGetRoute({ app, context })
|
||||||
|
registerGisTokenRefreshRoute({ app, context })
|
||||||
|
registerGisPrizeRoute({ app, context })
|
||||||
|
registerGisCheckPrizeStockRoute({ app, context })
|
||||||
|
registerCardpinValidateRoute({ app, context })
|
||||||
|
registerTeamHierarchyRoute({ app, context })
|
||||||
|
registerMemberProfileRoute({ app, context })
|
||||||
|
registerPromotionAllRoute({ app, context })
|
||||||
|
registerPlayerImageRoute({ app, context })
|
||||||
|
registerPlayerEarnRoute({ app, context })
|
||||||
|
registerPrizeRedeemRoute({ app, context })
|
||||||
|
registerPointVoidRoute({ app, context })
|
||||||
|
registerRedeemedPromotionDetailRoute({ app, context })
|
||||||
|
registerPlayerPromotionRedeemRoute({ app, context })
|
||||||
|
registerRedeemedPromotionVoidRoute({ app, context })
|
||||||
|
registerPlayerPromotionFulfillmentJourneyRoute({ app, context })
|
||||||
|
registerPlayerPromotionFulfillmentJourneyDetailRoute({ app, context })
|
||||||
|
registerPrizeRedemptionsSyncRoute({ app, context })
|
||||||
|
registerPromotionRedeemFulfillmentOverridesRoute({ app, context })
|
||||||
|
registerSmsPortalRoutes({ app })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { registerRoutes }
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
function registerSmsPortalRoutes({ app }) {
|
||||||
|
app.post('/api/SMSToken/get', (req, res) => {
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: '',
|
||||||
|
errorMsg: '',
|
||||||
|
accessToken: 'mock-sms-access-token',
|
||||||
|
refreshToken: 'mock-sms-refresh-token',
|
||||||
|
period: 3600,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/SMSToken/Refresh', (req, res) => {
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: '',
|
||||||
|
errorMsg: '',
|
||||||
|
accessToken: 'mock-sms-access-token',
|
||||||
|
refreshToken: 'mock-sms-refresh-token',
|
||||||
|
period: 3600,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/SMSRequest/Post', (req, res) => {
|
||||||
|
const recipients = Array.isArray(req.body?.recipientList) ? req.body.recipientList : []
|
||||||
|
const smsStatus = recipients.length > 0
|
||||||
|
? recipients.map((r) => ({
|
||||||
|
recipient: typeof r === 'string' ? r : r?.recipient || '',
|
||||||
|
status: 'OK',
|
||||||
|
}))
|
||||||
|
: [{ recipient: req.body?.recipient || '', status: 'OK' }]
|
||||||
|
return res.status(200).json({
|
||||||
|
errorCode: '',
|
||||||
|
errorMsg: '',
|
||||||
|
smsID: `mock-sms-${Date.now()}`,
|
||||||
|
smsStatus,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerSmsPortalRoutes
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
|
||||||
|
const express = require('express')
|
||||||
|
|
||||||
|
const { createMockContext } = require('./context/createMockContext')
|
||||||
|
const { createMockLdapService } = require('./ldap/createMockLdapService')
|
||||||
|
const { registerRoutes } = require('./routes/registerRoutes')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
app.use(express.json({ limit: '2mb' }))
|
||||||
|
|
||||||
|
const { requestLogger, log } = require('./logger')
|
||||||
|
app.use(requestLogger)
|
||||||
|
|
||||||
|
const context = createMockContext()
|
||||||
|
const ldapService = createMockLdapService({ ldapConfig: context.ldap })
|
||||||
|
|
||||||
|
registerRoutes({ app, context, ldapService })
|
||||||
|
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({
|
||||||
|
error: 'NOT_MOCKED',
|
||||||
|
message: `No mock configured for ${req.method} ${req.path}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(context.port, context.host, () => {
|
||||||
|
console.log(`Mock gateway listening on http://${context.host}:${context.port}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
ldapService.start()
|
||||||
@@ -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