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) <noreply@anthropic.com>
This commit is contained in:
Rediet
2026-08-03 15:13:34 +03:00
commit 8a34ceaa09
14 changed files with 1722 additions and 0 deletions

23
.env Normal file
View File

@@ -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

23
.env.dev Normal file
View File

@@ -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

23
.env.prod Normal file
View File

@@ -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

23
.env.staging Normal file
View File

@@ -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

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
playwright-report/
test-results/
dist/
*.log
.DS_Store

103
README.md Normal file
View File

@@ -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`.

467
package-lock.json generated Normal file
View File

@@ -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"
}
}
}
}

19
package.json Normal file
View File

@@ -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"
}
}

24
playwright.config.ts Normal file
View File

@@ -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'] },
},
],
});

346
src/api/client.ts Normal file
View File

@@ -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<string> {
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;
}
}
}

57
src/config/env.ts Normal file
View File

@@ -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,
},
};

View File

@@ -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<APIFixtures>({
apiClient: async ({}, use) => {
const client = new NeoPaaSClient();
await use(client);
},
});
export { expect } from '@playwright/test';

View File

@@ -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));
});
});

View File

@@ -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));
});
});