From 633690b0c438b1e94e1859764eead8c3e23fd8e6 Mon Sep 17 00:00:00 2001 From: "DESKTOP-U5J7A7B\\Willtech007" Date: Mon, 12 Oct 2026 17:55:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e.p/promotions-redeemed.json | 97 + fixtures/award.promotions.100000059.json | 1739 +++++++++++++ fixtures/member.profile.json | 47 + fixtures/promotions.backdate.800038008.json | 116 + fixtures/promotions.capture.10766.json | 45 + fixtures/promotions.capture.10852.json | 1066 ++++++++ fixtures/promotions.capture.800038008.json | 771 ++++++ fixtures/promotions.capture.800048718.json | 401 +++ fixtures/promotions.capture.80010991.json | 402 +++ fixtures/promotions.capture.80038008.json | 1425 ++++++++++ ...s.capture.80038008.json.bak.20260505173439 | 1435 +++++++++++ fixtures/promotions.sample.json | 402 +++ fixtures/token.get.json | 7 + runtime/redeemed-promotions.recorded.json | 2293 +++++++++++++++++ src/context/createMockContext.js | 1310 ++++++++++ src/ldap/createMockLdapService.js | 603 +++++ src/logger.js | 48 + src/routes/api/v1/prizeRedemptionsSync.js | 16 + src/routes/edip/tokenGet.js | 17 + src/routes/edip/tokenRefresh.js | 28 + src/routes/ewl/bal/cardpinValidate.js | 24 + src/routes/ewl/bal/playerEarn.js | 27 + src/routes/ewl/bal/playerImage.js | 13 + .../bal/playerPromotionFulfillmentJourney.js | 91 + ...playerPromotionFulfillmentJourneyDetail.js | 153 ++ ...playerPromotionFulfillmentJourneyShared.js | 37 + src/routes/ewl/bal/playerPromotionRedeem.js | 643 +++++ src/routes/ewl/bal/pointVoid.js | 11 + src/routes/ewl/bal/prizeRedeem.js | 11 + src/routes/ewl/bal/promotionAll.js | 61 + src/routes/ewl/bal/redeemedPromotionDetail.js | 47 + src/routes/ewl/bal/redeemedPromotionVoid.js | 216 ++ src/routes/ewl/edip/memberProfile.js | 122 + src/routes/ewl/edip/memberProfileCardNum.js | 62 + src/routes/ewl/mktRm/teamHierarchy.js | 62 + src/routes/gis/checkPrizeStock.js | 15 + src/routes/gis/prize.js | 14 + src/routes/gis/tokenGet.js | 30 + src/routes/gis/tokenRefresh.js | 48 + src/routes/gis/xmlResponse.js | 92 + src/routes/healthz.js | 12 + .../promotionRedeemFulfillmentOverrides.js | 60 + src/routes/registerRoutes.js | 51 + src/routes/sms/smsPortal.js | 39 + src/server.js | 31 + tests/createMockContext.test.js | 199 ++ tests/gisCheckPrizeStock.test.js | 57 + tests/memberProfileCardNum.test.js | 67 + tests/memberProfileCardNumRoute.test.js | 103 + tests/memberProfileRoute.test.js | 107 + .../playerPromotionFulfillmentJourney.test.js | 242 ++ tests/playerPromotionRedeem.test.js | 463 ++++ 52 files changed, 15478 insertions(+) create mode 100644 e.p/promotions-redeemed.json create mode 100644 fixtures/award.promotions.100000059.json create mode 100644 fixtures/member.profile.json create mode 100644 fixtures/promotions.backdate.800038008.json create mode 100644 fixtures/promotions.capture.10766.json create mode 100644 fixtures/promotions.capture.10852.json create mode 100644 fixtures/promotions.capture.800038008.json create mode 100644 fixtures/promotions.capture.800048718.json create mode 100644 fixtures/promotions.capture.80010991.json create mode 100644 fixtures/promotions.capture.80038008.json create mode 100644 fixtures/promotions.capture.80038008.json.bak.20260505173439 create mode 100644 fixtures/promotions.sample.json create mode 100644 fixtures/token.get.json create mode 100644 runtime/redeemed-promotions.recorded.json create mode 100644 src/context/createMockContext.js create mode 100644 src/ldap/createMockLdapService.js create mode 100644 src/logger.js create mode 100644 src/routes/api/v1/prizeRedemptionsSync.js create mode 100644 src/routes/edip/tokenGet.js create mode 100644 src/routes/edip/tokenRefresh.js create mode 100644 src/routes/ewl/bal/cardpinValidate.js create mode 100644 src/routes/ewl/bal/playerEarn.js create mode 100644 src/routes/ewl/bal/playerImage.js create mode 100644 src/routes/ewl/bal/playerPromotionFulfillmentJourney.js create mode 100644 src/routes/ewl/bal/playerPromotionFulfillmentJourneyDetail.js create mode 100644 src/routes/ewl/bal/playerPromotionFulfillmentJourneyShared.js create mode 100644 src/routes/ewl/bal/playerPromotionRedeem.js create mode 100644 src/routes/ewl/bal/pointVoid.js create mode 100644 src/routes/ewl/bal/prizeRedeem.js create mode 100644 src/routes/ewl/bal/promotionAll.js create mode 100644 src/routes/ewl/bal/redeemedPromotionDetail.js create mode 100644 src/routes/ewl/bal/redeemedPromotionVoid.js create mode 100644 src/routes/ewl/edip/memberProfile.js create mode 100644 src/routes/ewl/edip/memberProfileCardNum.js create mode 100644 src/routes/ewl/mktRm/teamHierarchy.js create mode 100644 src/routes/gis/checkPrizeStock.js create mode 100644 src/routes/gis/prize.js create mode 100644 src/routes/gis/tokenGet.js create mode 100644 src/routes/gis/tokenRefresh.js create mode 100644 src/routes/gis/xmlResponse.js create mode 100644 src/routes/healthz.js create mode 100644 src/routes/mockApi/promotionRedeemFulfillmentOverrides.js create mode 100644 src/routes/registerRoutes.js create mode 100644 src/routes/sms/smsPortal.js create mode 100644 src/server.js create mode 100644 tests/createMockContext.test.js create mode 100644 tests/gisCheckPrizeStock.test.js create mode 100644 tests/memberProfileCardNum.test.js create mode 100644 tests/memberProfileCardNumRoute.test.js create mode 100644 tests/memberProfileRoute.test.js create mode 100644 tests/playerPromotionFulfillmentJourney.test.js create mode 100644 tests/playerPromotionRedeem.test.js 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) +})