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

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