diff --git a/e.p/promotions-redeemed.json b/e.p/promotions-redeemed.json
new file mode 100644
index 0000000..6b5c289
--- /dev/null
+++ b/e.p/promotions-redeemed.json
@@ -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
+ }
+ ]
+ }
+}
diff --git a/fixtures/award.promotions.100000059.json b/fixtures/award.promotions.100000059.json
new file mode 100644
index 0000000..e9d8c63
--- /dev/null
+++ b/fixtures/award.promotions.100000059.json
@@ -0,0 +1,1739 @@
+{
+ "success": true,
+ "message": "Get redeemed promotions successfully.",
+ "data": {
+ "customerId": "100000059",
+ "outcomeList": [
+ {
+ "gamingDate": "2026-04-02",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-01T16:03:11.338897",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "DG-1pt-Breakfast",
+ "prizeRuleId": 663,
+ "promotionCode": "LP-EE-DG-2026-Q2",
+ "promotionId": 141,
+ "redeemedQty": 1,
+ "redemptionId": 44,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "0c772219-58eb-41d5-b4cb-c64a6f0546b8",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "4ee70d6a-6753-46d5-a30c-6287e04a7c2c",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "qa-randyng",
+ "voidedByEmployeeId": "171853",
+ "voidedDtm": "2026-04-02T16:32:21.849874",
+ "voidedGamingDate": "",
+ "voidedRemark": "qa",
+ "displayOnlyPromo": false,
+ "isVoided": true,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-01",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-01T13:24:07.435172",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "DG-1pt-Breakfast",
+ "prizeRuleId": 663,
+ "promotionCode": "LP-EE-DG-2026-Q2",
+ "promotionId": 141,
+ "redeemedQty": 1,
+ "redemptionId": 36,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "86bd3dc7-d15b-4c09-ba46-43423d6b23d0",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "18d2aa91-2f0c-4780-8b07-087b83648197",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "qa-nicolefeng",
+ "voidedDtm": "2026-04-01T16:02:44.565469",
+ "voidedGamingDate": "",
+ "voidedRemark": "test",
+ "displayOnlyPromo": false,
+ "isVoided": true,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-01",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-01T12:29:29.851076",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "DG-1pt-Breakfast",
+ "prizeRuleId": 663,
+ "promotionCode": "LP-EE-DG-2026-Q2",
+ "promotionId": 141,
+ "redeemedQty": 1,
+ "redemptionId": 34,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "72858ff2-287e-460e-8205-c945fafcbc82",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "bdc18d1e-3f2e-4385-8d9c-6a1753e2af41",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "qa-nicolefeng",
+ "voidedDtm": "2026-04-01T12:36:37.780013",
+ "voidedGamingDate": "",
+ "voidedRemark": "test",
+ "displayOnlyPromo": false,
+ "isVoided": true,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-01",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-01T10:22:28.900806",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "qa_1_point_rule",
+ "prizeRuleId": 107,
+ "promotionCode": "qa_1_point",
+ "promotionId": 26,
+ "redeemedQty": 1,
+ "redemptionId": 17,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it-test-ebo-$1",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "a4f23976-1d49-4824-89ff-bf91bdcef77a",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "a5d646e0-6a4d-4996-a1eb-9459adb39105",
+ "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.00,
+ "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": "2026-03-18T12:25:23.604192+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": {
+ "d-comp": 1,
+ "m-comp": 1
+ },
+ "compValue": 1,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-01",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-01T10:14:28.593730",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "qa_1_point_rule",
+ "prizeRuleId": 107,
+ "promotionCode": "qa_1_point",
+ "promotionId": 26,
+ "redeemedQty": 1,
+ "redemptionId": 15,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it-test-fb-$1",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "7f27d0d5-4669-4966-b556-a3c4c2f46d95",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "e5935b72-19e7-45d1-8dee-32144548fd7c",
+ "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.00,
+ "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-03-31T15:37:04.556191+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": {
+ "d-comp": 1,
+ "m-comp": 1
+ },
+ "compValue": 1,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-01",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-01T10:15:26.686278",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "Rule_IT-PromoTagB",
+ "prizeRuleId": 77,
+ "promotionCode": "IT-PromoTagB",
+ "promotionId": 16,
+ "redeemedQty": 1,
+ "redemptionId": 16,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-it-mqa-glpusb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "7a5b7375-f3a0-4c4a-9b58-43b88422df01",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": null,
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-it-mqa-glpusb",
+ "behaviorProfileCode": "OTHERS_AUTOSETTLE",
+ "name": "LP-IT-MQA-GLPUSB",
+ "description": "LP-IT-MQA-GLPUSB",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 80.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P50D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "Others",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "EXTERNAL_ONLY",
+ "allowNegativeBalance": false,
+ "createdAt": "2025-11-18T00:09:10.317981+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2025-11-18T00:09:10.317981+08:00",
+ "updatedBy": "HUB_MIGRATION",
+ "updatedByEmployeeId": null,
+ "tags": null,
+ "behaviorProfile": {
+ "profileCode": "OTHERS_AUTOSETTLE",
+ "profileName": "Others (AutoSettle)",
+ "prizeCategory": "Others",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "EXTERNAL_ONLY",
+ "description": "Other target-only item auto-settled externally",
+ "isActive": true,
+ "allowNegativeBalance": false,
+ "isBucket": false,
+ "isEbonus": false
+ },
+ "localizations": null,
+ "messages": null,
+ "isPrintVoucher": null,
+ "isExcludeGlobalMessages": null,
+ "effectiveFrom": null,
+ "effectiveTo": null,
+ "bucketPrices": {
+ "d-comp": 0,
+ "m-comp": 0
+ },
+ "compValue": 0,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": true
+ },
+ {
+ "gamingDate": "2026-04-01",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-01T12:37:09.994552",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "(Breakfast)LP-EE-DG-1PTS",
+ "prizeRuleId": 575,
+ "promotionCode": "LP-EE-DG-2026",
+ "promotionId": 136,
+ "redeemedQty": 1,
+ "redemptionId": 35,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "6384511d-6e07-48fc-a4a5-306bfa4b5ea3",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "0ddffb4a-ec28-463f-a92a-35aeef058b64",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "qa-nicolefeng",
+ "voidedDtm": "2026-04-01T16:03:00.153491",
+ "voidedGamingDate": "",
+ "voidedRemark": "test",
+ "displayOnlyPromo": false,
+ "isVoided": true,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-01",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-01T12:00:12.782221",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "(Breakfast)LP-EE-DG-1PTS",
+ "prizeRuleId": 575,
+ "promotionCode": "LP-EE-DG-2026",
+ "promotionId": 136,
+ "redeemedQty": 1,
+ "redemptionId": 31,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "d3c6cefe-c6b7-4c0c-8727-90624d98ff1e",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "c2738100-19d4-4f6d-852a-f9505413ab00",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "qa-nicolefeng",
+ "voidedDtm": "2026-04-01T12:36:47.461901",
+ "voidedGamingDate": "",
+ "voidedRemark": "test",
+ "displayOnlyPromo": false,
+ "isVoided": true,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-01",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-01T11:43:51.410112",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "(Breakfast)LP-EE-DG-1PTS",
+ "prizeRuleId": 575,
+ "promotionCode": "LP-EE-DG-2026",
+ "promotionId": 136,
+ "redeemedQty": 1,
+ "redemptionId": 21,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "3278409c-a1e6-4d33-89ee-96e604ceec10",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "a1303a54-a30a-4665-bb0f-d05cca1fc0a9",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "qa-nicolefeng",
+ "voidedDtm": "2026-04-01T11:45:57.557918",
+ "voidedGamingDate": "",
+ "voidedRemark": "test",
+ "displayOnlyPromo": false,
+ "isVoided": true,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-03",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-03T15:24:46.862990",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "DG-3PTS-Supper-GLPCafe",
+ "prizeRuleId": 683,
+ "promotionCode": "LP-EE-DG-2026-Q2",
+ "promotionId": 141,
+ "redeemedQty": 1,
+ "redemptionId": 102,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-3pts-glpcafe(supper)-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "29e9d69c-5927-483f-b710-6d3e1ed437d4",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "a40c221a-2c4b-495e-a5f5-d58bdfd3025f",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-3pts-glpcafe(supper)-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLPCafe Supper meal voucher",
+ "description": "GLPCafe Supper meal voucher",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 19.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-02-24T17:29:21.603715+08:00",
+ "updatedBy": "HUB_MIGRATION",
+ "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": 68.0000,
+ "m-comp": 68.0000
+ },
+ "compValue": 68.0000,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-02",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-02T16:32:52.556719",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "(Breakfast)LP-EE-DG-1PTS",
+ "prizeRuleId": 575,
+ "promotionCode": "LP-EE-DG-2026",
+ "promotionId": 136,
+ "redeemedQty": 1,
+ "redemptionId": 72,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "f861422d-3fc3-4ecd-af8c-5e1cc7f60e00",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "6a7a8c4e-2f41-4752-929a-d23c25ee258d",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-06",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-04T17:12:51.586106",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "qa_1_point_rule",
+ "prizeRuleId": 107,
+ "promotionCode": "qa_1_point",
+ "promotionId": 26,
+ "redeemedQty": 1,
+ "redemptionId": 145,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it-test-nnpc-$1",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "43a9ab81-bbff-4812-bd64-bfd4eaa50d72",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "0fda5bbf-c596-4131-9d8f-a4245b74c585",
+ "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.00,
+ "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": "2026-03-19T21:45:07.743781+08:00",
+ "updatedBy": "v-wt-mark",
+ "updatedByEmployeeId": "123456",
+ "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": {
+ "d-comp": 1,
+ "m-comp": 1
+ },
+ "compValue": 1,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "qa-randyng",
+ "voidedByEmployeeId": "171853",
+ "voidedDtm": "2026-04-06T16:11:05.584548",
+ "voidedGamingDate": "",
+ "voidedRemark": "qa",
+ "displayOnlyPromo": false,
+ "isVoided": true,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-04",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-04T10:25:35.548072",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "(Breakfast)LP-EE-DG-1PTS",
+ "prizeRuleId": 575,
+ "promotionCode": "LP-EE-DG-2026",
+ "promotionId": 136,
+ "redeemedQty": 1,
+ "redemptionId": 115,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "7cfd14b0-28b6-4f03-a970-83b4398f96d5",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "2f10cd73-c3ac-452a-b565-7090f112d13b",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-03",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-03T11:37:51.585467",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "DG-1pt-Breakfast",
+ "prizeRuleId": 663,
+ "promotionCode": "LP-EE-DG-2026-Q2",
+ "promotionId": 141,
+ "redeemedQty": 1,
+ "redemptionId": 100,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "5bf9e6a3-b13f-46bf-a174-710cd340da25",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "f416a1c3-164f-4cb6-85fa-410559622eee",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-03",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-03T11:16:39.906261",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "(Breakfast)LP-EE-DG-1PTS",
+ "prizeRuleId": 575,
+ "promotionCode": "LP-EE-DG-2026",
+ "promotionId": 136,
+ "redeemedQty": 1,
+ "redemptionId": 99,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-1pts-breakfast-1-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "9cff9695-da67-4b45-a965-91802727d8a2",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "39921e98-42ac-4285-bf0f-238abb26130e",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-1pts-breakfast-1-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Breakfast set voucher-1",
+ "description": "GLP Breakfast set voucher-1",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 6.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-02-24T17:29:21.603715+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2026-03-31T16:58:41.433528+08:00",
+ "updatedBy": "qa-nicolefeng",
+ "updatedByEmployeeId": "173094",
+ "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": 38,
+ "m-comp": 38
+ },
+ "compValue": 38,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-03",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-03T16:02:49.829413",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "Rule_IT-PromoTagB",
+ "prizeRuleId": 77,
+ "promotionCode": "IT-PromoTagB",
+ "promotionId": 16,
+ "redeemedQty": 1,
+ "redemptionId": 104,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-it-mqa-glpusb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "e0043e83-0ec5-4984-bb46-636ce551e0d4",
+ "issueTargetToWallet": false,
+ "walletItemPublicId": null,
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-it-mqa-glpusb",
+ "behaviorProfileCode": "OTHERS_AUTOSETTLE",
+ "name": "LP-IT-MQA-GLPUSB",
+ "description": "LP-IT-MQA-GLPUSB",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 80.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P50D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "Others",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "EXTERNAL_ONLY",
+ "allowNegativeBalance": false,
+ "createdAt": "2025-11-18T00:09:10.317981+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2025-11-18T00:09:10.317981+08:00",
+ "updatedBy": "HUB_MIGRATION",
+ "updatedByEmployeeId": null,
+ "tags": null,
+ "behaviorProfile": {
+ "profileCode": "OTHERS_AUTOSETTLE",
+ "profileName": "Others (AutoSettle)",
+ "prizeCategory": "Others",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "EXTERNAL_ONLY",
+ "description": "Other target-only item auto-settled externally",
+ "isActive": true,
+ "allowNegativeBalance": false,
+ "isBucket": false,
+ "isEbonus": false
+ },
+ "localizations": null,
+ "messages": null,
+ "isPrintVoucher": null,
+ "isExcludeGlobalMessages": null,
+ "effectiveFrom": null,
+ "effectiveTo": null,
+ "bucketPrices": {
+ "d-comp": 0,
+ "m-comp": 0
+ },
+ "compValue": 0,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": true
+ },
+ {
+ "gamingDate": "2026-04-03",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-03T16:03:50.119019",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "(DimSum)LP-EE-DG-5PTS ",
+ "prizeRuleId": 577,
+ "promotionCode": "LP-EE-DG-2026",
+ "promotionId": 136,
+ "redeemedQty": 1,
+ "redemptionId": 105,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-dg-5pts-dimsum-fb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "85a5c752-2a7f-4967-b0de-5a93efae6a63",
+ "issueTargetToWallet": true,
+ "walletItemPublicId": "d47e8c62-1e93-45d2-b6d4-99792312162f",
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-dg-5pts-dimsum-fb",
+ "behaviorProfileCode": "FNB_OPEN_ITEM",
+ "name": "GLP Dim Sum set voucher",
+ "description": "GLP Dim Sum set voucher",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 40.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P2D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2025-12-05T20:04:41.510899+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2025-12-05T20:04:41.510899+08:00",
+ "updatedBy": "HUB_MIGRATION",
+ "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": 120.0000,
+ "m-comp": 120.0000
+ },
+ "compValue": 120.0000,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": false
+ },
+ {
+ "gamingDate": "2026-04-04",
+ "normalPromo": true,
+ "postedBy": "qa-randyng",
+ "employeeId": "171853",
+ "postedDtm": "2026-04-04T17:12:11.095441",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "BOH-DT210716",
+ "ipAddress": "10.73.210.11",
+ "prizeRuleCode": "Rule_IT-PromoTagB",
+ "prizeRuleId": 77,
+ "promotionCode": "IT-PromoTagB",
+ "promotionId": 16,
+ "redeemedQty": 1,
+ "redemptionId": 144,
+ "remark": null,
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-it-mqa-glpusb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": null,
+ "transId": "b10e1c54-ffa8-4746-a71d-61fb4db78489",
+ "issueTargetToWallet": false,
+ "walletItemPublicId": null,
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-it-mqa-glpusb",
+ "behaviorProfileCode": "OTHERS_AUTOSETTLE",
+ "name": "LP-IT-MQA-GLPUSB",
+ "description": "LP-IT-MQA-GLPUSB",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 80.00,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P50D"
+ }
+ ],
+ "isEnabled": true
+ },
+ "prizeCategory": "Others",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "EXTERNAL_ONLY",
+ "allowNegativeBalance": false,
+ "createdAt": "2025-11-18T00:09:10.317981+08:00",
+ "createdBy": "HUB_MIGRATION",
+ "createdByEmployeeId": null,
+ "updatedAt": "2025-11-18T00:09:10.317981+08:00",
+ "updatedBy": "HUB_MIGRATION",
+ "updatedByEmployeeId": null,
+ "tags": null,
+ "behaviorProfile": {
+ "profileCode": "OTHERS_AUTOSETTLE",
+ "profileName": "Others (AutoSettle)",
+ "prizeCategory": "Others",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "EXTERNAL_ONLY",
+ "description": "Other target-only item auto-settled externally",
+ "isActive": true,
+ "allowNegativeBalance": false,
+ "isBucket": false,
+ "isEbonus": false
+ },
+ "localizations": null,
+ "messages": null,
+ "isPrintVoucher": null,
+ "isExcludeGlobalMessages": null,
+ "effectiveFrom": null,
+ "effectiveTo": null,
+ "bucketPrices": {
+ "d-comp": 0,
+ "m-comp": 0
+ },
+ "compValue": 0,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false,
+ "isDirectOffer": true
+ }
+ ]
+ }
+}
diff --git a/fixtures/member.profile.json b/fixtures/member.profile.json
new file mode 100644
index 0000000..ef2eb60
--- /dev/null
+++ b/fixtures/member.profile.json
@@ -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": []
+ }
+}
diff --git a/fixtures/promotions.backdate.800038008.json b/fixtures/promotions.backdate.800038008.json
new file mode 100644
index 0000000..989fe06
--- /dev/null
+++ b/fixtures/promotions.backdate.800038008.json
@@ -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": []
+ }
+}
diff --git a/fixtures/promotions.capture.10766.json b/fixtures/promotions.capture.10766.json
new file mode 100644
index 0000000..baeda4b
--- /dev/null
+++ b/fixtures/promotions.capture.10766.json
@@ -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": []
+ }
+}
diff --git a/fixtures/promotions.capture.10852.json b/fixtures/promotions.capture.10852.json
new file mode 100644
index 0000000..93c2ced
--- /dev/null
+++ b/fixtures/promotions.capture.10852.json
@@ -0,0 +1,1066 @@
+Task 10852: Dedicated entitled promotion fixture for member 80010852.
+Only this fixed member should see an enabled redeem button for the verified multi-outcome rule.
+{
+ "success": true,
+ "message": "Get all promotions successfully.",
+ "data": {
+ "customerId": "80010852",
+ "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": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 999,
+ "earnedPoints": 188,
+ "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": 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": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-cnygs26-188pts-martellnoblige-me",
+ "behaviorProfileCode": "RETAIL_OPEN_ITEM",
+ "name": "Martell Noblige",
+ "description": "Martell Noblige promotional prize",
+ "source": "EXTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 188,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P90D"
+ }
+ ],
+ "isEnabled": false
+ },
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-01-27T06:00:00+08:00",
+ "createdBy": "mock-gateway",
+ "updatedAt": "2026-01-27T06:00:00+08:00",
+ "updatedBy": "mock-gateway",
+ "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": false,
+ "isExcludeGlobalMessages": false,
+ "effectiveFrom": "2026-01-27T06:00:00+08:00",
+ "effectiveTo": "2026-04-30T06:00:00+08:00",
+ "bucketPrices": {},
+ "compValue": null,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 71768073,
+ "value": "lp-ee-cnygs26-188pts-lchabalone-me",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-cnygs26-188pts-lchabalone-me",
+ "behaviorProfileCode": "RETAIL_OPEN_ITEM",
+ "name": "LCH Can Abalone",
+ "description": "LCH canned abalone promotional prize",
+ "source": "EXTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 188,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P90D"
+ }
+ ],
+ "isEnabled": false
+ },
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-01-27T06:00:00+08:00",
+ "createdBy": "mock-gateway",
+ "updatedAt": "2026-01-27T06:00:00+08:00",
+ "updatedBy": "mock-gateway",
+ "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": false,
+ "isExcludeGlobalMessages": false,
+ "effectiveFrom": "2026-01-27T06:00:00+08:00",
+ "effectiveTo": "2026-04-30T06:00:00+08:00",
+ "bucketPrices": {},
+ "compValue": null,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "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": [
+ {
+ "awardUsedLimit": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 999,
+ "earnedPoints": 188,
+ "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": 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": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-cnygs26-188pts-martellnoblige-me",
+ "behaviorProfileCode": "RETAIL_OPEN_ITEM",
+ "name": "Martell Noblige",
+ "description": "Martell Noblige promotional prize",
+ "source": "EXTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 188,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P90D"
+ }
+ ],
+ "isEnabled": false
+ },
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-01-27T06:00:00+08:00",
+ "createdBy": "mock-gateway",
+ "updatedAt": "2026-01-27T06:00:00+08:00",
+ "updatedBy": "mock-gateway",
+ "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": false,
+ "isExcludeGlobalMessages": false,
+ "effectiveFrom": "2026-01-27T06:00:00+08:00",
+ "effectiveTo": "2026-04-30T06:00:00+08:00",
+ "bucketPrices": {},
+ "compValue": null,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 71768073,
+ "value": "lp-ee-cnygs26-188pts-lchabalone-me",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "correlationId": null,
+ "prizeCode": "lp-ee-cnygs26-188pts-lchabalone-me",
+ "behaviorProfileCode": "RETAIL_OPEN_ITEM",
+ "name": "LCH Can Abalone",
+ "description": "LCH canned abalone promotional prize",
+ "source": "EXTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "imageUrl": null,
+ "prizeValue": 188,
+ "expiryConfig": {
+ "strategy": "MIN",
+ "rules": [
+ {
+ "type": "ROLLING_DURATION",
+ "value": "P90D"
+ }
+ ],
+ "isEnabled": false
+ },
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "createdAt": "2026-01-27T06:00:00+08:00",
+ "createdBy": "mock-gateway",
+ "updatedAt": "2026-01-27T06:00:00+08:00",
+ "updatedBy": "mock-gateway",
+ "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": false,
+ "isExcludeGlobalMessages": false,
+ "effectiveFrom": "2026-01-27T06:00:00+08:00",
+ "effectiveTo": "2026-04-30T06:00:00+08:00",
+ "bucketPrices": {},
+ "compValue": null,
+ "note": null,
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/fixtures/promotions.capture.800038008.json b/fixtures/promotions.capture.800038008.json
new file mode 100644
index 0000000..b357f2e
--- /dev/null
+++ b/fixtures/promotions.capture.800038008.json
@@ -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": []
+ }
+}
diff --git a/fixtures/promotions.capture.800048718.json b/fixtures/promotions.capture.800048718.json
new file mode 100644
index 0000000..464a31e
--- /dev/null
+++ b/fixtures/promotions.capture.800048718.json
@@ -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
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/fixtures/promotions.capture.80010991.json b/fixtures/promotions.capture.80010991.json
new file mode 100644
index 0000000..1d9143c
--- /dev/null
+++ b/fixtures/promotions.capture.80010991.json
@@ -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": []
+ }
+}
diff --git a/fixtures/promotions.capture.80038008.json b/fixtures/promotions.capture.80038008.json
new file mode 100644
index 0000000..da18100
--- /dev/null
+++ b/fixtures/promotions.capture.80038008.json
@@ -0,0 +1,1425 @@
+{
+ "success": true,
+ "message": "Get all promotions successfully.",
+ "data": {
+ "customerId": "80038008",
+ "eligiblePromotionList": [],
+ "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"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 2,
+ "awardsCount": 1,
+ "awardsEligible": 11,
+ "awardsEntitled": 11,
+ "awardsRedeemLimit": 11,
+ "earnedPoints": 25,
+ "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": 38018,
+ "code": "PROMO-MEGA-CHOICE-2026-A",
+ "name": "Mega Choice Promotion A",
+ "ruleId": 3801801,
+ "ruleName": "Mega Choice A - choose 1 from 10 prizes",
+ "ruleNameDesc": "Mega Choice A - choose 1 from 10 prizes",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38018011,
+ "value": "it_fb_100",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "it_fb_100",
+ "name": "Mega Choice A FnB Voucher $10",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 10,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018012,
+ "value": "it_fb_150",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "it_fb_150",
+ "name": "Mega Choice A FnB Voucher $20",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 20,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 20,
+ "m-comp": 20
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018013,
+ "value": "nn-test-package-nnpc",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "nn-test-package-nnpc",
+ "name": "Mega Choice A NNPC Voucher $10",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 2000,
+ "prizeCategory": "NNPC",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018014,
+ "value": "lp-ee-do-jul2021-20pts-100pc",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "lp-ee-do-jul2021-20pts-100pc",
+ "name": "Mega Choice A NNPC Voucher $30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "NNPC",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018015,
+ "value": "ebonus_1",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "ebonus_1",
+ "name": "Mega Choice A eBonus 5",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 5,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018016,
+ "value": "ebonus",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "ebonus",
+ "name": "Mega Choice A eBonus 15",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 15,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018017,
+ "value": "reward-points",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "reward-points",
+ "name": "Mega Choice A Free Play 10",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 10,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 10,
+ "m-comp": 10
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018018,
+ "value": "reward-points2",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "reward-points2",
+ "name": "Mega Choice A Free Play 25",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 25,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 25,
+ "m-comp": 25
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018019,
+ "value": "opera_hotel",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "opera_hotel",
+ "name": "Mega Choice A Hotel Credit 50",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 50,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 50,
+ "m-comp": 50
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018020,
+ "value": "sjm-lm-tb-noble-spa",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "sjm-lm-tb-noble-spa",
+ "name": "Mega Choice A Spa Credit 80",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 80,
+ "prizeCategory": "Spa",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 80,
+ "m-comp": 80
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 36,
+ "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": 38028,
+ "code": "PROMO-MEGA-CHOICE-2026-B",
+ "name": "Mega Choice Promotion B",
+ "ruleId": 3802801,
+ "ruleName": "Mega Choice B - choose 1 from 10 prizes",
+ "ruleNameDesc": "Mega Choice B - choose 1 from 10 prizes",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38028011,
+ "value": "it_fb_300",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "it_fb_300",
+ "name": "Mega Choice B FnB Voucher $12",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 12,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 12,
+ "m-comp": 12
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028012,
+ "value": "it_fb_350",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "it_fb_350",
+ "name": "Mega Choice B FnB Voucher $24",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 24,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 24,
+ "m-comp": 24
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028013,
+ "value": "mqa-openprizea",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-openprizea",
+ "name": "Mega Choice B Retail Coupon 30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 30,
+ "m-comp": 30
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028014,
+ "value": "mqa-openprizeb",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-openprizeb",
+ "name": "Mega Choice B Retail Coupon 50",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 50,
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 50,
+ "m-comp": 50
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028015,
+ "value": "rewardapp-prizea",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "rewardapp-prizea",
+ "name": "Mega Choice B Movie Ticket x2",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 2,
+ "prizeCategory": "Entertainment",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 2,
+ "m-comp": 2
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028016,
+ "value": "mqa-expiryprizea",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-expiryprizea",
+ "name": "Mega Choice B Movie Ticket x4",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 4,
+ "prizeCategory": "Entertainment",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 4,
+ "m-comp": 4
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028017,
+ "value": "mqa-gisprizea",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-gisprizea",
+ "name": "Mega Choice B Taxi Credit 30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "Transport",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 30,
+ "m-comp": 30
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028018,
+ "value": "mqa-gisprizeb",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-gisprizeb",
+ "name": "Mega Choice B Taxi Credit 60",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 60,
+ "prizeCategory": "Transport",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 60,
+ "m-comp": 60
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028019,
+ "value": "nn-test-hotel",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "nn-test-hotel",
+ "name": "Mega Choice B Hotel Credit 88",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 88,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 88,
+ "m-comp": 88
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028020,
+ "value": "sjm-lm-tb-novel-spa",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "sjm-lm-tb-novel-spa",
+ "name": "Mega Choice B Spa Credit 108",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 108,
+ "prizeCategory": "Spa",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 108,
+ "m-comp": 108
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 30,
+ "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": 38038,
+ "code": "PROMO-MEGA-CHOICE-2026-C",
+ "name": "Mega Choice Promotion C",
+ "ruleId": 3803801,
+ "ruleName": "Mega Choice C - choose 1 from 10 prizes",
+ "ruleNameDesc": "Mega Choice C - choose 1 from 10 prizes",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38038011,
+ "value": "it_fb_650",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "it_fb_650",
+ "name": "Mega Choice C FnB Voucher $15",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 15,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 15,
+ "m-comp": 15
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038012,
+ "value": "it_fb_1000",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "it_fb_1000",
+ "name": "Mega Choice C FnB Voucher $30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 30,
+ "m-comp": 30
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038013,
+ "value": "mqa-openprizec",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-openprizec",
+ "name": "Mega Choice C Free Play 20",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 20,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 20,
+ "m-comp": 20
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038014,
+ "value": "mqa-gisprizec",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-gisprizec",
+ "name": "Mega Choice C Free Play 40",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 40,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 40,
+ "m-comp": 40
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038015,
+ "value": "ebounsopen",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "ebounsopen",
+ "name": "Mega Choice C eBonus 20",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 20,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 20,
+ "m-comp": 20
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038016,
+ "value": "lp-ee-peg-jul2021-w1-3500eb",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "lp-ee-peg-jul2021-w1-3500eb",
+ "name": "Mega Choice C eBonus 40",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 40,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 40,
+ "m-comp": 40
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038017,
+ "value": "uat-gl-u2-bd",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "uat-gl-u2-bd",
+ "name": "Mega Choice C Hotel Credit 120",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 120,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 120,
+ "m-comp": 120
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038018,
+ "value": "uat-gl-iphone17",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "uat-gl-iphone17",
+ "name": "Mega Choice C Suite Upgrade",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 1,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 1,
+ "m-comp": 1
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038019,
+ "value": "mqa-expiryprizec",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-expiryprizec",
+ "name": "Mega Choice C Spa Credit 128",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 128,
+ "prizeCategory": "Spa",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 128,
+ "m-comp": 128
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038020,
+ "value": "mqa-expiryprizeb",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-expiryprizeb",
+ "name": "Mega Choice C Show Ticket x2",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 2,
+ "prizeCategory": "Entertainment",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 2,
+ "m-comp": 2
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 1,
+ "earnedPoints": 3,
+ "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": 38048,
+ "code": "PROMO-AUTOSETTLE-2026",
+ "name": "Auto Settle eBonus Promotion",
+ "ruleId": 3804801,
+ "ruleName": "Auto settle eBonus reward",
+ "ruleNameDesc": "Auto settle eBonus reward",
+ "toEarn1Unit": 3,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38048011,
+ "value": "qa-ebonus-auto",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "qa-ebonus-auto",
+ "name": "Auto eBonus 30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "EXTERNAL_ONLY",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "EXTERNAL_ONLY"
+ },
+ "bucketPrices": {},
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 24,
+ "isDirectOffer": false,
+ "isEligible": true,
+ "isEntitled": true,
+ "isOptIn": false,
+ "isOptedInByPlayer": false,
+ "isRedeemCompleted": false,
+ "maxOutcome": 2,
+ "minOutcome": 2,
+ "startDate": "2026-01-01T00:00",
+ "endDate": "2036-12-31T23:59",
+ "id": 38058,
+ "code": "PROMO-REDEEM-ALL-2026",
+ "name": "Redeem All Package Promotion",
+ "ruleId": 3805801,
+ "ruleName": "Redeem both package prizes together",
+ "ruleNameDesc": "Redeem both package prizes together",
+ "toEarn1Unit": 20,
+ "isRedeemAll": true,
+ "outcomeList": [
+ {
+ "key": 38058011,
+ "value": "it_mealset",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "it_mealset",
+ "name": "Package FnB Voucher $15",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 15,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 15,
+ "m-comp": 15
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38058012,
+ "value": "it-test-coca",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "it-test-coca",
+ "name": "Package Hotel Credit 60",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 60,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 60,
+ "m-comp": 60
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 1,
+ "earnedPoints": 6,
+ "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": 38068,
+ "code": "PROMO-DINING-PLUS-2026",
+ "name": "Dining Plus Reward",
+ "ruleId": 3806801,
+ "ruleName": "Dining Plus single voucher",
+ "ruleNameDesc": "Dining Plus single voucher",
+ "toEarn1Unit": 6,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38068011,
+ "value": "lp-ee-do-jul2021-10pts-set-fb",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "lp-ee-do-jul2021-10pts-set-fb",
+ "name": "Dining Plus Voucher $25",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 25,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 25,
+ "m-comp": 25
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 20,
+ "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": 38078,
+ "code": "PROMO-HOTEL-STAY-2026",
+ "name": "Hotel Stay Credit Promotion",
+ "ruleId": 3807801,
+ "ruleName": "Hotel stay credit reward",
+ "ruleNameDesc": "Hotel stay credit reward",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38078011,
+ "value": "uat-gl-u2-bl",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "uat-gl-u2-bl",
+ "name": "Hotel Credit 100",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 100,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 100,
+ "m-comp": 100
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "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-01T00:00",
+ "endDate": "2036-12-31T23:59",
+ "id": 38088,
+ "code": "PROMO-FREEPLAY-BOOST-2026",
+ "name": "Free Play Booster",
+ "ruleId": 3808801,
+ "ruleName": "Free play single reward",
+ "ruleNameDesc": "Free play single reward",
+ "toEarn1Unit": 8,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38088011,
+ "value": "lp-ee-nsuwr-5pts-20-fb",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "lp-ee-nsuwr-5pts-20-fb",
+ "name": "Free Play 50",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 50,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 50,
+ "m-comp": 50
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 42,
+ "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": 38098,
+ "code": "PROMO-SPA-TREAT-2026",
+ "name": "Spa Treat Promotion",
+ "ruleId": 3809801,
+ "ruleName": "Spa reward single item",
+ "ruleNameDesc": "Spa reward single item",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38098011,
+ "value": "mqa-expiryprized",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mqa-expiryprized",
+ "name": "Spa Credit 150",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 150,
+ "prizeCategory": "Spa",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 150,
+ "m-comp": 150
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "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-12-31T23:59",
+ "id": 38108,
+ "code": "PROMO-VIP-TRANSPORT-2026",
+ "name": "VIP Transport Reward",
+ "ruleId": 3810801,
+ "ruleName": "Transport reward single item",
+ "ruleNameDesc": "Transport reward single item",
+ "toEarn1Unit": 5,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38108011,
+ "value": "uat-gl-eco-ferry",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "uat-gl-eco-ferry",
+ "name": "VIP Limo Transfer",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 1,
+ "prizeCategory": "Transport",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 1,
+ "m-comp": 1
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 1,
+ "earnedPoints": 7,
+ "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": 38118,
+ "code": "PROMO-RETAIL-COUPON-2026",
+ "name": "Retail Coupon Reward",
+ "ruleId": 3811801,
+ "ruleName": "Retail reward single item",
+ "ruleNameDesc": "Retail reward single item",
+ "toEarn1Unit": 7,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38118011,
+ "value": "gl-mkt-fnb-gl_estoril",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "gl-mkt-fnb-gl_estoril",
+ "name": "Retail Coupon 40",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 40,
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 40,
+ "m-comp": 40
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/fixtures/promotions.capture.80038008.json.bak.20260505173439 b/fixtures/promotions.capture.80038008.json.bak.20260505173439
new file mode 100644
index 0000000..e734736
--- /dev/null
+++ b/fixtures/promotions.capture.80038008.json.bak.20260505173439
@@ -0,0 +1,1435 @@
+{
+ "success": true,
+ "message": "Get all promotions successfully.",
+ "data": {
+ "customerId": "80038008",
+ "eligiblePromotionList": [],
+ "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"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 2,
+ "awardsCount": 1,
+ "awardsEligible": 3,
+ "awardsEntitled": 3,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 25,
+ "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": 38018,
+ "code": "PROMO-MEGA-CHOICE-2026-A",
+ "name": "Mega Choice Promotion A",
+ "ruleId": 3801801,
+ "ruleName": "Mega Choice A - choose 1 from 10 prizes",
+ "ruleNameDesc": "Mega Choice A - choose 1 from 10 prizes",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38018011,
+ "value": "mock-mega-choice-a-fb-10",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-fb-10",
+ "name": "Mega Choice A FnB Voucher $10",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 10,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 10,
+ "m-comp": 10
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018012,
+ "value": "mock-mega-choice-a-fb-20",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-fb-20",
+ "name": "Mega Choice A FnB Voucher $20",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 20,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 20,
+ "m-comp": 20
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018013,
+ "value": "mock-mega-choice-a-nnpc-10",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-nnpc-10",
+ "name": "Mega Choice A NNPC Voucher $10",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 10,
+ "prizeCategory": "NNPC",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 10,
+ "m-comp": 10
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018014,
+ "value": "mock-mega-choice-a-nnpc-30",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-nnpc-30",
+ "name": "Mega Choice A NNPC Voucher $30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "NNPC",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 30,
+ "m-comp": 30
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018015,
+ "value": "mock-mega-choice-a-ebonus-5",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-ebonus-5",
+ "name": "Mega Choice A eBonus 5",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 5,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 5,
+ "m-comp": 5
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018016,
+ "value": "mock-mega-choice-a-ebonus-15",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-ebonus-15",
+ "name": "Mega Choice A eBonus 15",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 15,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 15,
+ "m-comp": 15
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018017,
+ "value": "mock-mega-choice-a-freeplay-10",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-freeplay-10",
+ "name": "Mega Choice A Free Play 10",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 10,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 10,
+ "m-comp": 10
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018018,
+ "value": "mock-mega-choice-a-freeplay-25",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-freeplay-25",
+ "name": "Mega Choice A Free Play 25",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 25,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 25,
+ "m-comp": 25
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018019,
+ "value": "mock-mega-choice-a-hotel-50",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-hotel-50",
+ "name": "Mega Choice A Hotel Credit 50",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 50,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 50,
+ "m-comp": 50
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38018020,
+ "value": "mock-mega-choice-a-spa-80",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-a-spa-80",
+ "name": "Mega Choice A Spa Credit 80",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 80,
+ "prizeCategory": "Spa",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 80,
+ "m-comp": 80
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 36,
+ "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": 38028,
+ "code": "PROMO-MEGA-CHOICE-2026-B",
+ "name": "Mega Choice Promotion B",
+ "ruleId": 3802801,
+ "ruleName": "Mega Choice B - choose 1 from 10 prizes",
+ "ruleNameDesc": "Mega Choice B - choose 1 from 10 prizes",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38028011,
+ "value": "mock-mega-choice-b-fb-12",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-fb-12",
+ "name": "Mega Choice B FnB Voucher $12",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 12,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 12,
+ "m-comp": 12
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028012,
+ "value": "mock-mega-choice-b-fb-24",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-fb-24",
+ "name": "Mega Choice B FnB Voucher $24",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 24,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 24,
+ "m-comp": 24
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028013,
+ "value": "mock-mega-choice-b-retail-30",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-retail-30",
+ "name": "Mega Choice B Retail Coupon 30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 30,
+ "m-comp": 30
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028014,
+ "value": "mock-mega-choice-b-retail-50",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-retail-50",
+ "name": "Mega Choice B Retail Coupon 50",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 50,
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 50,
+ "m-comp": 50
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028015,
+ "value": "mock-mega-choice-b-movie-2",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-movie-2",
+ "name": "Mega Choice B Movie Ticket x2",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 2,
+ "prizeCategory": "Entertainment",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 2,
+ "m-comp": 2
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028016,
+ "value": "mock-mega-choice-b-movie-4",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-movie-4",
+ "name": "Mega Choice B Movie Ticket x4",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 4,
+ "prizeCategory": "Entertainment",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 4,
+ "m-comp": 4
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028017,
+ "value": "mock-mega-choice-b-taxi-30",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-taxi-30",
+ "name": "Mega Choice B Taxi Credit 30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "Transport",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 30,
+ "m-comp": 30
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028018,
+ "value": "mock-mega-choice-b-taxi-60",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-taxi-60",
+ "name": "Mega Choice B Taxi Credit 60",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 60,
+ "prizeCategory": "Transport",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 60,
+ "m-comp": 60
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028019,
+ "value": "mock-mega-choice-b-hotel-88",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-hotel-88",
+ "name": "Mega Choice B Hotel Credit 88",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 88,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 88,
+ "m-comp": 88
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38028020,
+ "value": "mock-mega-choice-b-spa-108",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-b-spa-108",
+ "name": "Mega Choice B Spa Credit 108",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 108,
+ "prizeCategory": "Spa",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 108,
+ "m-comp": 108
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 30,
+ "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": 38038,
+ "code": "PROMO-MEGA-CHOICE-2026-C",
+ "name": "Mega Choice Promotion C",
+ "ruleId": 3803801,
+ "ruleName": "Mega Choice C - choose 1 from 10 prizes",
+ "ruleNameDesc": "Mega Choice C - choose 1 from 10 prizes",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38038011,
+ "value": "mock-mega-choice-c-fb-15",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-fb-15",
+ "name": "Mega Choice C FnB Voucher $15",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 15,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 15,
+ "m-comp": 15
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038012,
+ "value": "mock-mega-choice-c-fb-30",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-fb-30",
+ "name": "Mega Choice C FnB Voucher $30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 30,
+ "m-comp": 30
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038013,
+ "value": "mock-mega-choice-c-freeplay-20",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-freeplay-20",
+ "name": "Mega Choice C Free Play 20",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 20,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 20,
+ "m-comp": 20
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038014,
+ "value": "mock-mega-choice-c-freeplay-40",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-freeplay-40",
+ "name": "Mega Choice C Free Play 40",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 40,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 40,
+ "m-comp": 40
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038015,
+ "value": "mock-mega-choice-c-ebonus-20",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-ebonus-20",
+ "name": "Mega Choice C eBonus 20",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 20,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 20,
+ "m-comp": 20
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038016,
+ "value": "mock-mega-choice-c-ebonus-40",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-ebonus-40",
+ "name": "Mega Choice C eBonus 40",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 40,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 40,
+ "m-comp": 40
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038017,
+ "value": "mock-mega-choice-c-hotel-120",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-hotel-120",
+ "name": "Mega Choice C Hotel Credit 120",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 120,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 120,
+ "m-comp": 120
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038018,
+ "value": "mock-mega-choice-c-suite-1",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-suite-1",
+ "name": "Mega Choice C Suite Upgrade",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 1,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 1,
+ "m-comp": 1
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038019,
+ "value": "mock-mega-choice-c-spa-128",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-spa-128",
+ "name": "Mega Choice C Spa Credit 128",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 128,
+ "prizeCategory": "Spa",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 128,
+ "m-comp": 128
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38038020,
+ "value": "mock-mega-choice-c-show-2",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-mega-choice-c-show-2",
+ "name": "Mega Choice C Show Ticket x2",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 2,
+ "prizeCategory": "Entertainment",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 2,
+ "m-comp": 2
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 1,
+ "earnedPoints": 3,
+ "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": 38048,
+ "code": "PROMO-AUTOSETTLE-2026",
+ "name": "Auto Settle eBonus Promotion",
+ "ruleId": 3804801,
+ "ruleName": "Auto settle eBonus reward",
+ "ruleNameDesc": "Auto settle eBonus reward",
+ "toEarn1Unit": 3,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38048011,
+ "value": "mock-auto-ebonus-30",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-auto-ebonus-30",
+ "name": "Auto eBonus 30",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 30,
+ "prizeCategory": "eBonus",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "EXTERNAL_ONLY",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "EXTERNAL_ONLY"
+ },
+ "bucketPrices": {},
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 24,
+ "isDirectOffer": false,
+ "isEligible": true,
+ "isEntitled": true,
+ "isOptIn": false,
+ "isOptedInByPlayer": false,
+ "isRedeemCompleted": false,
+ "maxOutcome": 2,
+ "minOutcome": 2,
+ "startDate": "2026-01-01T00:00",
+ "endDate": "2036-12-31T23:59",
+ "id": 38058,
+ "code": "PROMO-REDEEM-ALL-2026",
+ "name": "Redeem All Package Promotion",
+ "ruleId": 3805801,
+ "ruleName": "Redeem both package prizes together",
+ "ruleNameDesc": "Redeem both package prizes together",
+ "toEarn1Unit": 20,
+ "isRedeemAll": true,
+ "outcomeList": [
+ {
+ "key": 38058011,
+ "value": "mock-package-fb-15",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-package-fb-15",
+ "name": "Package FnB Voucher $15",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 15,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 15,
+ "m-comp": 15
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ },
+ {
+ "key": 38058012,
+ "value": "mock-package-hotel-60",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-package-hotel-60",
+ "name": "Package Hotel Credit 60",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 60,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 60,
+ "m-comp": 60
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 1,
+ "earnedPoints": 6,
+ "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": 38068,
+ "code": "PROMO-DINING-PLUS-2026",
+ "name": "Dining Plus Reward",
+ "ruleId": 3806801,
+ "ruleName": "Dining Plus single voucher",
+ "ruleNameDesc": "Dining Plus single voucher",
+ "toEarn1Unit": 6,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38068011,
+ "value": "mock-dining-plus-25",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-dining-plus-25",
+ "name": "Dining Plus Voucher $25",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 25,
+ "prizeCategory": "FnB",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 25,
+ "m-comp": 25
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 20,
+ "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": 38078,
+ "code": "PROMO-HOTEL-STAY-2026",
+ "name": "Hotel Stay Credit Promotion",
+ "ruleId": 3807801,
+ "ruleName": "Hotel stay credit reward",
+ "ruleNameDesc": "Hotel stay credit reward",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38078011,
+ "value": "mock-hotel-credit-100",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-hotel-credit-100",
+ "name": "Hotel Credit 100",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 100,
+ "prizeCategory": "Hotel",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 100,
+ "m-comp": 100
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "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-01T00:00",
+ "endDate": "2036-12-31T23:59",
+ "id": 38088,
+ "code": "PROMO-FREEPLAY-BOOST-2026",
+ "name": "Free Play Booster",
+ "ruleId": 3808801,
+ "ruleName": "Free play single reward",
+ "ruleNameDesc": "Free play single reward",
+ "toEarn1Unit": 8,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38088011,
+ "value": "mock-freeplay-50",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-freeplay-50",
+ "name": "Free Play 50",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 50,
+ "prizeCategory": "FreePlay",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 50,
+ "m-comp": 50
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 1,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 5,
+ "earnedPoints": 42,
+ "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": 38098,
+ "code": "PROMO-SPA-TREAT-2026",
+ "name": "Spa Treat Promotion",
+ "ruleId": 3809801,
+ "ruleName": "Spa reward single item",
+ "ruleNameDesc": "Spa reward single item",
+ "toEarn1Unit": 20,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38098011,
+ "value": "mock-spa-credit-150",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-spa-credit-150",
+ "name": "Spa Credit 150",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 150,
+ "prizeCategory": "Spa",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 150,
+ "m-comp": 150
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "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-12-31T23:59",
+ "id": 38108,
+ "code": "PROMO-VIP-TRANSPORT-2026",
+ "name": "VIP Transport Reward",
+ "ruleId": 3810801,
+ "ruleName": "Transport reward single item",
+ "ruleNameDesc": "Transport reward single item",
+ "toEarn1Unit": 5,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38108011,
+ "value": "mock-vip-limo-1",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-vip-limo-1",
+ "name": "VIP Limo Transfer",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 1,
+ "prizeCategory": "Transport",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 1,
+ "m-comp": 1
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ },
+ {
+ "awardUsedLimit": 0,
+ "awardsCount": 0,
+ "awardsEligible": 1,
+ "awardsEntitled": 1,
+ "awardsRedeemLimit": 1,
+ "earnedPoints": 7,
+ "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": 38118,
+ "code": "PROMO-RETAIL-COUPON-2026",
+ "name": "Retail Coupon Reward",
+ "ruleId": 3811801,
+ "ruleName": "Retail reward single item",
+ "ruleNameDesc": "Retail reward single item",
+ "toEarn1Unit": 7,
+ "isRedeemAll": false,
+ "outcomeList": [
+ {
+ "key": 38118011,
+ "value": "mock-retail-coupon-40",
+ "isInventoryCheck": null,
+ "qtyOnHand": null,
+ "prize": {
+ "prizeCode": "mock-retail-coupon-40",
+ "name": "Retail Coupon 40",
+ "source": "INTERNAL",
+ "isVoidable": true,
+ "isEnabled": true,
+ "prizeValue": 40,
+ "prizeCategory": "Retail",
+ "usageBehavior": "TARGET_ONLY",
+ "deliveryMode": "USER_CHOICE",
+ "allowNegativeBalance": false,
+ "behaviorProfile": {
+ "deliveryMode": "USER_CHOICE"
+ },
+ "bucketPrices": {
+ "d-comp": 40,
+ "m-comp": 40
+ },
+ "uniqueCodeRequirement": "NONE"
+ }
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/fixtures/promotions.sample.json b/fixtures/promotions.sample.json
new file mode 100644
index 0000000..4e9bd8b
--- /dev/null
+++ b/fixtures/promotions.sample.json
@@ -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": []
+ }
+}
diff --git a/fixtures/token.get.json b/fixtures/token.get.json
new file mode 100644
index 0000000..bffe545
--- /dev/null
+++ b/fixtures/token.get.json
@@ -0,0 +1,7 @@
+{
+ "errorCode": "",
+ "errorMsg": "",
+ "accessToken": "",
+ "refreshToken": "",
+ "period": 3600
+}
diff --git a/runtime/redeemed-promotions.recorded.json b/runtime/redeemed-promotions.recorded.json
new file mode 100644
index 0000000..215d9a6
--- /dev/null
+++ b/runtime/redeemed-promotions.recorded.json
@@ -0,0 +1,2293 @@
+{
+ "updatedAt": "2026-10-12T09:36:02.726Z",
+ "accounts": {
+ "80038008": [
+ {
+ "gamingDate": "2026-10-12",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-10-12T09:35:33.525Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1791797215,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_150",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "771ceb04-be19-4868-9d98-eefb1855795f"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-10-12T09:36:02.725Z",
+ "voidedGamingDate": "2026-10-12",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-10-12",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-10-12T09:29:57.020Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1791797204,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_150",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "a43b7e4f-3e83-4bec-b9e8-17cc26a4774c"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-30",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-30T06:40:12.337Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790749134,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "3327943f-f083-41f3-8d18-d6e342b091d8"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-30T06:50:40.102Z",
+ "voidedGamingDate": "2026-09-30",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-30",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-30T06:28:20.136Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790749132,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "b7893577-ea6b-4b66-a74c-3f08afd8e541"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-30",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-30T06:23:58.727Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790749130,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "a6aade9f-f90d-4eb1-b8b9-b604e4c929c8"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-30",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-30T06:22:52.638Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790749128,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "2e61a21b-7356-4ea0-b24f-d14065d978fb"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-30",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-30T06:15:45.234Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790748928,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "59da5bd1-4005-4836-a334-5e9c981e5694"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-22",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-22T03:21:39.099Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790046612,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "6d33d329-3577-44b7-8d12-6de7639ff130"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-22",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-22T03:17:34.899Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790046604,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "6e60c7b0-9e57-4a7c-9588-7d5f1948a4f8"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-22",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-22T03:16:53.820Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790046600,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "43d8511a-0c69-41f8-860d-5b7b4d6bab99"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-22T03:20:12.292Z",
+ "voidedGamingDate": "2026-09-22",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-22",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-22T01:38:19.444Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790040054,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "67fdc0d3-83cf-40da-ab79-819353b73347"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-22T01:39:17.522Z",
+ "voidedGamingDate": "2026-09-22",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-22",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-22T01:24:00.070Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 3,
+ "redemptionId": 1790040043,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "c01850ca-acdc-4fe4-a915-c3d4b69aec34"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-22T01:27:24.397Z",
+ "voidedGamingDate": "2026-09-22",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-22",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-22T01:23:24.060Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1790040039,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "76f31b24-135d-4040-ab88-7d8e2a7ca885"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-22T01:24:14.096Z",
+ "voidedGamingDate": "2026-09-22",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T11:48:07.927Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 10,
+ "redemptionId": 1789987683,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "6a3eb99a-7246-4d1a-b597-004a3cbe2b45"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T09:02:00.141Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789980247,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "13d2821c-bfd4-43de-86b8-e71a1506dce4"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T07:13:48.760Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 10,
+ "redemptionId": 1789974819,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "0d68f6d2-27cf-43e5-8570-deb739122005"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T06:28:43.017Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789972106,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "e4057741-2407-4756-9411-a56943730aad"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-21T06:29:11.482Z",
+ "voidedGamingDate": "2026-09-21",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T06:28:14.822Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789971148,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "021094e9-a31b-4f9c-8c01-beeefab49168"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-21T06:33:06.598Z",
+ "voidedGamingDate": "2026-09-21",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T03:42:12.414Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 5,
+ "redemptionId": 1789962082,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "e50e2aef-36aa-40a1-aa3a-89ce8f62a97c"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T02:40:34.264Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789957331,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "467a30b3-f95c-4c66-a172-65daab86c52f"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-21T02:50:47.093Z",
+ "voidedGamingDate": "2026-09-21",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T02:22:23.899Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789957321,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "65a98d69-e08e-485d-9e75-db4f8a530f80"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-21T02:26:06.256Z",
+ "voidedGamingDate": "2026-09-21",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T02:18:58.348Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789956767,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "5be04297-71c6-467c-9454-758e58c9ccd1"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-21T03:08:45.646Z",
+ "voidedGamingDate": "2026-09-21",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T02:13:31.784Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789956764,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "ac411033-f8bd-4fb7-800f-68b79b85fc77"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-21T03:08:35.554Z",
+ "voidedGamingDate": "2026-09-21",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T02:07:54.130Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789956455,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_150",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "ec951127-db57-4a3f-b6cf-bd0d68ed6fbb"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T01:53:48.213Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789953647,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "it_fb_100",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "FnB",
+ "transId": "d52dbbcd-1e97-4e1d-9767-281102cff67e"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-21T02:06:53.072Z",
+ "voidedGamingDate": "2026-09-21",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-21",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-21T01:52:25.881Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice C - choose 1 from 10 prizes",
+ "prizeRuleId": 3803801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-C",
+ "promotionId": 38038,
+ "redeemedQty": 1,
+ "redemptionId": 1789953642,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-peg-jul2021-w1-3500eb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "eBonus",
+ "transId": "c368f96c-8ce7-4d0d-a42b-b7ee41f96d11"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-21T01:53:09.009Z",
+ "voidedGamingDate": "2026-09-21",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-18",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-18T09:56:20.094Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789724427,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "dfa5dcaf-c5d8-46fe-9cb7-57a035d79061"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-18",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-18T09:40:35.551Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789724419,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "5ba6cb90-87cf-42ae-884a-1189a5c875c8"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-21T01:48:46.075Z",
+ "voidedGamingDate": "2026-09-21",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-18",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-18T07:58:23.412Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789718287,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "55c5eef7-621b-439d-a005-fafc7e68c438"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-17",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-17T02:21:17.195Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789610141,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "3c7cd974-ec60-4e56-9800-a669e40ab189"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-17",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-17T02:15:34.198Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789610137,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "db17c6e7-e76c-45f0-b877-f83b63e2c0d0"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-17T02:20:26.831Z",
+ "voidedGamingDate": "2026-09-17",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-16",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-16T09:00:57.162Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789549242,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "3b66d129-1cdd-447b-8cdc-59244a572af0"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-16",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-16T09:00:22.130Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789548973,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "15089993-542b-49b7-8178-344003d737f0"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-16",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-16T08:59:50.359Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789548971,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "13c70ff9-f5a9-42e1-bea3-dbfa1f091ce9"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-16",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-16T08:56:24.056Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789548968,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "dc5f0189-a63d-42c5-bf09-4c56ad354cde"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-16",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-16T08:54:51.422Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789548813,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "8e6dbe7d-1a81-4897-887d-cdf094f3a32c"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-15",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-15T01:43:29.028Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789436410,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "94a2c91f-35a1-4277-8e63-0587da0e4bea"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-15T02:25:32.450Z",
+ "voidedGamingDate": "2026-09-15",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-14",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-14T10:07:50.549Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1789380459,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "3e24d7da-2899-4a84-8edc-2e0f560b511c"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-14T10:12:46.607Z",
+ "voidedGamingDate": "2026-09-14",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-09",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-09T05:06:32.945Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788930355,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "a5f2a1ba-e02c-4cc0-9937-2a43ecda1e41"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-08",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-08T03:46:06.282Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788838754,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "0de2dab6-6a4a-41d5-980d-5288eaadd7bb"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-08",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-08T01:51:50.694Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788832285,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "56fdd911-0eac-4eb2-b9dd-4ec7b5b68bf1"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-07",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-07T09:31:33.724Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788773242,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "2166856f-9f41-4ecc-93d4-54574df908cf"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T10:16:40.128Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788342993,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "5055421b-a322-4e71-8a7e-052d16f7e445"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-07T07:35:03.423Z",
+ "voidedGamingDate": "2026-09-07",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T10:05:06.600Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "view-localhost",
+ "ipAddress": "127.0.0.1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788342989,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "55a3100a-20ab-41de-8a48-5fdf220eed09"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-03T02:12:55.853Z",
+ "voidedGamingDate": "2026-09-03",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T09:54:14.128Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788342740,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "5e554dfe-f2f6-4b62-bbc8-f64c60a9833b"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:41:50.178Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788331023,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "911cd3ab-c414-4a99-af0a-5144a70bb685"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "admin",
+ "voidedDtm": "2026-09-02T10:16:45.706Z",
+ "voidedGamingDate": "2026-09-02",
+ "voidedRemark": "Mock voided redeemed promotion",
+ "displayOnlyPromo": false,
+ "isVoided": true
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:37:45.772Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "view-localhost",
+ "ipAddress": "127.0.0.1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788331021,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "e2806302-ddbf-46d7-ada6-3b334f371f74"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:34:14.356Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327976,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "9614a371-b729-48fc-84ca-5dc3478d137c"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:32:59.007Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327975,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "f01998a6-4d2f-4e35-81fb-59611ae6aa03"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:32:14.551Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327974,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "14189c72-3a61-417a-85ea-ea4761964950"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:31:27.351Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327973,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "1b7d42c6-a6e5-4a47-a010-848825b3fbcd"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:30:39.938Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327972,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "14126a15-fa5d-4508-bccb-0d9fabf3a79b"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:30:21.876Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327971,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "4f096bb6-6298-4d96-b9a7-97fb26dc2ad8"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:28:33.045Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327970,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "23d2460b-7b22-4e6f-855f-8396447317ab"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:27:52.137Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327969,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "d8185c49-8265-4130-ab84-e6ed8d3fbbdd"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:26:54.508Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327968,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "430b1e8b-145d-4a37-b3b3-cbf480e97bde"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T06:26:13.927Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788327967,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "5015fe41-0548-4e99-b76e-a2a6e3b1ad2d"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T05:13:11.071Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788325148,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "6271109e-4b1c-41c8-ab24-7099bf25aa31"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T05:12:43.450Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788325147,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "7f8e1db8-dc58-46a4-9632-f073751ac190"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T05:11:38.920Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788325145,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "d29dad4e-4cf4-4dfb-9676-d925e9aba219"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T05:04:29.147Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788325141,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "034ba127-e9bd-4b50-ba19-fd3ec23866b1"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T05:03:54.957Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788325138,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "27ef8da5-f9f6-4064-9a97-5805f8a3987d"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T05:01:35.305Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788325135,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "ff0a40fd-1908-4ceb-a2b6-bd736f89703d"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T05:01:08.984Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788325132,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "ceac0fe0-0a8f-4cdf-a331-1fd73f576e96"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T04:59:52.148Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788325129,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "cbedcd72-663a-4098-a96d-78e052b40ccb"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ },
+ {
+ "gamingDate": "2026-09-02",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-02T04:32:25.996Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "Mega Choice A - choose 1 from 10 prizes",
+ "prizeRuleId": 3801801,
+ "promotionCode": "PROMO-MEGA-CHOICE-2026-A",
+ "promotionId": 38018,
+ "redeemedQty": 1,
+ "redemptionId": 1788323514,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "nn-test-package-nnpc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "ef148ffc-c7ad-41fb-a258-b219c08a61fa"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ }
+ ],
+ "800038008": [
+ {
+ "gamingDate": "2026-09-16",
+ "normalPromo": true,
+ "postedBy": "admin",
+ "postedDtm": "2026-09-16T04:10:48.811Z",
+ "casinoCode": "GLP",
+ "locationCode": "GLPBOOTH",
+ "computerName": "0:0:0:0:0:0:0:1",
+ "ipAddress": "0:0:0:0:0:0:0:1",
+ "prizeRuleCode": "CNYGS-388pts-Moutai 200ML + $1,000 NP/EB",
+ "prizeRuleId": 71768029,
+ "promotionCode": "LP-EE-CNYGS26",
+ "promotionId": 71768004,
+ "redeemedQty": 1,
+ "redemptionId": 1789528452,
+ "remark": "Mock redeemed promotion detail",
+ "outcome": {
+ "redeemedList": [
+ {
+ "outcomeRemark": "lp-ee-cnygs26-388pts-1000-pc",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "NNPC",
+ "transId": "a1b9ce70-9a4a-4960-b9a2-e4cf110f9543"
+ },
+ {
+ "outcomeRemark": "lp-ee-cnygs26-388pts-1000-eb",
+ "outcomeType": "PRIZE",
+ "prizeCategory": "eBonus",
+ "transId": "a1b9ce70-9a4a-4960-b9a2-e4cf110f9543"
+ }
+ ]
+ },
+ "outcomeAward": null,
+ "voidedBy": "",
+ "voidedDtm": "",
+ "voidedGamingDate": "",
+ "voidedRemark": "",
+ "displayOnlyPromo": false,
+ "isVoided": false
+ }
+ ]
+ }
+}
diff --git a/src/context/createMockContext.js b/src/context/createMockContext.js
new file mode 100644
index 0000000..2707962
--- /dev/null
+++ b/src/context/createMockContext.js
@@ -0,0 +1,1310 @@
+const crypto = require('node:crypto')
+const fs = require('node:fs')
+const path = require('node:path')
+
+const MAX_PROMOTION_REDEMPTION_ID = 2147483647
+
+function resolveDefaultPromotionRedemptionIdStart() {
+ return Math.floor(Date.now() / 1000)
+}
+
+function createMockContext() {
+ const host = process.env.MOCK_GATEWAY_HOST || process.env.HOST || '127.0.0.1'
+ const port = Number(process.env.MOCK_GATEWAY_PORT || process.env.PORT || 19090)
+ const tokenPeriodSeconds = Number(process.env.MOCK_TOKEN_PERIOD_SECONDS || 3600)
+ const defaultValidateResult = process.env.MOCK_VALIDATE_RESULT || 'SUCCESS'
+ const configuredFailPins = (process.env.MOCK_VALIDATE_FAIL_PINS || '0000')
+ .split(',')
+ .map((value) => value.trim())
+ .filter(Boolean)
+ const failPins = new Set(configuredFailPins)
+ const promotionMaxItemsRaw = Number(process.env.MOCK_PROMOTION_MAX_ITEMS || 30)
+ const promotionMaxItems =
+ Number.isFinite(promotionMaxItemsRaw) && promotionMaxItemsRaw > 0
+ ? Math.floor(promotionMaxItemsRaw)
+ : 30
+ const promotionCaptureFile =
+ process.env.MOCK_PROMOTION_CAPTURE_FILE ||
+ path.resolve(__dirname, '../../fixtures/promotions.capture.80038008.json')
+ const tokenGetFixtureFile =
+ process.env.MOCK_TOKEN_GET_FIXTURE_FILE ||
+ path.resolve(__dirname, '../../fixtures/token.get.json')
+ const memberProfileFixtureFile =
+ process.env.MOCK_MEMBER_PROFILE_FIXTURE_FILE ||
+ path.resolve(__dirname, '../../fixtures/member.profile.json')
+ const memberProfileForceStatusCodeRaw = Number(process.env.MOCK_MEMBER_PROFILE_FORCE_STATUS_CODE || 0)
+ const memberProfileForceStatusCode =
+ Number.isInteger(memberProfileForceStatusCodeRaw) && memberProfileForceStatusCodeRaw > 0
+ ? memberProfileForceStatusCodeRaw
+ : 0
+ const memberProfileForceErrorCode = process.env.MOCK_MEMBER_PROFILE_FORCE_ERROR_CODE || ''
+ const memberProfileForceErrorMsg =
+ process.env.MOCK_MEMBER_PROFILE_FORCE_ERROR_MSG || 'Mock member profile error'
+ const memberProfileDelayMsRaw = Number(process.env.MOCK_MEMBER_PROFILE_DELAY_MS || 0)
+ const memberProfileDelayMs =
+ Number.isFinite(memberProfileDelayMsRaw) && memberProfileDelayMsRaw > 0
+ ? Math.floor(memberProfileDelayMsRaw)
+ : 0
+ const promotionsRedeemedFixtureFile =
+ process.env.MOCK_PROMOTIONS_REDEEMED_FIXTURE_FILE ||
+ path.resolve(__dirname, '../../e.p/promotions-redeemed.json')
+ const recordedRedeemedPromotionsPersistEnabled =
+ (process.env.MOCK_REDEEMED_PROMOTION_RECORDING_ENABLED || 'true').toLowerCase() !== 'false'
+ const recordedRedeemedPromotionsFile = recordedRedeemedPromotionsPersistEnabled
+ ? path.resolve(
+ __dirname,
+ '../../',
+ process.env.MOCK_REDEEMED_PROMOTION_RECORDING_FILE ||
+ './runtime/redeemed-promotions.recorded.json',
+ )
+ : ''
+ const promotionRedeemFulfillmentOverridesPersistEnabled =
+ (process.env.MOCK_PROMOTION_REDEEM_FULFILLMENT_OVERRIDES_ENABLED || 'true').toLowerCase() !==
+ 'false'
+ const promotionRedeemFulfillmentOverridesFile = promotionRedeemFulfillmentOverridesPersistEnabled
+ ? path.resolve(
+ __dirname,
+ '../../',
+ process.env.MOCK_PROMOTION_REDEEM_FULFILLMENT_OVERRIDES_FILE ||
+ './runtime/promotion-redeem-fulfillment-overrides.json',
+ )
+ : ''
+ // Task 10766: Add dedicated knobs so the promotion redeem failure detail can be reproduced with a fixed member account.
+ const task10766Enabled = (process.env.MOCK_TASK_10766_ENABLED || 'false').toLowerCase() === 'true'
+ const task10766AccountNum = process.env.MOCK_TASK_10766_ACCOUNT_NUM || '80010766'
+ const task10766PromotionCaptureFile =
+ process.env.MOCK_TASK_10766_PROMOTION_CAPTURE_FILE ||
+ path.resolve(__dirname, '../../fixtures/promotions.capture.10766.json')
+ const task10766MemberEBonus = process.env.MOCK_TASK_10766_MEMBER_EBONUS || '8888'
+ const task10766RedeemErrorCode =
+ process.env.MOCK_TASK_10766_REDEEM_ERROR_CODE || 'EWALLET_HTTP_ERROR'
+ const task10766RedeemErrorMsg =
+ process.env.MOCK_TASK_10766_REDEEM_ERROR_MSG ||
+ 'ewallet connection failed: 404 NOT_FOUND, {"success":false,"message":"Prize definition not found"}'
+ const task10766RedeemResult =
+ (process.env.MOCK_TASK_10766_REDEEM_RESULT || 'false').toLowerCase() === 'true'
+ // Task 10852: Add dedicated knobs so a fixed member can always hit the verified multi-outcome promotion capture.
+ const task10852Enabled = (process.env.MOCK_TASK_10852_ENABLED || 'false').toLowerCase() === 'true'
+ const task10852AccountNum = process.env.MOCK_TASK_10852_ACCOUNT_NUM || '80010852'
+ const task10852PromotionCaptureFile =
+ process.env.MOCK_TASK_10852_PROMOTION_CAPTURE_FILE ||
+ path.resolve(__dirname, '../../fixtures/promotions.capture.10852.json')
+ const task10852MemberEBonus = process.env.MOCK_TASK_10852_MEMBER_EBONUS || '188'
+ // Task 10852: Keep the mock narrowly tied to the reported multi-outcome rule from the bug payload.
+ const task10852PromotionRuleId = 71768026
+ // Task 15171: Active card sync scenario for Bug #14958 (Dr TEST GL1 / 600000154).
+ const task15171Enabled = (process.env.MOCK_TASK_15171_ENABLED || 'true').toLowerCase() === 'true'
+
+ const teamHierarchyStatus =
+ (process.env.MOCK_TEAM_HIERARCHY_STATUS || 'true').toLowerCase() !== 'false'
+ const teamHierarchyErrorCode = process.env.MOCK_TEAM_HIERARCHY_ERROR_CODE || ''
+ const teamHierarchyMessage = process.env.MOCK_TEAM_HIERARCHY_MESSAGE || 'Mock team hierarchy'
+ const teamHierarchyRole = process.env.MOCK_TEAM_HIERARCHY_ROLE || 'SUPPORT'
+ const teamHierarchyDepartment = process.env.MOCK_TEAM_HIERARCHY_DEPARTMENT || 'IT'
+ const teamHierarchyTags = (process.env.MOCK_TEAM_HIERARCHY_TAGS || 'MOCK')
+ .split(',')
+ .map((value) => value.trim())
+ .filter(Boolean)
+
+ const prizeRedeemErrorCode = process.env.MOCK_PRIZE_REDEEM_ERROR_CODE || ''
+ const prizeRedeemErrorMsg = process.env.MOCK_PRIZE_REDEEM_ERROR_MSG || ''
+ const prizeRedeemValue = process.env.MOCK_PRIZE_REDEEM_VALUE || ''
+ const gisStockUniqueCodes = new Set(
+ (process.env.MOCK_GIS_STOCK_UNIQUE_CODES || '88888888,12341234')
+ .split(',')
+ .map((value) => value.trim())
+ .filter(Boolean),
+ )
+
+ const pointVoidErrorCode = process.env.MOCK_POINT_VOID_ERROR_CODE || ''
+ const pointVoidErrorMsg = process.env.MOCK_POINT_VOID_ERROR_MSG || ''
+ const pointVoidResult = (process.env.MOCK_POINT_VOID_RESULT || 'true').toLowerCase() === 'true'
+
+ const redeemedPromotionDetailErrorCode =
+ process.env.MOCK_REDEEMED_PROMOTION_DETAIL_ERROR_CODE || ''
+ const redeemedPromotionDetailErrorMsg =
+ process.env.MOCK_REDEEMED_PROMOTION_DETAIL_ERROR_MSG || ''
+ const redeemedPromotionDetailIncludeDefaultRecord =
+ (process.env.MOCK_REDEEMED_PROMOTION_DETAIL_INCLUDE_DEFAULT_RECORD || 'true').toLowerCase() !==
+ 'false'
+ const redeemedPromotionDetailVoided =
+ (process.env.MOCK_REDEEMED_PROMOTION_DETAIL_VOIDED || 'false').toLowerCase() === 'true'
+
+ const playerPromotionRedeemErrorCode =
+ process.env.MOCK_PLAYERPROMOTION_REDEEM_ERROR_CODE || ''
+ const playerPromotionRedeemErrorMsg =
+ process.env.MOCK_PLAYERPROMOTION_REDEEM_ERROR_MSG || ''
+ const playerPromotionRedeemResult =
+ (process.env.MOCK_PLAYERPROMOTION_REDEEM_RESULT || 'true').toLowerCase() === 'true'
+ const playerPromotionFulfillmentJourneyErrorCode =
+ process.env.MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_ERROR_CODE || ''
+ const playerPromotionFulfillmentJourneyErrorMsg =
+ process.env.MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_ERROR_MSG || ''
+ const playerPromotionFulfillmentJourneyEmptyResult =
+ (process.env.MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_EMPTY_RESULT || 'false').toLowerCase() ===
+ 'true'
+ const playerPromotionFulfillmentJourneyDetailErrorCode =
+ process.env.MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_DETAIL_ERROR_CODE || ''
+ const playerPromotionFulfillmentJourneyDetailErrorMsg =
+ process.env.MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_DETAIL_ERROR_MSG || ''
+ const playerPromotionFulfillmentJourneyDetailEmptyResult =
+ (process.env.MOCK_PLAYERPROMOTION_FULFILLMENT_JOURNEY_DETAIL_EMPTY_RESULT || 'false')
+ .toLowerCase() === 'true'
+ const promotionRedemptionBridgeEnabled =
+ (process.env.MOCK_PROMOTION_REDEMPTION_BRIDGE_ENABLED || 'false').toLowerCase() === 'true'
+ const promotionRedemptionBridgeBaseUrl =
+ process.env.MOCK_PROMOTION_REDEMPTION_BRIDGE_BASE_URL || 'http://127.0.0.1:8080'
+ const promotionRedemptionBridgeLoginUsername =
+ process.env.MOCK_PROMOTION_REDEMPTION_BRIDGE_LOGIN_USERNAME || ''
+ const promotionRedemptionBridgeLoginPassword =
+ process.env.MOCK_PROMOTION_REDEMPTION_BRIDGE_LOGIN_PASSWORD || 'mock-password'
+ const promotionRedemptionBridgeTimeoutMsRaw =
+ Number(process.env.MOCK_PROMOTION_REDEMPTION_BRIDGE_TIMEOUT_MS || 30000)
+ const promotionRedemptionBridgeTimeoutMs =
+ Number.isFinite(promotionRedemptionBridgeTimeoutMsRaw) && promotionRedemptionBridgeTimeoutMsRaw > 0
+ ? Math.floor(promotionRedemptionBridgeTimeoutMsRaw)
+ : 30000
+ const promotionRedemptionInitiateSource =
+ process.env.MOCK_PROMOTION_REDEMPTION_INITIATE_SOURCE || 'PROMOTION_MODULE'
+ const promotionRedemptionSourceBucket =
+ (process.env.MOCK_PROMOTION_REDEMPTION_SOURCE_BUCKET || 'm-comp').trim().toLowerCase()
+ const promotionRedemptionSimulateErrorAfterBridge =
+ (process.env.MOCK_PROMOTION_REDEMPTION_SIMULATE_ERROR_AFTER_BRIDGE || 'false').toLowerCase() ===
+ 'true'
+ const defaultPromotionRedemptionIdStart = resolveDefaultPromotionRedemptionIdStart()
+ const promotionRedemptionIdStartRaw =
+ Number(process.env.MOCK_PROMOTION_REDEMPTION_ID_START || defaultPromotionRedemptionIdStart)
+ const promotionRedemptionIdStart =
+ Number.isFinite(promotionRedemptionIdStartRaw) &&
+ promotionRedemptionIdStartRaw > 0 &&
+ promotionRedemptionIdStartRaw <= MAX_PROMOTION_REDEMPTION_ID
+ ? Math.floor(promotionRedemptionIdStartRaw)
+ : defaultPromotionRedemptionIdStart
+
+ const redeemedPromotionVoidErrorCode =
+ process.env.MOCK_REDEEMEDPROMOTION_VOID_ERROR_CODE || ''
+ const redeemedPromotionVoidErrorMsg =
+ process.env.MOCK_REDEEMEDPROMOTION_VOID_ERROR_MSG || ''
+ const redeemedPromotionVoidResult =
+ (process.env.MOCK_REDEEMEDPROMOTION_VOID_RESULT || 'true').toLowerCase() === 'true'
+
+ const prizeRedemptionsSyncSuccess =
+ (process.env.MOCK_PRIZE_REDEMPTIONS_SYNC_SUCCESS || 'true').toLowerCase() !== 'false'
+ const prizeRedemptionsSyncMessage =
+ process.env.MOCK_PRIZE_REDEMPTIONS_SYNC_MESSAGE || 'Mock prize redemptions sync success'
+
+ const ldapEnabled = (process.env.MOCK_LDAP_ENABLED || 'true').toLowerCase() !== 'false'
+ const ldapHost = process.env.MOCK_LDAP_HOST || '0.0.0.0'
+ const ldapPort = Number(process.env.MOCK_LDAP_PORT || 1389)
+ const ldapBaseDn = process.env.MOCK_LDAP_BASE_DN || 'dc=macausjm-glp,dc=com'
+ const ldapManagerDn =
+ process.env.MOCK_LDAP_MANAGER_DN || `cn=s-cicd-app,OU=ServiceAccount,${ldapBaseDn}`
+ const ldapGroupBaseDn =
+ process.env.MOCK_LDAP_GROUP_BASE_DN || `OU=SecurityGroups,OU=GLPUsers,${ldapBaseDn}`
+ const ldapManagerPassword = process.env.MOCK_LDAP_MANAGER_PASSWORD || ''
+ const ldapAllowAnyUserMode =
+ (process.env.MOCK_LDAP_ALLOW_ANY_USER_MODE || 'true').toLowerCase() === 'true'
+ const ldapAllowAnyUserOu = process.env.MOCK_LDAP_ALLOW_ANY_USER_OU || 'OU=GLPUsers'
+ const ldapAllowAnyUserGroupCn =
+ process.env.MOCK_LDAP_ALLOW_ANY_USER_GROUP_CN || 'SG-APP-EWALLET-SUPER-ADMIN'
+ const ldapAllowAnyUserMailDomain =
+ process.env.MOCK_LDAP_ALLOW_ANY_USER_MAIL_DOMAIN || 'macausjm-glp.com'
+ const ldapForceNoSuchObject =
+ (process.env.MOCK_LDAP_FORCE_NO_SUCH_OBJECT || '').toLowerCase() === 'true'
+ const ldapForceNoSuchObjectMatch =
+ process.env.MOCK_LDAP_FORCE_NO_SUCH_OBJECT_MATCH || 'OU=macauin-glp,DC=com'
+
+ const mockProfile = {
+ firstName: process.env.MOCK_MEMBER_FIRST_NAME || 'Gary',
+ lastName: process.env.MOCK_MEMBER_LAST_NAME || 'Chan',
+ nickName: process.env.MOCK_MEMBER_NICK_NAME || 'xxx',
+ tier: process.env.MOCK_MEMBER_TIER || 'Novel',
+ eBonus: process.env.MOCK_MEMBER_EBONUS || '0',
+ phoneNum: process.env.MOCK_MEMBER_PHONE || '+853-68282140',
+ birthday: process.env.MOCK_MEMBER_BIRTHDAY || '1990-01-11',
+ gender: process.env.MOCK_MEMBER_GENDER || 'Male',
+ image: process.env.MOCK_MEMBER_IMAGE || '',
+ onDayTierPoint: Number.isFinite(Number(process.env.MOCK_MEMBER_ODTP))
+ ? Number(process.env.MOCK_MEMBER_ODTP)
+ : 150,
+ lastLocn: process.env.MOCK_MEMBER_LAST_LOCN || 'Table 22 (VIP)',
+ }
+ const fallbackEligiblePromotionList = [
+ {
+ 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,
+ },
+ {
+ key: 71768070,
+ value: 'lp-ee-cnygs26-188pts-lchscallop-me',
+ isInventoryCheck: null,
+ qtyOnHand: null,
+ },
+ ],
+ },
+ ]
+ const fallbackTokenGetResponseFixture = {
+ errorCode: '',
+ errorMsg: '',
+ accessToken: '',
+ refreshToken: '',
+ period: 3600,
+ }
+ const fallbackMemberProfileResponseFixture = {
+ 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: [],
+ },
+ }
+ const fallbackPromotionsRedeemedFixture = {
+ success: true,
+ message: 'Get redeemed promotions successfully.',
+ data: {
+ customerId: '',
+ outcomeList: [],
+ },
+ }
+
+ const issuedRefreshTokens = new Set()
+ const recordedRedeemedPromotionsByAccount = new Map()
+ const promotionRedeemFulfillmentOverrides = new Map()
+ let nextPromotionRedemptionId = promotionRedemptionIdStart
+
+ function generateToken(prefix) {
+ return `${prefix}_${crypto.randomUUID().replace(/-/g, '')}`
+ }
+
+ function resolveAccountNum(requestBody) {
+ return requestBody?.data?.accountNum || requestBody?.data?.playerId || ''
+ }
+
+ function cloneJsonValue(value) {
+ if (value === undefined || value === null) {
+ return value
+ }
+
+ return JSON.parse(JSON.stringify(value))
+ }
+
+ function normalizeOverrideKeyPart(value) {
+ if (value === undefined || value === null) {
+ return ''
+ }
+
+ return String(value).trim()
+ }
+
+ 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 toNullableString(value) {
+ if (value === undefined || value === null) {
+ return null
+ }
+
+ const text = String(value).trim()
+ return text ? text : null
+ }
+
+ function buildPromotionRedeemFulfillmentOverrideKey({
+ accountNum,
+ promotionId,
+ promotionRuleId,
+ outcomeId,
+ }) {
+ return [
+ normalizeOverrideKeyPart(accountNum),
+ normalizeOverrideKeyPart(promotionId),
+ normalizeOverrideKeyPart(promotionRuleId),
+ normalizeOverrideKeyPart(outcomeId),
+ ].join('::')
+ }
+
+ function normalizePromotionRedeemFulfillmentOverride(payload) {
+ const accountNum = normalizeOverrideKeyPart(payload?.accountNum)
+ const promotionId = normalizeOverrideKeyPart(payload?.promotionId)
+ const promotionRuleId = normalizeOverrideKeyPart(payload?.promotionRuleId)
+ const outcomeId = normalizeOverrideKeyPart(payload?.outcomeId)
+ const issueTargetToWallet = toNullableBoolean(payload?.issueTargetToWallet)
+
+ if (!accountNum || !promotionId || !promotionRuleId || !outcomeId || issueTargetToWallet === null) {
+ throw new Error(
+ 'accountNum, promotionId, promotionRuleId, outcomeId, issueTargetToWallet are required',
+ )
+ }
+
+ return {
+ accountNum,
+ promotionId,
+ promotionRuleId,
+ outcomeId,
+ issueTargetToWallet,
+ }
+ }
+
+ function matchesPromotionRedeemFulfillmentOverride(entry, filter = {}) {
+ if (!entry) {
+ return false
+ }
+
+ const accountNum = normalizeOverrideKeyPart(filter.accountNum)
+ const promotionId = normalizeOverrideKeyPart(filter.promotionId)
+ const promotionRuleId = normalizeOverrideKeyPart(filter.promotionRuleId)
+ const outcomeId = normalizeOverrideKeyPart(filter.outcomeId)
+
+ if (accountNum && entry.accountNum !== accountNum) {
+ return false
+ }
+ if (promotionId && entry.promotionId !== promotionId) {
+ return false
+ }
+ if (promotionRuleId && entry.promotionRuleId !== promotionRuleId) {
+ return false
+ }
+ if (outcomeId && entry.outcomeId !== outcomeId) {
+ return false
+ }
+
+ return true
+ }
+
+ function setPromotionRedeemFulfillmentOverride(payload) {
+ const normalizedEntry = normalizePromotionRedeemFulfillmentOverride(payload)
+ const key = buildPromotionRedeemFulfillmentOverrideKey(normalizedEntry)
+ const nextEntry = {
+ ...normalizedEntry,
+ updatedAt: new Date().toISOString(),
+ }
+
+ promotionRedeemFulfillmentOverrides.set(key, nextEntry)
+ persistPromotionRedeemFulfillmentOverrides()
+ return cloneJsonValue(nextEntry)
+ }
+
+ function getPromotionRedeemFulfillmentOverrides(filter = {}) {
+ return Array.from(promotionRedeemFulfillmentOverrides.values())
+ .filter((entry) => matchesPromotionRedeemFulfillmentOverride(entry, filter))
+ .map((entry) => cloneJsonValue(entry))
+ }
+
+ function clearPromotionRedeemFulfillmentOverrides(filter = {}) {
+ const matchedEntries = getPromotionRedeemFulfillmentOverrides(filter)
+
+ matchedEntries.forEach((entry) => {
+ promotionRedeemFulfillmentOverrides.delete(
+ buildPromotionRedeemFulfillmentOverrideKey(entry),
+ )
+ })
+
+ if (matchedEntries.length > 0) {
+ persistPromotionRedeemFulfillmentOverrides()
+ }
+
+ return matchedEntries.length
+ }
+
+ function resolvePromotionRedeemFulfillmentOverrideByOutcomeId({
+ accountNum,
+ promotionId,
+ promotionRuleId,
+ outcomeIds,
+ }) {
+ const resolvedOverrides = {}
+
+ if (!Array.isArray(outcomeIds) || outcomeIds.length === 0) {
+ return resolvedOverrides
+ }
+
+ outcomeIds.forEach((outcomeId) => {
+ const key = buildPromotionRedeemFulfillmentOverrideKey({
+ accountNum,
+ promotionId,
+ promotionRuleId,
+ outcomeId,
+ })
+ const entry = promotionRedeemFulfillmentOverrides.get(key)
+ if (entry) {
+ resolvedOverrides[String(outcomeId)] = entry.issueTargetToWallet
+ }
+ })
+
+ return resolvedOverrides
+ }
+
+ function tokenSuccessResponse() {
+ const tokenTemplate = cloneJsonValue(tokenGetResponseFixture)
+ const accessToken = generateToken('mock_access')
+ const refreshToken = generateToken('mock_refresh')
+ issuedRefreshTokens.add(refreshToken)
+ const templatePeriod = Number(tokenTemplate?.period)
+ const envPeriod =
+ Number.isFinite(tokenPeriodSeconds) && tokenPeriodSeconds > 0 ? tokenPeriodSeconds : null
+ const resolvedPeriod =
+ envPeriod ??
+ (Number.isFinite(templatePeriod) && templatePeriod > 0
+ ? templatePeriod
+ : fallbackTokenGetResponseFixture.period)
+
+ return {
+ ...tokenTemplate,
+ errorCode: '',
+ errorMsg: '',
+ accessToken,
+ refreshToken,
+ period: resolvedPeriod,
+ }
+ }
+
+ function extractJsonPayload(rawText) {
+ const firstJsonCharIndex = rawText.indexOf('{')
+ if (firstJsonCharIndex < 0) {
+ return null
+ }
+
+ const jsonText = rawText.slice(firstJsonCharIndex).trim()
+ if (!jsonText) {
+ return null
+ }
+
+ return JSON.parse(jsonText)
+ }
+
+ function normalizeOutcomeList(outcomeList) {
+ if (!Array.isArray(outcomeList)) {
+ return []
+ }
+
+ return outcomeList.map((item) => ({
+ key: item?.key ?? null,
+ value: item?.value || '',
+ isInventoryCheck: item?.isInventoryCheck ?? null,
+ qtyOnHand: item?.qtyOnHand ?? null,
+ prize: cloneJsonValue(item?.prize ?? null),
+ }))
+ }
+
+ function normalizePromotionList(promotionList) {
+ if (!Array.isArray(promotionList)) {
+ return []
+ }
+
+ return promotionList.map((item) => ({
+ awardUsedLimit: item?.awardUsedLimit ?? 0,
+ awardsCount: item?.awardsCount ?? 0,
+ awardsEligible: item?.awardsEligible ?? 0,
+ awardsEntitled: item?.awardsEntitled ?? 0,
+ awardsRedeemLimit: item?.awardsRedeemLimit ?? 0,
+ earnedPoints: item?.earnedPoints ?? 0,
+ isDirectOffer: item?.isDirectOffer ?? false,
+ isEligible: item?.isEligible ?? false,
+ isEntitled: item?.isEntitled ?? false,
+ isOptIn: item?.isOptIn ?? false,
+ isOptedInByPlayer: item?.isOptedInByPlayer ?? false,
+ isRedeemCompleted: item?.isRedeemCompleted ?? false,
+ maxOutcome: item?.maxOutcome ?? 0,
+ minOutcome: item?.minOutcome ?? 0,
+ startDate: item?.startDate || '',
+ endDate: item?.endDate || '',
+ id: item?.id ?? null,
+ code: item?.code || '',
+ name: item?.name || '',
+ ruleId: item?.ruleId ?? null,
+ ruleName: item?.ruleName || '',
+ ruleNameDesc: item?.ruleNameDesc || '',
+ toEarn1Unit: item?.toEarn1Unit ?? 0,
+ isRedeemAll: item?.isRedeemAll ?? false,
+ outcomeList: normalizeOutcomeList(item?.outcomeList),
+ }))
+ }
+
+ function buildMockRedeemedPromotionOutcome(requestBody) {
+ const now = new Date().toISOString()
+ const loginId = requestBody?.data?.loginId || 'mock.user'
+ const gamingDate = now.slice(0, 10)
+ const redemptionId = nextMockPromotionRedemptionId()
+
+ return {
+ gamingDate,
+ normalPromo: true,
+ postedBy: loginId,
+ postedDtm: now,
+ casinoCode: requestBody?.data?.casinoCode || 'GLP',
+ locationCode: requestBody?.data?.locationCode || 'MOCK',
+ computerName: requestBody?.schema?.computerName || 'mock-gateway',
+ ipAddress: requestBody?.schema?.ipAddress || host,
+ prizeRuleCode: 'MOCK-PRIZE-RULE',
+ prizeRuleId: 100001,
+ promotionCode: 'MOCK-PROMOTION',
+ promotionId: 100002,
+ redeemedQty: 1,
+ redemptionId,
+ remark: 'Mock redeemed promotion detail',
+ outcome: {
+ redeemedList: [
+ {
+ outcomeRemark: 'MOCK-PRIZE-CODE',
+ outcomeType: 'PRIZE',
+ prizeCategory: 'PROMOTION',
+ transId: generateToken('mock_trans'),
+ },
+ ],
+ },
+ outcomeAward: null,
+ voidedBy: redeemedPromotionDetailVoided ? loginId : '',
+ voidedDtm: redeemedPromotionDetailVoided ? now : '',
+ voidedGamingDate: redeemedPromotionDetailVoided ? gamingDate : '',
+ voidedRemark: redeemedPromotionDetailVoided ? 'Mock voided redeemed promotion' : '',
+ displayOnlyPromo: false,
+ isVoided: redeemedPromotionDetailVoided,
+ }
+ }
+
+ function nextMockPromotionRedemptionId() {
+ const redemptionId = nextPromotionRedemptionId
+ nextPromotionRedemptionId += 1
+ return redemptionId
+ }
+
+ function loadPromotionCapture(filePath) {
+ if (!filePath || !fs.existsSync(filePath)) {
+ return null
+ }
+
+ try {
+ const rawContent = fs.readFileSync(filePath, 'utf8')
+ const parsed = extractJsonPayload(rawContent)
+ if (!parsed) {
+ return null
+ }
+
+ const data = parsed?.data && typeof parsed.data === 'object' ? parsed.data : parsed
+ const eligiblePromotionList = normalizePromotionList(data?.eligiblePromotionList)
+ const entitledPromotionList = normalizePromotionList(data?.entitledPromotionList)
+
+ if (eligiblePromotionList.length === 0 && entitledPromotionList.length === 0) {
+ return null
+ }
+
+ console.log(`Loaded promotion fixture from ${filePath}`)
+ return {
+ eligiblePromotionList,
+ entitledPromotionList,
+ }
+ } catch (error) {
+ console.warn(`Failed to load promotion fixture from ${filePath}: ${error.message}`)
+ return null
+ }
+ }
+
+ function loadJsonFixture(filePath, fallbackValue, fixtureName) {
+ if (!filePath || !fs.existsSync(filePath)) {
+ return cloneJsonValue(fallbackValue)
+ }
+
+ try {
+ const rawContent = fs.readFileSync(filePath, 'utf8')
+ let parsed
+ try {
+ parsed = JSON.parse(rawContent)
+ } catch (error) {
+ parsed = extractJsonPayload(rawContent)
+ }
+
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
+ throw new Error('fixture must be a JSON object')
+ }
+
+ console.log(`Loaded ${fixtureName} fixture from ${filePath}`)
+ return parsed
+ } catch (error) {
+ console.warn(`Failed to load ${fixtureName} fixture from ${filePath}: ${error.message}`)
+ return cloneJsonValue(fallbackValue)
+ }
+ }
+
+ function trimPromotionList(promotionList) {
+ return promotionList.slice(0, promotionMaxItems)
+ }
+
+ function getAccountSpecificPromotionCapture(accountNum, fixturePrefix = 'promotions.capture') {
+ if (!accountNum) {
+ return null
+ }
+
+ const candidateAccounts = [String(accountNum)]
+ if (fixturePrefix === 'promotions.backdate') {
+ if (String(accountNum) === '80038008') {
+ candidateAccounts.push('800038008')
+ } else if (String(accountNum) === '800038008') {
+ candidateAccounts.push('80038008')
+ }
+ }
+
+ for (const candidateAccount of candidateAccounts) {
+ const accountCaptureFile = path.resolve(
+ __dirname,
+ `../../fixtures/${fixturePrefix}.${candidateAccount}.json`,
+ )
+ const capture = loadPromotionCapture(accountCaptureFile)
+ if (capture) {
+ return capture
+ }
+ }
+
+ return null
+ }
+
+ function getAccountSpecificRedeemedPromotionsFixture(accountNum) {
+ if (!accountNum) {
+ return null
+ }
+
+ const fixtureFile = path.resolve(
+ __dirname,
+ `../../fixtures/award.promotions.${accountNum}.json`,
+ )
+ if (!fs.existsSync(fixtureFile)) {
+ return null
+ }
+
+ return loadJsonFixture(fixtureFile, null, `award.promotions.${accountNum}`)
+ }
+
+ function createTask10852PromotionCapture(basePromotionCapture) {
+ if (!basePromotionCapture) {
+ return null
+ }
+
+ return {
+ // Task 10852: Mark only the reported multi-outcome rule as entitled so the frontend Redeem button becomes clickable for that case.
+ eligiblePromotionList: basePromotionCapture.eligiblePromotionList.map((item) =>
+ item?.ruleId === task10852PromotionRuleId
+ ? {
+ ...item,
+ isEligible: true,
+ isEntitled: true,
+ awardsEligible: Math.max(item.awardsEligible ?? 0, 1),
+ awardsEntitled: Math.max(item.awardsEntitled ?? 0, 1),
+ earnedPoints: Math.max(item.earnedPoints ?? 0, item.toEarn1Unit ?? 0),
+ isRedeemCompleted: false,
+ }
+ : item,
+ ),
+ entitledPromotionList: basePromotionCapture.entitledPromotionList,
+ }
+ }
+
+ const normalizedFallbackEligiblePromotionList = normalizePromotionList(fallbackEligiblePromotionList)
+ const tokenGetResponseFixture = loadJsonFixture(
+ tokenGetFixtureFile,
+ fallbackTokenGetResponseFixture,
+ 'token.get',
+ )
+ const memberProfileResponseFixture = loadJsonFixture(
+ memberProfileFixtureFile,
+ fallbackMemberProfileResponseFixture,
+ 'member.profile',
+ )
+ const promotionsRedeemedFixture = loadJsonFixture(
+ promotionsRedeemedFixtureFile,
+ fallbackPromotionsRedeemedFixture,
+ 'promotions.redeemed',
+ )
+
+ function loadRecordedRedeemedPromotions() {
+ if (!recordedRedeemedPromotionsFile || !fs.existsSync(recordedRedeemedPromotionsFile)) {
+ return
+ }
+
+ const persisted = loadJsonFixture(
+ recordedRedeemedPromotionsFile,
+ null,
+ 'promotions.redeemed.recording',
+ )
+ if (!persisted || typeof persisted !== 'object' || Array.isArray(persisted)) {
+ return
+ }
+
+ const accounts =
+ persisted.accounts && typeof persisted.accounts === 'object' && !Array.isArray(persisted.accounts)
+ ? persisted.accounts
+ : persisted
+ let maxRedemptionId = promotionRedemptionIdStart - 1
+
+ for (const [accountNum, outcomeList] of Object.entries(accounts)) {
+ if (!Array.isArray(outcomeList) || outcomeList.length === 0) {
+ continue
+ }
+
+ const normalizedOutcomeList = outcomeList
+ .filter((item) => item && typeof item === 'object' && !Array.isArray(item))
+ .map((item) => cloneJsonValue(item))
+
+ if (normalizedOutcomeList.length === 0) {
+ continue
+ }
+
+ recordedRedeemedPromotionsByAccount.set(String(accountNum), normalizedOutcomeList)
+
+ normalizedOutcomeList.forEach((item) => {
+ const redemptionId = Number(item?.redemptionId)
+ if (Number.isInteger(redemptionId) && redemptionId > maxRedemptionId) {
+ maxRedemptionId = redemptionId
+ }
+ })
+ }
+
+ if (maxRedemptionId >= promotionRedemptionIdStart) {
+ nextPromotionRedemptionId = maxRedemptionId + 1
+ }
+ }
+
+ function persistRecordedRedeemedPromotions() {
+ if (!recordedRedeemedPromotionsFile) {
+ return
+ }
+
+ try {
+ fs.mkdirSync(path.dirname(recordedRedeemedPromotionsFile), { recursive: true })
+ const payload = {
+ updatedAt: new Date().toISOString(),
+ accounts: Object.fromEntries(
+ Array.from(recordedRedeemedPromotionsByAccount.entries(), ([accountNum, outcomeList]) => [
+ accountNum,
+ cloneJsonValue(outcomeList),
+ ]),
+ ),
+ }
+ const tempFile = `${recordedRedeemedPromotionsFile}.tmp`
+ fs.writeFileSync(tempFile, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
+ fs.renameSync(tempFile, recordedRedeemedPromotionsFile)
+ } catch (error) {
+ console.warn(
+ `Failed to persist redeemed promotion recordings to ${recordedRedeemedPromotionsFile}: ${error.message}`,
+ )
+ }
+ }
+
+ function loadPromotionRedeemFulfillmentOverrides() {
+ if (
+ !promotionRedeemFulfillmentOverridesFile ||
+ !fs.existsSync(promotionRedeemFulfillmentOverridesFile)
+ ) {
+ return
+ }
+
+ const persisted = loadJsonFixture(
+ promotionRedeemFulfillmentOverridesFile,
+ null,
+ 'promotion.redeem.fulfillment.overrides',
+ )
+
+ if (!persisted || typeof persisted !== 'object') {
+ return
+ }
+
+ const items = Array.isArray(persisted.items)
+ ? persisted.items
+ : Array.isArray(persisted)
+ ? persisted
+ : []
+
+ items.forEach((item) => {
+ try {
+ const normalizedEntry = normalizePromotionRedeemFulfillmentOverride(item)
+ const key = buildPromotionRedeemFulfillmentOverrideKey(normalizedEntry)
+ promotionRedeemFulfillmentOverrides.set(key, {
+ ...normalizedEntry,
+ updatedAt: item?.updatedAt || new Date().toISOString(),
+ })
+ } catch (error) {
+ console.warn(
+ `Skipped invalid promotion redeem fulfillment override from ${promotionRedeemFulfillmentOverridesFile}: ${error.message}`,
+ )
+ }
+ })
+ }
+
+ function persistPromotionRedeemFulfillmentOverrides() {
+ if (!promotionRedeemFulfillmentOverridesFile) {
+ return
+ }
+
+ try {
+ fs.mkdirSync(path.dirname(promotionRedeemFulfillmentOverridesFile), { recursive: true })
+ const payload = {
+ updatedAt: new Date().toISOString(),
+ items: Array.from(promotionRedeemFulfillmentOverrides.values()).map((entry) =>
+ cloneJsonValue(entry),
+ ),
+ }
+ const tempFile = `${promotionRedeemFulfillmentOverridesFile}.tmp`
+ fs.writeFileSync(tempFile, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
+ fs.renameSync(tempFile, promotionRedeemFulfillmentOverridesFile)
+ } catch (error) {
+ console.warn(
+ `Failed to persist promotion redeem fulfillment overrides to ${promotionRedeemFulfillmentOverridesFile}: ${error.message}`,
+ )
+ }
+ }
+
+ loadRecordedRedeemedPromotions()
+ loadPromotionRedeemFulfillmentOverrides()
+
+ function isTask10766Account(accountNum) {
+ return task10766Enabled && Boolean(accountNum) && accountNum === task10766AccountNum
+ }
+
+ // Task 10852: Match only the dedicated member number so the special fixture stays tightly scoped.
+ function isTask10852Account(accountNum) {
+ return task10852Enabled && Boolean(accountNum) && accountNum === task10852AccountNum
+ }
+
+ function getMockProfileForAccount(accountNum) {
+ // Task 10766: Preserve the existing failure reproduction if someone points both scenarios at the same member by mistake.
+ if (isTask10766Account(accountNum)) {
+ return {
+ ...mockProfile,
+ eBonus: task10766MemberEBonus,
+ }
+ }
+
+ // Task 10852: Give the dedicated member a recognizable balance so UI verification can confirm the mock path quickly.
+ if (isTask10852Account(accountNum)) {
+ return {
+ ...mockProfile,
+ eBonus: task10852MemberEBonus,
+ }
+ }
+
+ return mockProfile
+ }
+
+ function getPromotionLists(accountNum) {
+ const defaultPromotionCapture = loadPromotionCapture(promotionCaptureFile)
+ const task10766PromotionCapture = task10766Enabled
+ ? loadPromotionCapture(task10766PromotionCaptureFile)
+ : null
+ // Task 10852: Resolve the dedicated fixture on demand so fixture edits take effect without restarting the mock server.
+ const task10852PromotionCapture = task10852Enabled
+ ? createTask10852PromotionCapture(loadPromotionCapture(task10852PromotionCaptureFile))
+ : null
+
+ // Task 10766: Keep the existing redeem-failure reproduction isolated to its own dedicated member.
+ if (isTask10766Account(accountNum) && task10766PromotionCapture) {
+ return {
+ eligiblePromotionList: trimPromotionList(task10766PromotionCapture.eligiblePromotionList),
+ entitledPromotionList: trimPromotionList(task10766PromotionCapture.entitledPromotionList),
+ }
+ }
+
+ // Task 10852: Route the dedicated member to the verified multi-outcome promotion capture without changing default members.
+ if (isTask10852Account(accountNum) && task10852PromotionCapture) {
+ return {
+ eligiblePromotionList: trimPromotionList(task10852PromotionCapture.eligiblePromotionList),
+ entitledPromotionList: trimPromotionList(task10852PromotionCapture.entitledPromotionList),
+ }
+ }
+
+ const accountSpecificPromotionCapture = getAccountSpecificPromotionCapture(accountNum)
+ if (accountSpecificPromotionCapture) {
+ return {
+ eligiblePromotionList: trimPromotionList(accountSpecificPromotionCapture.eligiblePromotionList),
+ entitledPromotionList: trimPromotionList(accountSpecificPromotionCapture.entitledPromotionList),
+ }
+ }
+
+ // Use the shared capture as the default source when no dedicated 10766 fixture is loaded.
+ const eligiblePromotionList =
+ defaultPromotionCapture?.eligiblePromotionList?.length > 0
+ ? defaultPromotionCapture.eligiblePromotionList
+ : normalizedFallbackEligiblePromotionList
+ const entitledPromotionList =
+ defaultPromotionCapture?.entitledPromotionList?.length > 0
+ ? defaultPromotionCapture.entitledPromotionList
+ : []
+
+ return {
+ eligiblePromotionList: trimPromotionList(eligiblePromotionList),
+ entitledPromotionList: trimPromotionList(entitledPromotionList),
+ }
+ }
+
+ function getBackdatePromotionLists(accountNum) {
+ const backdatePromotionCapture = getAccountSpecificPromotionCapture(
+ accountNum,
+ 'promotions.backdate',
+ )
+ if (backdatePromotionCapture) {
+ return {
+ eligiblePromotionList: trimPromotionList(backdatePromotionCapture.eligiblePromotionList),
+ entitledPromotionList: trimPromotionList(backdatePromotionCapture.entitledPromotionList),
+ }
+ }
+
+ return {
+ eligiblePromotionList: [],
+ entitledPromotionList: [],
+ }
+ }
+
+ function getPromotionEntriesForAccount(accountNum, options = {}) {
+ const promotionLists = options?.backdate
+ ? getBackdatePromotionLists(accountNum)
+ : getPromotionLists(accountNum)
+ return [
+ ...(Array.isArray(promotionLists.eligiblePromotionList)
+ ? promotionLists.eligiblePromotionList
+ : []),
+ ...(Array.isArray(promotionLists.entitledPromotionList)
+ ? promotionLists.entitledPromotionList
+ : []),
+ ]
+ }
+
+ function resolvePromotionRedeemSelection(
+ accountNum,
+ promotionId,
+ promotionRuleId,
+ outcomeIds,
+ options = {},
+ ) {
+ const matchedPromotion = getPromotionEntriesForAccount(accountNum, options).find(
+ (promotion) => promotion?.id === promotionId && promotion?.ruleId === promotionRuleId,
+ )
+
+ if (!matchedPromotion) {
+ return null
+ }
+
+ const requestedOutcomeIds = Array.isArray(outcomeIds) ? outcomeIds : []
+ const outcomeMap = new Map()
+ ;(Array.isArray(matchedPromotion.outcomeList) ? matchedPromotion.outcomeList : []).forEach((item) => {
+ if (item?.key !== undefined && item?.key !== null) {
+ outcomeMap.set(item.key, cloneJsonValue(item))
+ }
+ })
+
+ const selectedOutcomes = []
+ for (const outcomeId of requestedOutcomeIds) {
+ if (!outcomeMap.has(outcomeId)) {
+ return null
+ }
+ selectedOutcomes.push(outcomeMap.get(outcomeId))
+ }
+
+ return {
+ promotion: cloneJsonValue(matchedPromotion),
+ selectedOutcomes,
+ }
+ }
+
+ function recordRedeemedPromotionOutcome(accountNum, redeemedOutcome) {
+ if (!accountNum || !redeemedOutcome) {
+ return
+ }
+
+ const accountKey = String(accountNum)
+ const existingList = recordedRedeemedPromotionsByAccount.get(accountKey) || []
+ const nextList = [
+ cloneJsonValue(redeemedOutcome),
+ ...existingList.filter((item) => item?.redemptionId !== redeemedOutcome.redemptionId),
+ ]
+ recordedRedeemedPromotionsByAccount.set(accountKey, nextList)
+ persistRecordedRedeemedPromotions()
+ }
+
+ function getRecordedRedeemedPromotionOutcomes(accountNum) {
+ if (!accountNum) {
+ return []
+ }
+
+ return cloneJsonValue(recordedRedeemedPromotionsByAccount.get(String(accountNum)) || [])
+ }
+
+ function voidRecordedRedeemedPromotionOutcome(redeemedId, loginId) {
+ const numericRedeemedId = Number(redeemedId)
+ if (!Number.isFinite(numericRedeemedId)) {
+ return false
+ }
+
+ const now = new Date().toISOString()
+ const gamingDate = now.slice(0, 10)
+
+ for (const [accountKey, outcomeList] of recordedRedeemedPromotionsByAccount.entries()) {
+ const targetIndex = outcomeList.findIndex((item) => item?.redemptionId === numericRedeemedId)
+ if (targetIndex < 0) {
+ continue
+ }
+
+ const targetRecord = outcomeList[targetIndex]
+ outcomeList[targetIndex] = {
+ ...targetRecord,
+ isVoided: true,
+ voidedBy: loginId || targetRecord?.voidedBy || '',
+ voidedDtm: now,
+ voidedGamingDate: gamingDate,
+ voidedRemark: targetRecord?.voidedRemark || 'Mock voided redeemed promotion',
+ }
+ recordedRedeemedPromotionsByAccount.set(accountKey, outcomeList)
+ persistRecordedRedeemedPromotions()
+ return true
+ }
+
+ return false
+ }
+
+ function findRecordedRedeemedPromotionOutcome(redeemedId) {
+ const numericRedeemedId = Number(redeemedId)
+ if (!Number.isFinite(numericRedeemedId)) {
+ return null
+ }
+
+ for (const outcomeList of recordedRedeemedPromotionsByAccount.values()) {
+ const found = outcomeList.find((item) => item?.redemptionId === numericRedeemedId)
+ if (found) {
+ return found
+ }
+ }
+
+ return null
+ }
+
+ // Task 10766: Return a deterministic redeem failure for the dedicated member so error rendering can be checked reliably.
+ function getPlayerPromotionRedeemMock(accountNum) {
+ if (isTask10766Account(accountNum)) {
+ return {
+ errorCode: task10766RedeemErrorCode,
+ errorMsg: task10766RedeemErrorMsg,
+ result: task10766RedeemResult,
+ }
+ }
+
+ return {
+ errorCode: playerPromotionRedeemErrorCode,
+ errorMsg: playerPromotionRedeemErrorMsg,
+ result: playerPromotionRedeemResult,
+ }
+ }
+
+ function getMockGisCheckPrizeStockResponse(requestBody = {}) {
+ const prizeIdentifier = toNullableString(requestBody?.prizeIdentifier)
+ const response = {
+ propertyId: toNullableString(requestBody?.propertyId) || '',
+ locationId: toNullableString(requestBody?.locationId) || '',
+ prizeCode: toNullableString(requestBody?.prizeCode) || '',
+ prizeName: toNullableString(requestBody?.prizeName) || '',
+ prizeCategory: toNullableString(requestBody?.prizeCategory) || '',
+ supplierID: '',
+ uniqueCode: prizeIdentifier || '',
+ sapItemCode: '',
+ luxuryFlag: '',
+ receivingID: '',
+ quantity: 0,
+ errorCode: '',
+ errorMsg: '',
+ }
+
+ if (!prizeIdentifier || !gisStockUniqueCodes.has(prizeIdentifier)) {
+ return {
+ ...response,
+ errorCode: 'NotFound',
+ errorMsg: 'Not found matching prize',
+ }
+ }
+
+ return {
+ ...response,
+ quantity: 1,
+ }
+ }
+
+ return {
+ host,
+ port,
+ defaultValidateResult,
+ failPins,
+ mockProfile,
+ teamHierarchyStatus,
+ teamHierarchyErrorCode,
+ teamHierarchyMessage,
+ teamHierarchyRole,
+ teamHierarchyDepartment,
+ teamHierarchyTags,
+ prizeRedeemErrorCode,
+ prizeRedeemErrorMsg,
+ prizeRedeemValue,
+ pointVoidErrorCode,
+ pointVoidErrorMsg,
+ pointVoidResult,
+ redeemedPromotionDetailErrorCode,
+ redeemedPromotionDetailErrorMsg,
+ redeemedPromotionDetailIncludeDefaultRecord,
+ redeemedPromotionDetailVoided,
+ memberProfileForceStatusCode,
+ memberProfileForceErrorCode,
+ memberProfileForceErrorMsg,
+ memberProfileDelayMs,
+ playerPromotionRedeemErrorCode,
+ playerPromotionRedeemErrorMsg,
+ playerPromotionRedeemResult,
+ playerPromotionFulfillmentJourneyErrorCode,
+ playerPromotionFulfillmentJourneyErrorMsg,
+ playerPromotionFulfillmentJourneyEmptyResult,
+ playerPromotionFulfillmentJourneyDetailErrorCode,
+ playerPromotionFulfillmentJourneyDetailErrorMsg,
+ playerPromotionFulfillmentJourneyDetailEmptyResult,
+ promotionRedemptionBridgeEnabled,
+ promotionRedemptionBridgeBaseUrl,
+ promotionRedemptionBridgeLoginUsername,
+ promotionRedemptionBridgeLoginPassword,
+ promotionRedemptionBridgeTimeoutMs,
+ promotionRedemptionInitiateSource,
+ promotionRedemptionSourceBucket,
+ promotionRedemptionSimulateErrorAfterBridge,
+ redeemedPromotionVoidErrorCode,
+ redeemedPromotionVoidErrorMsg,
+ redeemedPromotionVoidResult,
+ prizeRedemptionsSyncSuccess,
+ prizeRedemptionsSyncMessage,
+ ldap: {
+ ldapEnabled,
+ ldapHost,
+ ldapPort,
+ ldapBaseDn,
+ ldapManagerDn,
+ ldapGroupBaseDn,
+ ldapManagerPassword,
+ ldapAllowAnyUserMode,
+ ldapAllowAnyUserOu,
+ ldapAllowAnyUserGroupCn,
+ ldapAllowAnyUserMailDomain,
+ ldapForceNoSuchObject,
+ ldapForceNoSuchObjectMatch,
+ },
+ edipForceErrorCode: process.env.MOCK_EDIP_FORCE_ERROR_CODE || '',
+ edipForceErrorMsg: process.env.MOCK_EDIP_FORCE_ERROR_MSG || 'Mock EDIP token error',
+ edipRefreshTokenMustExist:
+ (process.env.MOCK_EDIP_REFRESH_TOKEN_MUST_EXIST || 'false') === 'true',
+ promotionCapture: loadPromotionCapture(promotionCaptureFile),
+ task10766Enabled,
+ task10766AccountNum,
+ // Task 10852: Expose the dedicated scenario flags so route handlers can keep using the shared context contract.
+ task10852Enabled,
+ task10852AccountNum,
+ task15171Enabled,
+ gisStockUniqueCodes,
+ normalizedFallbackEligiblePromotionList,
+ memberProfileResponseFixture,
+ promotionsRedeemedFixture,
+ issuedRefreshTokens,
+ generateToken,
+ resolveAccountNum,
+ tokenSuccessResponse,
+ trimPromotionList,
+ getMockProfileForAccount,
+ getPromotionLists,
+ getBackdatePromotionLists,
+ resolvePromotionRedeemSelection,
+ getPlayerPromotionRedeemMock,
+ getMockGisCheckPrizeStockResponse,
+ setPromotionRedeemFulfillmentOverride,
+ getPromotionRedeemFulfillmentOverrides,
+ clearPromotionRedeemFulfillmentOverrides,
+ resolvePromotionRedeemFulfillmentOverrideByOutcomeId,
+ buildMockRedeemedPromotionOutcome,
+ nextMockPromotionRedemptionId,
+ recordRedeemedPromotionOutcome,
+ getRecordedRedeemedPromotionOutcomes,
+ voidRecordedRedeemedPromotionOutcome,
+ findRecordedRedeemedPromotionOutcome,
+ getAccountSpecificRedeemedPromotionsFixture,
+ cloneJsonValue,
+ }
+}
+
+module.exports = { createMockContext }
diff --git a/src/ldap/createMockLdapService.js b/src/ldap/createMockLdapService.js
new file mode 100644
index 0000000..562152b
--- /dev/null
+++ b/src/ldap/createMockLdapService.js
@@ -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 }
diff --git a/src/logger.js b/src/logger.js
new file mode 100644
index 0000000..74532b3
--- /dev/null
+++ b/src/logger.js
@@ -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 }
diff --git a/src/routes/api/v1/prizeRedemptionsSync.js b/src/routes/api/v1/prizeRedemptionsSync.js
new file mode 100644
index 0000000..1b08c49
--- /dev/null
+++ b/src/routes/api/v1/prizeRedemptionsSync.js
@@ -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
diff --git a/src/routes/edip/tokenGet.js b/src/routes/edip/tokenGet.js
new file mode 100644
index 0000000..2fa926e
--- /dev/null
+++ b/src/routes/edip/tokenGet.js
@@ -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
diff --git a/src/routes/edip/tokenRefresh.js b/src/routes/edip/tokenRefresh.js
new file mode 100644
index 0000000..c2892c4
--- /dev/null
+++ b/src/routes/edip/tokenRefresh.js
@@ -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
diff --git a/src/routes/ewl/bal/cardpinValidate.js b/src/routes/ewl/bal/cardpinValidate.js
new file mode 100644
index 0000000..19b554b
--- /dev/null
+++ b/src/routes/ewl/bal/cardpinValidate.js
@@ -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
diff --git a/src/routes/ewl/bal/playerEarn.js b/src/routes/ewl/bal/playerEarn.js
new file mode 100644
index 0000000..fb61ef0
--- /dev/null
+++ b/src/routes/ewl/bal/playerEarn.js
@@ -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
diff --git a/src/routes/ewl/bal/playerImage.js b/src/routes/ewl/bal/playerImage.js
new file mode 100644
index 0000000..46c3ae6
--- /dev/null
+++ b/src/routes/ewl/bal/playerImage.js
@@ -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
diff --git a/src/routes/ewl/bal/playerPromotionFulfillmentJourney.js b/src/routes/ewl/bal/playerPromotionFulfillmentJourney.js
new file mode 100644
index 0000000..fd5258b
--- /dev/null
+++ b/src/routes/ewl/bal/playerPromotionFulfillmentJourney.js
@@ -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,
+}
diff --git a/src/routes/ewl/bal/playerPromotionFulfillmentJourneyDetail.js b/src/routes/ewl/bal/playerPromotionFulfillmentJourneyDetail.js
new file mode 100644
index 0000000..d9ae6cb
--- /dev/null
+++ b/src/routes/ewl/bal/playerPromotionFulfillmentJourneyDetail.js
@@ -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,
+}
diff --git a/src/routes/ewl/bal/playerPromotionFulfillmentJourneyShared.js b/src/routes/ewl/bal/playerPromotionFulfillmentJourneyShared.js
new file mode 100644
index 0000000..7ef41b9
--- /dev/null
+++ b/src/routes/ewl/bal/playerPromotionFulfillmentJourneyShared.js
@@ -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,
+}
diff --git a/src/routes/ewl/bal/playerPromotionRedeem.js b/src/routes/ewl/bal/playerPromotionRedeem.js
new file mode 100644
index 0000000..11d4c43
--- /dev/null
+++ b/src/routes/ewl/bal/playerPromotionRedeem.js
@@ -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,
+}
diff --git a/src/routes/ewl/bal/pointVoid.js b/src/routes/ewl/bal/pointVoid.js
new file mode 100644
index 0000000..2dd3434
--- /dev/null
+++ b/src/routes/ewl/bal/pointVoid.js
@@ -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
diff --git a/src/routes/ewl/bal/prizeRedeem.js b/src/routes/ewl/bal/prizeRedeem.js
new file mode 100644
index 0000000..7f6ea51
--- /dev/null
+++ b/src/routes/ewl/bal/prizeRedeem.js
@@ -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
diff --git a/src/routes/ewl/bal/promotionAll.js b/src/routes/ewl/bal/promotionAll.js
new file mode 100644
index 0000000..7165c21
--- /dev/null
+++ b/src/routes/ewl/bal/promotionAll.js
@@ -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
diff --git a/src/routes/ewl/bal/redeemedPromotionDetail.js b/src/routes/ewl/bal/redeemedPromotionDetail.js
new file mode 100644
index 0000000..a6a8e43
--- /dev/null
+++ b/src/routes/ewl/bal/redeemedPromotionDetail.js
@@ -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
diff --git a/src/routes/ewl/bal/redeemedPromotionVoid.js b/src/routes/ewl/bal/redeemedPromotionVoid.js
new file mode 100644
index 0000000..4cf2c06
--- /dev/null
+++ b/src/routes/ewl/bal/redeemedPromotionVoid.js
@@ -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
diff --git a/src/routes/ewl/edip/memberProfile.js b/src/routes/ewl/edip/memberProfile.js
new file mode 100644
index 0000000..ad06c53
--- /dev/null
+++ b/src/routes/ewl/edip/memberProfile.js
@@ -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
diff --git a/src/routes/ewl/edip/memberProfileCardNum.js b/src/routes/ewl/edip/memberProfileCardNum.js
new file mode 100644
index 0000000..32520f6
--- /dev/null
+++ b/src/routes/ewl/edip/memberProfileCardNum.js
@@ -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,
+}
diff --git a/src/routes/ewl/mktRm/teamHierarchy.js b/src/routes/ewl/mktRm/teamHierarchy.js
new file mode 100644
index 0000000..2c9bf86
--- /dev/null
+++ b/src/routes/ewl/mktRm/teamHierarchy.js
@@ -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
diff --git a/src/routes/gis/checkPrizeStock.js b/src/routes/gis/checkPrizeStock.js
new file mode 100644
index 0000000..92c82d7
--- /dev/null
+++ b/src/routes/gis/checkPrizeStock.js
@@ -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
diff --git a/src/routes/gis/prize.js b/src/routes/gis/prize.js
new file mode 100644
index 0000000..3d86306
--- /dev/null
+++ b/src/routes/gis/prize.js
@@ -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
diff --git a/src/routes/gis/tokenGet.js b/src/routes/gis/tokenGet.js
new file mode 100644
index 0000000..c2e5d08
--- /dev/null
+++ b/src/routes/gis/tokenGet.js
@@ -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
diff --git a/src/routes/gis/tokenRefresh.js b/src/routes/gis/tokenRefresh.js
new file mode 100644
index 0000000..28c5fa4
--- /dev/null
+++ b/src/routes/gis/tokenRefresh.js
@@ -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
diff --git a/src/routes/gis/xmlResponse.js b/src/routes/gis/xmlResponse.js
new file mode 100644
index 0000000..c4fb939
--- /dev/null
+++ b/src/routes/gis/xmlResponse.js
@@ -0,0 +1,92 @@
+function escapeXml(value) {
+ return String(value ?? '')
+ .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 `
+
+ ${escapeXml(errorCode)}
+ ${escapeXml(errorMsg)}
+ ${escapeXml(accessToken)}
+ ${escapeXml(refreshToken)}
+ ${toPositiveInteger(period, 0)}
+`
+}
+
+function buildPrizeResponseXml({ errorCode = '', errorMsg = '' } = {}) {
+ return `
+
+ ${escapeXml(errorCode)}
+ ${escapeXml(errorMsg)}
+`
+}
+
+function buildCheckPrizeStockResponseEntryXml({
+ propertyId = '',
+ locationId = '',
+ prizeCode = '',
+ prizeName = '',
+ prizeCategory = '',
+ supplierID = '',
+ uniqueCode = '',
+ sapItemCode = '',
+ luxuryFlag = '',
+ receivingID = '',
+ quantity = 0,
+ errorCode = '',
+ errorMsg = '',
+} = {}) {
+ return `
+ ${escapeXml(propertyId)}
+ ${escapeXml(locationId)}
+ ${escapeXml(prizeCode)}
+ ${escapeXml(prizeName)}
+ ${escapeXml(prizeCategory)}
+ ${escapeXml(supplierID)}
+ ${escapeXml(uniqueCode)}
+ ${escapeXml(sapItemCode)}
+ ${escapeXml(luxuryFlag)}
+ ${escapeXml(receivingID)}
+ ${toPositiveInteger(quantity, 0)}
+ ${escapeXml(errorCode)}
+ ${escapeXml(errorMsg)}
+ `
+}
+
+function buildCheckPrizeStockResponseXml(entries = []) {
+ const normalizedEntries = Array.isArray(entries) ? entries : []
+ const responseEntries =
+ normalizedEntries.length > 0
+ ? normalizedEntries.map((entry) => buildCheckPrizeStockResponseEntryXml(entry)).join('\n')
+ : buildCheckPrizeStockResponseEntryXml()
+
+ return `
+
+${responseEntries}
+`
+}
+
+module.exports = {
+ buildTokenResponseXml,
+ buildPrizeResponseXml,
+ buildCheckPrizeStockResponseXml,
+}
diff --git a/src/routes/healthz.js b/src/routes/healthz.js
new file mode 100644
index 0000000..813e17d
--- /dev/null
+++ b/src/routes/healthz.js
@@ -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
diff --git a/src/routes/mockApi/promotionRedeemFulfillmentOverrides.js b/src/routes/mockApi/promotionRedeemFulfillmentOverrides.js
new file mode 100644
index 0000000..98fe690
--- /dev/null
+++ b/src/routes/mockApi/promotionRedeemFulfillmentOverrides.js
@@ -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
diff --git a/src/routes/registerRoutes.js b/src/routes/registerRoutes.js
new file mode 100644
index 0000000..d3feca1
--- /dev/null
+++ b/src/routes/registerRoutes.js
@@ -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 }
diff --git a/src/routes/sms/smsPortal.js b/src/routes/sms/smsPortal.js
new file mode 100644
index 0000000..099f0c9
--- /dev/null
+++ b/src/routes/sms/smsPortal.js
@@ -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
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 0000000..54b03a7
--- /dev/null
+++ b/src/server.js
@@ -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()
diff --git a/tests/createMockContext.test.js b/tests/createMockContext.test.js
new file mode 100644
index 0000000..e327f55
--- /dev/null
+++ b/tests/createMockContext.test.js
@@ -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')
+})
diff --git a/tests/gisCheckPrizeStock.test.js b/tests/gisCheckPrizeStock.test.js
new file mode 100644
index 0000000..932ca28
--- /dev/null
+++ b/tests/gisCheckPrizeStock.test.js
@@ -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, //)
+ assert.match(xml, //)
+ assert.match(xml, /12341234<\/uniqueCode>/)
+ assert.match(xml, /1<\/quantity>/)
+})
diff --git a/tests/memberProfileCardNum.test.js b/tests/memberProfileCardNum.test.js
new file mode 100644
index 0000000..fc51889
--- /dev/null
+++ b/tests/memberProfileCardNum.test.js
@@ -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'],
+ )
+})
diff --git a/tests/memberProfileCardNumRoute.test.js b/tests/memberProfileCardNumRoute.test.js
new file mode 100644
index 0000000..5babd38
--- /dev/null
+++ b/tests/memberProfileCardNumRoute.test.js
@@ -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()
+ }
+})
diff --git a/tests/memberProfileRoute.test.js b/tests/memberProfileRoute.test.js
new file mode 100644
index 0000000..1bd473e
--- /dev/null
+++ b/tests/memberProfileRoute.test.js
@@ -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()
+ }
+})
diff --git a/tests/playerPromotionFulfillmentJourney.test.js b/tests/playerPromotionFulfillmentJourney.test.js
new file mode 100644
index 0000000..63fb6f3
--- /dev/null
+++ b/tests/playerPromotionFulfillmentJourney.test.js
@@ -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')
+ },
+ ))
diff --git a/tests/playerPromotionRedeem.test.js b/tests/playerPromotionRedeem.test.js
new file mode 100644
index 0000000..145e817
--- /dev/null
+++ b/tests/playerPromotionRedeem.test.js
@@ -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)
+})