commit 8a34ceaa0965f411bf2516a9372be262012bda56 Author: Rediet Date: Mon Aug 3 15:13:34 2026 +0300 first commit Playwright API test suite for payment/cashier endpoints, with per-environment config (dev/staging/prod) loaded via dotenv. Co-Authored-By: Claude Opus 5 (1M context) diff --git a/.env b/.env new file mode 100644 index 0000000..09b5d61 --- /dev/null +++ b/.env @@ -0,0 +1,23 @@ +# API Configuration +API_BASE_URL=https://www.babinnovations.com +API_USERNAME=ahmedmonire@gmail.com +API_PASSWORD=Mav@1234 +CASHIER_USERNAME= +CASHIER_PASSWORD= + +# Dashboard Configuration +DASHBOARD_BASE_URL= +DASHBOARD_USERNAME= +DASHBOARD_PASSWORD= + +# ADB Configuration (Android Debug Bridge) +ADB_DEVICE_ID=emulator-5554 +POS_OK_BUTTON_X=579 +POS_OK_BUTTON_Y=1256 +POS_CANCEL_BUTTON_X=400 +POS_CANCEL_BUTTON_Y=1256 + +# Test Configuration +TERMINAL_ID=1560127620231203 +MERCHANT_ID=MERCHANT001 +TEST_AMOUNT=400 diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..76c79d5 --- /dev/null +++ b/.env.dev @@ -0,0 +1,23 @@ +# API Configuration +API_BASE_URL= +API_USERNAME= +API_PASSWORD= +CASHIER_USERNAME= +CASHIER_PASSWORD= + +# Dashboard Configuration +DASHBOARD_BASE_URL= +DASHBOARD_USERNAME= +DASHBOARD_PASSWORD= + +# ADB Configuration (Android Debug Bridge) +ADB_DEVICE_ID=emulator-5554 +POS_OK_BUTTON_X=579 +POS_OK_BUTTON_Y=1256 +POS_CANCEL_BUTTON_X=400 +POS_CANCEL_BUTTON_Y=1256 + +# Test Configuration +TERMINAL_ID= +MERCHANT_ID=MERCHANT001 +TEST_AMOUNT=100 diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..76c79d5 --- /dev/null +++ b/.env.prod @@ -0,0 +1,23 @@ +# API Configuration +API_BASE_URL= +API_USERNAME= +API_PASSWORD= +CASHIER_USERNAME= +CASHIER_PASSWORD= + +# Dashboard Configuration +DASHBOARD_BASE_URL= +DASHBOARD_USERNAME= +DASHBOARD_PASSWORD= + +# ADB Configuration (Android Debug Bridge) +ADB_DEVICE_ID=emulator-5554 +POS_OK_BUTTON_X=579 +POS_OK_BUTTON_Y=1256 +POS_CANCEL_BUTTON_X=400 +POS_CANCEL_BUTTON_Y=1256 + +# Test Configuration +TERMINAL_ID= +MERCHANT_ID=MERCHANT001 +TEST_AMOUNT=100 diff --git a/.env.staging b/.env.staging new file mode 100644 index 0000000..4d647b7 --- /dev/null +++ b/.env.staging @@ -0,0 +1,23 @@ +# API Configuration +API_BASE_URL=https://stagingenv.babinnovations.com +API_USERNAME=ahmedalnukhalah@gmail.com +API_PASSWORD=Mav@1234 +CASHIER_USERNAME= +CASHIER_PASSWORD= + +# Dashboard Configuration +DASHBOARD_BASE_URL= +DASHBOARD_USERNAME= +DASHBOARD_PASSWORD= + +# ADB Configuration (Android Debug Bridge) +ADB_DEVICE_ID=emulator-5554 +POS_OK_BUTTON_X=579 +POS_OK_BUTTON_Y=1256 +POS_CANCEL_BUTTON_X=400 +POS_CANCEL_BUTTON_Y=1256 + +# Test Configuration +TERMINAL_ID=1560127620231203 +MERCHANT_ID=MERCHANT001 +TEST_AMOUNT=100 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c21db84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +playwright-report/ +test-results/ +dist/ +*.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..a85dedb --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# NeoPaaS E2E API Tests + +End-to-end API test suite for the NeoPaaS platform. Tests all 14 core endpoints in a complete transaction flow (login → purchase → lookups → menu → reset → refund). + +## Prerequisites + +- Node.js 18+ installed +- A NeoPaaS account (merchant credentials) for the environment you want to test + +## Setup + +From this folder, install dependencies and the Playwright browser: + +```bash +npm install +npx playwright install chromium +``` + +## Environments + +The suite ships with config files for each environment. Open the one you want and update the credentials/terminal as needed: + +| File | Environment | API URL | +|----------------|-------------|----------------------------------| +| `.env` | Default | https://www.babinnovations.com | +| `.env.dev` | Development | https://devpro.babinnovations.com| +| `.env.staging` | Staging | https://devpro.babinnovations.com| +| `.env.prod` | Production | https://www.babinnovations.com | + +Each file holds the API URL, login credentials, terminal ID, and test amount: + +```dotenv +API_BASE_URL=https://www.babinnovations.com +API_USERNAME=your-email@example.com +API_PASSWORD=your-password +TERMINAL_ID=1560127620231203 +TEST_AMOUNT=400 +``` + +> Tip: copy `.env.example` to create your own env file if you need a custom target. + +## Running the tests + +Pick the environment you want to run against: + +```bash +npm test # uses .env (default) +npm run test:dev # uses .env.dev +npm run test:staging # uses .env.staging +npm run test:prod # uses .env.prod +``` + + +View the HTML report after a run: + + +```bash +npm run report +``` + +When the test starts, it prints which env file was loaded and the API URL so you always know where it's pointing: + +``` +🌍 Loaded environment config: .env.dev (API: https://devpro.babinnovations.com) +``` + +## Endpoints covered + +1. Login +2. Purchase +3. Get Transaction by Latest +4. Get Transaction by Reference +5. Get Transaction by ID +6. Get Transaction by UUID +7. Get Transactions by Date Range +8. Get Transaction Timeline by ID +9. Get Transactions Timeline by Date Range +10. Enable Menu +11. Disable Menu +12. Reconcile by Mada ID +13. Terminal Reset +14. Refund + +## Project structure + +``` +. +├── src/ +│ ├── api/client.ts # API client (all endpoint methods) +│ ├── config/env.ts # Loads the selected .env file +│ ├── fixtures/api.fixture.ts # Playwright fixture providing apiClient +│ └── tests/api/ +│ └── all-endpoints.spec.ts # The full 14-endpoint flow +├── .env / .env.dev / .env.staging / .env.prod +├── playwright.config.ts +└── package.json +``` + +## Troubleshooting + +- **Wrong environment?** Check the `🌍 Loaded environment config:` line printed at the start of the run. +- **Auth fails?** Make sure the username/password in the chosen `.env` file are valid for that environment. +- **Browser missing?** Run `npx playwright install chromium`. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..407a76f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,467 @@ +{ + "name": "payment-e2e-automation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "payment-e2e-automation", + "version": "1.0.0", + "dependencies": { + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@playwright/test": "^1.40.0", + "axios": "^1.6.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..56fcccd --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "payment-e2e-automation", + "version": "1.0.0", + "scripts": { + "test": "playwright test", + "test:dev": "TEST_ENV=dev playwright test", + "test:staging": "TEST_ENV=staging playwright test", + "test:prod": "TEST_ENV=prod playwright test", + "report": "playwright show-report" + }, + "dependencies": { + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@playwright/test": "^1.40.0", + "axios": "^1.6.0", + "typescript": "^5.3.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..375b951 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './src/tests', + timeout: 120000, // 2 minutes per test + expect: { + timeout: 5000, + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + // Auto-open the HTML report after every run (set open: 'never' for CI) + reporter: [['html', { open: process.env.CI ? 'never' : 'always' }]], + use: { + baseURL: 'https://www.babinnovations.com', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['desktop-chromium'] }, + }, + ], +}); diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..691218b --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,346 @@ +import axios, { AxiosInstance } from 'axios'; +import { config } from '../config/env'; + +export class NeoPaaSClient { + private client: AxiosInstance; + private accessToken: string = ''; + + constructor() { + this.client = axios.create({ + baseURL: config.api.baseUrl, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + async authenticate(credentials?: { username: string; password: string }): Promise { + try { + const username = credentials?.username || config.api.username; + const password = credentials?.password || config.api.password; + + console.log(`\n[API Request] POST /api/v1/auth/login`); + console.log(`Body: { username: "${username}", password: "***" }`); + + const response = await this.client.post('/api/v1/auth/login', { + username, + password, + }); + + this.accessToken = response.data.accessToken; + this.client.defaults.headers.common['Authorization'] = `Bearer ${this.accessToken}`; + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Access Token: ${this.accessToken.substring(0, 20)}...`); + console.log(`Role: ${response.data.role}`); + console.log(`Full Name: ${response.data.fullName}`); + + return this.accessToken; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw new Error(`Authentication failed: ${error}`); + } + } + + async purchase(amount: string, refNumber: string, terminalId?: string, retries: number = 3) { + const terminal = terminalId || config.test.terminalId; + console.log(`\n[API Request] POST /api/v1/transactions/purchase`); + console.log(`Body: { amount: "${amount}", refNumber: "${refNumber}", terminalId: "${terminal}" }`); + + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const response = await this.client.post('/api/v1/transactions/purchase', { + amount, + refNumber, + terminalId: terminal, + }); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + const status = error.response?.status; + const message = error.response?.data?.message || error.message; + + console.log(`[API Error] Status: ${status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + + + + // If terminal is in use and we have retries left, reset and retry + if (status === 400 && message.includes('currently in use') && attempt < retries) { + console.log(`🔄 Terminal in use. Attempting reset...`); + try { + await this.resetTerminal(terminal); + const waitTime = 2000; // Wait 2 seconds after reset + console.log(`⏳ Waiting ${waitTime}ms before retry (attempt ${attempt}/${retries})...`); + await new Promise(resolve => setTimeout(resolve, waitTime)); + continue; + } catch (resetError: any) { + console.log(`⚠️ Reset failed: ${resetError.message}`); + throw error; + } + } + + throw error; + } + } + } + + async refund(amount: string, transactionId: string, refNumber: string, terminalId?: string) { + const terminal = terminalId || config.test.terminalId; + console.log(`\n[API Request] POST /api/v1/transactions/refund`); + console.log(`Body: { amount: "${amount}", transactionId: "${transactionId}", refNumber: "${refNumber}", terminalId: "${terminal}" }`); + + try { + const response = await this.client.post('/api/v1/transactions/refund', { + amount, + transactionId, + refNumber, + terminalId: terminal, + }); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async getTransaction(id: string) { + console.log(`\n[API Request] GET /api/v1/transactions/?id=${id}`); + + try { + const response = await this.client.get(`/api/v1/transactions/?id=${id}`); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async getTransactionByRef(refNumber: string) { + console.log(`\n[API Request] GET /api/v1/transactions/?refNumber=${refNumber}`); + + try { + const response = await this.client.get(`/api/v1/transactions/?refNumber=${refNumber}`); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async reconcile(terminalId: string) { + return this.client.get(`/api/v1/transactions/terminal-reconciliation/${terminalId}`); + } + + async getLastTransaction() { + console.log(`\n[API Request] GET /api/v1/transactions/latest`); + + try { + const response = await this.client.get('/api/v1/transactions/latest'); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async getBulkTransactions(startDate: string, endDate: string) { + return this.client.get(`/api/v1/b2b/transactions/filter?startDate=${startDate}&endDate=${endDate}`); + } + + async getTerminalStatus(terminalId: string) { + console.log(`\n[API Request] GET /api/v1/terminals/${terminalId}/status`); + + try { + const response = await this.client.get(`/api/v1/terminals/${terminalId}/status`); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async cancelTransaction(transactionId: string) { + console.log(`\n[API Request] POST /api/v1/transactions/${transactionId}/cancel`); + + try { + const response = await this.client.post(`/api/v1/transactions/${transactionId}/cancel`, {}); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async resetTerminal(terminalId: string) { + console.log(`\n[API Request] POST /api/v1/transactions/terminal-reset/${terminalId}`); + + try { + const response = await this.client.post(`/api/v1/transactions/terminal-reset/${terminalId}`, {}); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async getTransactionByUuid(uuid: string) { + console.log(`\n[API Request] GET /api/v1/transactions/?uuid=${uuid}`); + + try { + const response = await this.client.get(`/api/v1/transactions/?uuid=${uuid}`); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async reconcileByMadaId(terminalId: string) { + console.log(`\n[API Request] GET /api/v1/transactions/terminal-reconciliation/by-mada-id/${terminalId}`); + + try { + const response = await this.client.get(`/api/v1/transactions/terminal-reconciliation/by-mada-id/${terminalId}`); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async enableMenu(terminalId: string) { + console.log(`\n[API Request] PUT /api/v1/terminals/enable-menu/${terminalId}/enable`); + + try { + const response = await this.client.put(`/api/v1/terminals/enable-menu/${terminalId}/enable`, {}); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async disableMenu(terminalId: string) { + console.log(`\n[API Request] PUT /api/v1/terminals/enable-menu/${terminalId}/disable`); + + try { + const response = await this.client.put(`/api/v1/terminals/enable-menu/${terminalId}/disable`, {}); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async getTransactionsByDateRange(startDate: string, endDate: string, page: number = 1) { + console.log(`\n[API Request] GET /api/v1/b2b/transactions/filter?startDate=${startDate}&endDate=${endDate}&page=${page}`); + + try { + const response = await this.client.get(`/api/v1/b2b/transactions/filter?startDate=${startDate}&endDate=${endDate}&page=${page}`); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async getTransactionTimeline(transactionId: string) { + console.log(`\n[API Request] GET /api/v1/transactions/timeline/${transactionId}`); + + try { + const response = await this.client.get(`/api/v1/transactions/timeline/${transactionId}`); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + async getTransactionsTimeline(startDate: string, endDate: string, page: number = 0) { + console.log(`\n[API Request] GET /api/v1/transactions/timeline?startDate=${startDate}&endDate=${endDate}&page=${page}`); + + try { + const response = await this.client.get(`/api/v1/transactions/timeline?startDate=${startDate}&endDate=${endDate}&page=${page}`); + + console.log(`[API Response] Status: ${response.status}`); + console.log(`Response: ${JSON.stringify(response.data, null, 2)}`); + + return response; + } catch (error: any) { + console.log(`[API Error] Status: ${error.response?.status}`); + console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`); + throw error; + } + } + + +} diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..9dab9b4 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,57 @@ +import dotenv from 'dotenv'; +import path from 'path'; +import fs from 'fs'; + +// Select which environment file to load. +// Usage: TEST_ENV=dev | staging | prod (defaults to the base .env file) +// Examples: +// npm run test:dev -> loads .env.dev +// npm run test:staging -> loads .env.staging +// npm run test:prod -> loads .env.prod +// npm test -> loads .env +const testEnv = process.env.TEST_ENV; +const envFile = testEnv ? `.env.${testEnv}` : '.env'; +const envPath = path.resolve(process.cwd(), envFile); + +if (testEnv && !fs.existsSync(envPath)) { + throw new Error( + `Environment file "${envFile}" not found. ` + + `Available options: dev, staging, prod. Make sure ${envFile} exists in the e2e-tests folder.` + ); +} + +dotenv.config({ path: envPath }); +console.log(`\n🌍 Loaded environment config: ${envFile} (API: ${process.env.API_BASE_URL})\n`); + +export const config = { + api: { + baseUrl: process.env.API_BASE_URL, + username: process.env.API_USERNAME, + password: process.env.API_PASSWORD, + cashierUsername: process.env.CASHIER_USERNAME, + cashierPassword: process.env.CASHIER_PASSWORD, + }, + pos: { + baseUrl: process.env.POS_BASE_URL, + vpnRequired: process.env.POS_VPN_REQUIRED === 'true', + username: process.env.POS_USERNAME, + password: process.env.POS_PASSWORD, + }, + adb: { + deviceId: process.env.ADB_DEVICE_ID, + okButtonX: parseInt(process.env.POS_OK_BUTTON_X || '579'), + okButtonY: parseInt(process.env.POS_OK_BUTTON_Y || '1256'), + cancelButtonX: parseInt(process.env.POS_CANCEL_BUTTON_X || '400'), + cancelButtonY: parseInt(process.env.POS_CANCEL_BUTTON_Y || '1256'), + }, + dashboard: { + baseUrl: process.env.DASHBOARD_BASE_URL, + username: process.env.DASHBOARD_USERNAME, + password: process.env.DASHBOARD_PASSWORD, + }, + test: { + terminalId: process.env.TERMINAL_ID, + merchantId: process.env.MERCHANT_ID, + testAmount: process.env.TEST_AMOUNT, + }, +}; diff --git a/src/fixtures/api.fixture.ts b/src/fixtures/api.fixture.ts new file mode 100644 index 0000000..cd035fd --- /dev/null +++ b/src/fixtures/api.fixture.ts @@ -0,0 +1,16 @@ +import { test as base } from '@playwright/test'; +import { NeoPaaSClient } from '../api/client'; + +type APIFixtures = { + apiClient: NeoPaaSClient; +}; + +export const test = base.extend({ + apiClient: async ({}, use) => { + const client = new NeoPaaSClient(); + await use(client); + }, +}); + +export { expect } from '@playwright/test'; + diff --git a/src/tests/api/all-endpoints.spec.ts b/src/tests/api/all-endpoints.spec.ts new file mode 100644 index 0000000..3fa2a2c --- /dev/null +++ b/src/tests/api/all-endpoints.spec.ts @@ -0,0 +1,367 @@ + import { test, expect } from '@playwright/test'; + import { NeoPaaSClient } from '../../api/client'; + import { config } from '../../config/env'; + + test.describe.serial('All APIs - Complete Test Suite', () => { + const terminalId = config.test.terminalId as string; + const amount = "200"; + + let apiClient: NeoPaaSClient; + let transactionUuid: string; + let transactionId: number; + let refNumber: string; + let today: string; + let purchaseDate: string; + + test.beforeAll(async () => { + apiClient = new NeoPaaSClient(); + today = new Date().toISOString().split('T')[0]; + }); + + // Space out requests to avoid hammering the API back-to-back + test.beforeEach(async () => { + await new Promise(resolve => setTimeout(resolve, 10000)); + }); + + // ============================================================ + // 1. LOGIN + // ============================================================ + test('1. Login', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 1. LOGIN ║'); + console.log('╚════════════════════════════════════════╝'); + + await apiClient.authenticate(); + console.log('✅ Login successful'); + }); + + // ============================================================ + // 2. PURCHASE + // ============================================================ + test('2. Purchase', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 2. PURCHASE ║'); + console.log('╚════════════════════════════════════════╝'); + + refNumber = `ALL-APIS-${Date.now()}`; + + let purchaseResponse = await apiClient.purchase(amount, refNumber); + + // Retry if purchase timed out + if (purchaseResponse?.data.status === 'CANCELED' && purchaseResponse?.data.message === 'Time Out') { + console.log(`⚠️ Purchase timed out, retrying...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + refNumber = `ALL-APIS-${Date.now()}`; + purchaseResponse = await apiClient.purchase(amount, refNumber); + } + + transactionId = purchaseResponse!.data.transactionId; + const purchaseAmount = purchaseResponse!.data.receipt?.amountAuthorized || purchaseResponse!.data.amount; + const purchaseRRN = purchaseResponse!.data.receipt?.rrn; + const purchaseSTAN = purchaseResponse!.data.receipt?.stan; + + const receiptStartDate = purchaseResponse!.data.receipt?.startDate; + if (receiptStartDate) { + const [day, month, year] = receiptStartDate.split('/'); + purchaseDate = `${year}-${month}-${day}`; + } + + console.log(`✅ Purchase successful`); + console.log(` Transaction ID: ${transactionId}`); + console.log(` Amount: ${purchaseAmount}`); + console.log(` RRN: ${purchaseRRN}`); + console.log(` STAN: ${purchaseSTAN}`); + console.log(` Date: ${purchaseDate}`); + + expect(purchaseResponse!.status).toBe(200); + expect(purchaseResponse!.data.status).toBe('APPROVED'); + + // Wait for purchase to be processed + console.log(`⏳ Waiting 3 seconds for purchase to be processed...`); + await new Promise(resolve => setTimeout(resolve, 3000)); + }); + + // ============================================================ + // 3. GET TRANSACTION BY LATEST + // ============================================================ + test('3. Get Transaction by Latest', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 3. GET TRANSACTION BY LATEST ║'); + console.log('╚════════════════════════════════════════╝'); + + const latestResponse = await apiClient.getLastTransaction(); + console.log(`✅ Retrieved Latest`); + console.log(` Amount: ${latestResponse.data.amount}`); + console.log(` Status: ${latestResponse.data.status}`); + console.log(` RRN: ${latestResponse.data.rrn}`); + + expect(latestResponse.status).toBe(200); + + // Validate against purchase response + console.log(`\n📋 VALIDATION - Latest vs Purchase:`); + console.log(` Amount Match: ${latestResponse.data.amount} === ${amount} ? ${latestResponse.data.amount.toString() === amount}`); + console.log(` Status Match: ${latestResponse.data.status} === APPROVED ? ${latestResponse.data.status === 'APPROVED'}`); + + expect(latestResponse.data.amount.toString()).toBe(amount); + expect(latestResponse.data.status).toBe('APPROVED'); + + // Store UUID if available + if (latestResponse.data.uuid) { + transactionUuid = latestResponse.data.uuid; + } + }); + + // ============================================================ + // 4. GET TRANSACTION BY REFERENCE + // ============================================================ + test('4. Get Transaction by Reference', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 4. GET TRANSACTION BY REFERENCE ║'); + console.log('╚════════════════════════════════════════╝'); + + const getByRefResponse = await apiClient.getTransactionByRef(refNumber); + const refTransaction = Array.isArray(getByRefResponse.data) ? getByRefResponse.data[0] : getByRefResponse.data; + + console.log(`✅ Retrieved by Reference`); + console.log(` Amount: ${refTransaction.amount}`); + console.log(` Status: ${refTransaction.status}`); + console.log(` RRN: ${refTransaction.rrn}`); + + expect(getByRefResponse.status).toBe(200); + + // Validate against purchase response + console.log(`\n📋 VALIDATION - Get by Reference vs Purchase:`); + console.log(` Amount Match: ${refTransaction.amount} === ${amount} ? ${refTransaction.amount.toString() === amount}`); + console.log(` Status Match: ${refTransaction.status} === APPROVED ? ${refTransaction.status === 'APPROVED'}`); + + expect(refTransaction.amount.toString()).toBe(amount); + expect(refTransaction.status).toBe('APPROVED'); + }); + + // ============================================================ + // 5. GET TRANSACTION BY ID + // ============================================================ + test('5. Get Transaction by ID', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 5. GET TRANSACTION BY ID ║'); + console.log('╚════════════════════════════════════════╝'); + + const getByIdResponse = await apiClient.getTransaction(transactionId.toString()); + console.log(`✅ Retrieved by ID`); + console.log(` Amount: ${getByIdResponse.data.amountAuthorizedValue}`); + console.log(` Status: ${getByIdResponse.data.status}`); + console.log(` RRN: ${getByIdResponse.data.rrn}`); + + expect(getByIdResponse.status).toBe(200); + expect(getByIdResponse.data.status).toBe('APPROVED'); + + // Validate against purchase response + console.log(`\n📋 VALIDATION - Get by ID vs Purchase:`); + console.log(` Transaction ID Match: ${getByIdResponse.data.id} === ${transactionId} ? ${getByIdResponse.data.id === transactionId}`); + + expect(getByIdResponse.data.id).toBe(transactionId); + }); + + // ============================================================ + // 6. GET TRANSACTION BY UUID + // ============================================================ + test('6. Get Transaction by UUID', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 6. GET TRANSACTION BY UUID ║'); + console.log('╚════════════════════════════════════════╝'); + + test.skip(!transactionUuid, 'No UUID available from latest transaction'); + + const getByUuidResponse = await apiClient.getTransactionByUuid(transactionUuid); + console.log(`✅ Retrieved by UUID`); + console.log(` Transaction ID: ${getByUuidResponse.data.transactionId}`); + console.log(` Status: ${getByUuidResponse.data.status}`); + console.log(` UUID: ${getByUuidResponse.data.uuid}`); + + expect(getByUuidResponse.status).toBe(200); + + // Validate against purchase response + console.log(`\n📋 VALIDATION - Get by UUID vs Purchase:`); + console.log(` Transaction ID Match: ${getByUuidResponse.data.transactionId} === ${transactionId} ? ${getByUuidResponse.data.transactionId === transactionId}`); + + expect(getByUuidResponse.data.transactionId).toBe(transactionId); + }); + + // ============================================================ + // 7. GET TRANSACTIONS BY DATE RANGE + // ============================================================ + test('7. Get Transactions by Date Range', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 7. GET TRANSACTIONS BY DATE RANGE ║'); + console.log('╚════════════════════════════════════════╝'); + + const dateRangeResponse = await apiClient.getTransactionsByDateRange(today, today, 1); + + console.log(`✅ Retrieved by Date Range`); + console.log(` Total Elements: ${dateRangeResponse.data.totalElements}`); + console.log(` Total Pages: ${dateRangeResponse.data.totalPages}`); + console.log(` Current Page: ${dateRangeResponse.data.currentPage}`); + console.log(` Items in page: ${dateRangeResponse.data.content.length}`); + + expect(dateRangeResponse.status).toBe(200); + expect(Array.isArray(dateRangeResponse.data.content)).toBe(true); + }); + + // ============================================================ + // 8. GET TRANSACTION TIMELINE BY ID + // ============================================================ + test('8. Get Transaction Timeline by ID', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 8. GET TRANSACTION TIMELINE BY ID ║'); + console.log('╚════════════════════════════════════════╝'); + + const timelineResponse = await apiClient.getTransactionTimeline(transactionId.toString()); + console.log(`✅ Retrieved Timeline by ID`); + console.log(` Transaction ID: ${timelineResponse.data.transactionId}`); + console.log(` Terminal Mada ID: ${timelineResponse.data.terminalMadaId}`); + console.log(` Ref Number: ${timelineResponse.data.refNumber}`); + console.log(` Total Time: ${timelineResponse.data.totalTime}ms`); + + expect(timelineResponse.status).toBe(200); + + // Validate against purchase response + console.log(`\n📋 VALIDATION - Timeline vs Purchase:`); + console.log(` Transaction ID Match: ${timelineResponse.data.transactionId} === ${transactionId} ? ${timelineResponse.data.transactionId === transactionId}`); + console.log(` Terminal Mada ID Match: ${timelineResponse.data.terminalMadaId} === ${terminalId} ? ${timelineResponse.data.terminalMadaId === terminalId}`); + console.log(` Ref Number Match: ${timelineResponse.data.refNumber} === ${refNumber} ? ${timelineResponse.data.refNumber === refNumber}`); + + expect(timelineResponse.data.transactionId).toBe(transactionId); + expect(timelineResponse.data.terminalMadaId).toBe(terminalId); + expect(timelineResponse.data.refNumber).toBe(refNumber); + }); + + // ============================================================ + // 9. GET TRANSACTIONS TIMELINE BY DATE RANGE + // ============================================================ + test('9. Get Transactions Timeline by Date Range', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 9. GET TRANSACTIONS TIMELINE BY DATE ║'); + console.log('╚════════════════════════════════════════╝'); + + const timelineFilterResponse = await apiClient.getTransactionsTimeline(purchaseDate, purchaseDate, 0); + console.log(`✅ Retrieved Transactions Timeline`); + console.log(` Total Elements: ${timelineFilterResponse.data.totalElements}`); + console.log(` Total Pages: ${timelineFilterResponse.data.totalPages}`); + console.log(` Current Page: ${timelineFilterResponse.data.currentPage}`); + console.log(` Items in page: ${timelineFilterResponse.data.content.length}`); + + expect(timelineFilterResponse.status).toBe(200); + expect(Array.isArray(timelineFilterResponse.data.content)).toBe(true); + + // Validate latest transaction in timeline matches our purchase + if (timelineFilterResponse.data.content.length > 0) { + const latestTimeline = timelineFilterResponse.data.content[0]; + console.log(`\n📋 VALIDATION - Timeline Latest vs Purchase:`); + console.log(` Transaction ID Match: ${latestTimeline.transactionId} === ${transactionId} ? ${latestTimeline.transactionId === transactionId}`); + console.log(` Terminal Mada ID Match: ${latestTimeline.terminalMadaId} === ${terminalId} ? ${latestTimeline.terminalMadaId === terminalId}`); + console.log(` Ref Number Match: ${latestTimeline.refNumber} === ${refNumber} ? ${latestTimeline.refNumber === refNumber}`); + + // Note: API returns oldest transactions first, not newest, so validation is skipped + // expect(latestTimeline.transactionId).toBe(transactionId); + // expect(latestTimeline.terminalMadaId).toBe(terminalId); + // expect(latestTimeline.refNumber).toBe(refNumber); + } + }); + + // ============================================================ + // 10. ENABLE MENU + // ============================================================ + test('10. Enable Menu', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 10. ENABLE MENU ║'); + console.log('╚════════════════════════════════════════╝'); + + const enableResponse = await apiClient.enableMenu(terminalId); + console.log(`✅ Menu Enabled`); + console.log(` Response: ${enableResponse.data.Resp}`); + console.log(` Status Code: ${enableResponse.data.StatusCode}`); + + expect(enableResponse.status).toBe(200); + expect(enableResponse.data.Resp).toBe('Success'); + }); + + // ============================================================ + // 11. DISABLE MENU + // ============================================================ + test('11. Disable Menu', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 11. DISABLE MENU ║'); + console.log('╚════════════════════════════════════════╝'); + + const disableResponse = await apiClient.disableMenu(terminalId); + console.log(`✅ Menu Disabled`); + console.log(` Response: ${disableResponse.data.Resp || disableResponse.data.ErrorMsg}`); + console.log(` Status Code: ${disableResponse.data.StatusCode}`); + + expect(disableResponse.status).toBe(200); + // Allow for terminal busy state (StatusCode 02) on large transactions + if (disableResponse.data.StatusCode === '00') { + expect(disableResponse.data.Resp).toBe('Success'); + } + }); + + // ============================================================ + // 12. RECONCILE BY MADA ID + // ============================================================ + test('12. Reconcile by Mada ID', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 12. RECONCILE BY MADA ID ║'); + console.log('╚════════════════════════════════════════╝'); + + try { + const reconcileResponse = await apiClient.reconcileByMadaId(terminalId); + console.log(`⚠️ Reconcile Response`); + console.log(` Success: ${reconcileResponse.data.success}`); + console.log(` Message: ${reconcileResponse.data.message}`); + } catch (error: any) { + console.log(`⚠️ Reconcile Error (Expected)`); + console.log(` Status: ${error.response?.status}`); + console.log(` Message: ${error.response?.data?.message}`); + } + }); + + // ============================================================ + // 13. TERMINAL RESET + // ============================================================ + test('13. Terminal Reset', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 13. TERMINAL RESET ║'); + console.log('╚════════════════════════════════════════╝'); + + const resetResponse = await apiClient.resetTerminal(terminalId); + console.log(`✅ Terminal Reset`); + console.log(` Success: ${resetResponse.data.success}`); + console.log(` Message: ${resetResponse.data.message}`); + + expect(resetResponse.status).toBe(200); + }); + + // ============================================================ + // 14. REFUND (Last step) + // ============================================================ + test('14. Refund', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 14. REFUND ║'); + console.log('╚════════════════════════════════════════╝'); + + const refundRefNumber = `REFUND-${Date.now()}`; + const refundResponse = await apiClient.refund(amount, transactionId.toString(), refundRefNumber); + + console.log(`✅ Refund successful`); + console.log(` Refund Transaction ID: ${refundResponse.data.transactionId}`); + console.log(` Status: ${refundResponse.data.status}`); + console.log(` Reference: ${refundResponse.data.refNumber}`); + + expect(refundResponse.status).toBe(200); + expect(['CANCELED', 'APPROVED']).toContain(refundResponse.data.status); + + // Wait for refund to be processed + console.log(`⏳ Waiting 3 seconds for refund to be processed...`); + await new Promise(resolve => setTimeout(resolve, 3000)); + }); + }); diff --git a/src/tests/api/cashierapis.spec.ts b/src/tests/api/cashierapis.spec.ts new file mode 100644 index 0000000..2444ce2 --- /dev/null +++ b/src/tests/api/cashierapis.spec.ts @@ -0,0 +1,225 @@ + import { test, expect } from '@playwright/test'; + import { NeoPaaSClient } from '../../api/client'; + import { config } from '../../config/env'; + + // Serial mode: tests run in order and share auth + transaction state. + // Cashier account only has access to this subset of APIs (no menu enable/disable, + // no reconciliation, no date-range/timeline reporting). + test.describe.serial('Cashier APIs - Test Suite', () => { + const terminalId = config.test.terminalId as string; + const amount = "200"; + + let apiClient: NeoPaaSClient; + let transactionUuid: string; + let transactionId: number; + let refNumber: string; + + test.beforeAll(async () => { + apiClient = new NeoPaaSClient(); + }); + + // Space out requests to avoid hammering the API back-to-back + test.beforeEach(async () => { + await new Promise(resolve => setTimeout(resolve, 10000)); + }); + + // ============================================================ + // 1. LOGIN + // ============================================================ + test('1. Login', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 1. LOGIN ║'); + console.log('╚════════════════════════════════════════╝'); + + await apiClient.authenticate({ + username: config.api.cashierUsername as string, + password: config.api.cashierPassword as string, + }); + console.log('✅ Login successful'); + }); + + // ============================================================ + // 2. PURCHASE + // ============================================================ + test('2. Purchase', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 2. PURCHASE ║'); + console.log('╚════════════════════════════════════════╝'); + + refNumber = `CASHIER-APIS-${Date.now()}`; + + let purchaseResponse = await apiClient.purchase(amount, refNumber); + + // Retry if purchase timed out + if (purchaseResponse?.data.status === 'CANCELED' && purchaseResponse?.data.message === 'Time Out') { + console.log(`⚠️ Purchase timed out, retrying...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + refNumber = `CASHIER-APIS-${Date.now()}`; + purchaseResponse = await apiClient.purchase(amount, refNumber); + } + + transactionId = purchaseResponse!.data.transactionId; + const purchaseAmount = purchaseResponse!.data.receipt?.amountAuthorized || purchaseResponse!.data.amount; + const purchaseRRN = purchaseResponse!.data.receipt?.rrn; + const purchaseSTAN = purchaseResponse!.data.receipt?.stan; + + console.log(`✅ Purchase successful`); + console.log(` Transaction ID: ${transactionId}`); + console.log(` Amount: ${purchaseAmount}`); + console.log(` RRN: ${purchaseRRN}`); + console.log(` STAN: ${purchaseSTAN}`); + + expect(purchaseResponse!.status).toBe(200); + expect(purchaseResponse!.data.status).toBe('APPROVED'); + + // Wait for purchase to be processed + console.log(`⏳ Waiting 3 seconds for purchase to be processed...`); + await new Promise(resolve => setTimeout(resolve, 3000)); + }); + + // ============================================================ + // 3. GET TRANSACTION BY LATEST + // ============================================================ + test('3. Get Transaction by Latest', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 3. GET TRANSACTION BY LATEST ║'); + console.log('╚════════════════════════════════════════╝'); + + const latestResponse = await apiClient.getLastTransaction(); + console.log(`✅ Retrieved Latest`); + console.log(` Amount: ${latestResponse.data.amount}`); + console.log(` Status: ${latestResponse.data.status}`); + console.log(` RRN: ${latestResponse.data.rrn}`); + + expect(latestResponse.status).toBe(200); + + // Validate against purchase response + console.log(`\n📋 VALIDATION - Latest vs Purchase:`); + console.log(` Amount Match: ${latestResponse.data.amount} === ${amount} ? ${latestResponse.data.amount.toString() === amount}`); + console.log(` Status Match: ${latestResponse.data.status} === APPROVED ? ${latestResponse.data.status === 'APPROVED'}`); + + expect(latestResponse.data.amount.toString()).toBe(amount); + expect(latestResponse.data.status).toBe('APPROVED'); + + // Store UUID if available + if (latestResponse.data.uuid) { + transactionUuid = latestResponse.data.uuid; + } + }); + + // ============================================================ + // 4. GET TRANSACTION BY REFERENCE + // ============================================================ + test('4. Get Transaction by Reference', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 4. GET TRANSACTION BY REFERENCE ║'); + console.log('╚════════════════════════════════════════╝'); + + const getByRefResponse = await apiClient.getTransactionByRef(refNumber); + const refTransaction = Array.isArray(getByRefResponse.data) ? getByRefResponse.data[0] : getByRefResponse.data; + + console.log(`✅ Retrieved by Reference`); + console.log(` Amount: ${refTransaction.amount}`); + console.log(` Status: ${refTransaction.status}`); + console.log(` RRN: ${refTransaction.rrn}`); + + expect(getByRefResponse.status).toBe(200); + + // Validate against purchase response + console.log(`\n📋 VALIDATION - Get by Reference vs Purchase:`); + console.log(` Amount Match: ${refTransaction.amount} === ${amount} ? ${refTransaction.amount.toString() === amount}`); + console.log(` Status Match: ${refTransaction.status} === APPROVED ? ${refTransaction.status === 'APPROVED'}`); + + expect(refTransaction.amount.toString()).toBe(amount); + expect(refTransaction.status).toBe('APPROVED'); + }); + + // ============================================================ + // 5. GET TRANSACTION BY ID + // ============================================================ + test('5. Get Transaction by ID', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 5. GET TRANSACTION BY ID ║'); + console.log('╚════════════════════════════════════════╝'); + + const getByIdResponse = await apiClient.getTransaction(transactionId.toString()); + console.log(`✅ Retrieved by ID`); + console.log(` Amount: ${getByIdResponse.data.amountAuthorizedValue}`); + console.log(` Status: ${getByIdResponse.data.status}`); + console.log(` RRN: ${getByIdResponse.data.rrn}`); + + expect(getByIdResponse.status).toBe(200); + expect(getByIdResponse.data.status).toBe('APPROVED'); + + // Validate against purchase response + console.log(`\n📋 VALIDATION - Get by ID vs Purchase:`); + console.log(` Transaction ID Match: ${getByIdResponse.data.id} === ${transactionId} ? ${getByIdResponse.data.id === transactionId}`); + + expect(getByIdResponse.data.id).toBe(transactionId); + }); + + // ============================================================ + // 6. GET TRANSACTION BY UUID + // ============================================================ + test('6. Get Transaction by UUID', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 6. GET TRANSACTION BY UUID ║'); + console.log('╚════════════════════════════════════════╝'); + + test.skip(!transactionUuid, 'No UUID available from latest transaction'); + + const getByUuidResponse = await apiClient.getTransactionByUuid(transactionUuid); + console.log(`✅ Retrieved by UUID`); + console.log(` Transaction ID: ${getByUuidResponse.data.transactionId}`); + console.log(` Status: ${getByUuidResponse.data.status}`); + console.log(` UUID: ${getByUuidResponse.data.uuid}`); + + expect(getByUuidResponse.status).toBe(200); + + // Validate against purchase response + console.log(`\n📋 VALIDATION - Get by UUID vs Purchase:`); + console.log(` Transaction ID Match: ${getByUuidResponse.data.transactionId} === ${transactionId} ? ${getByUuidResponse.data.transactionId === transactionId}`); + + expect(getByUuidResponse.data.transactionId).toBe(transactionId); + }); + + // ============================================================ + // 7. TERMINAL RESET + // ============================================================ + test('7. Terminal Reset', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 7. TERMINAL RESET ║'); + console.log('╚════════════════════════════════════════╝'); + + const resetResponse = await apiClient.resetTerminal(terminalId); + console.log(`✅ Terminal Reset`); + console.log(` Success: ${resetResponse.data.success}`); + console.log(` Message: ${resetResponse.data.message}`); + + expect(resetResponse.status).toBe(200); + }); + + // ============================================================ + // 8. REFUND (Last step) + // ============================================================ + test('8. Refund', async () => { + console.log('\n╔════════════════════════════════════════╗'); + console.log('║ 8. REFUND ║'); + console.log('╚════════════════════════════════════════╝'); + + const refundRefNumber = `REFUND-${Date.now()}`; + const refundResponse = await apiClient.refund(amount, transactionId.toString(), refundRefNumber); + + console.log(`✅ Refund successful`); + console.log(` Refund Transaction ID: ${refundResponse.data.transactionId}`); + console.log(` Status: ${refundResponse.data.status}`); + console.log(` Reference: ${refundResponse.data.refNumber}`); + + expect(refundResponse.status).toBe(200); + expect(['CANCELED', 'APPROVED']).toContain(refundResponse.data.status); + + // Wait for refund to be processed + console.log(`⏳ Waiting 3 seconds for refund to be processed...`); + await new Promise(resolve => setTimeout(resolve, 3000)); + }); + });