diff --git a/.github/workflows/postman-tests.yml b/.github/workflows/postman-tests.yml deleted file mode 100644 index 0344ab8e..00000000 --- a/.github/workflows/postman-tests.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Postman Tests - -on: - pull_request: -env: - REEVE_DB_MIGRATIONS_REPO: cardano-foundation/cf-reeve-db-migrations - REEVE_DB_MIGRATIONS_REF: main - REEVE_DB_MIGRATIONS_PATH: cf-application/src/main/resources/db/migration/postgresql/cf-reeve-db-migrations - GITLAB_MAVEN_REGISTRY_URL: ${{ secrets.GITLAB_MAVEN_REGISTRY_URL }} - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Checkout cf-reeve-db-migrations - uses: actions/checkout@v4 - with: - repository: ${{ env.REEVE_DB_MIGRATIONS_REPO }} - ref: ${{ env.REEVE_DB_MIGRATIONS_REF }} - ssh-key: ${{ secrets.CF_REEVE_DB_MIGRATIONS_SSH_DEPLOY_KEY }} - path: ${{ env.REEVE_DB_MIGRATIONS_PATH }} - - - name: Set up Docker Compose - run: | - docker compose build --build-arg GITLAB_MAVEN_REGISTRY_URL="${GITLAB_MAVEN_REGISTRY_URL}" - docker compose up -d - timeout 150 docker compose logs -f api || true - - name: Services health-check API - run: | - timeout 180 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:9000/swagger-ui/index.html)" != "200" ]]; do sleep 2;echo "."; done' - echo "API is up" - docker ps - - name: Services health-check Keycloak - run: | - timeout 180 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8080/realms/master)" != "200" ]]; do sleep 2;echo "."; done' - echo "Keycloak is up" - docker ps - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: Install Newman (Postman CLI) - run: npm install -g newman - - name: Run Postman Collection with Environment - run: | - newman run postman/Reeve_Integration.postman_collection.json \ - --environment postman/Reeve_env.postman_environment.json diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 00000000..c39b147c --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,36 @@ +name: Sync platform versions + +on: + repository_dispatch: + types: + - cf-reeve-platform-pr + +jobs: + create-or-update-pr: + runs-on: ubuntu-latest + steps: + - name: test + run: | + echo "triggered" + echo "payload: ${{ github.event.client_payload.TRIGGERING_PAYLOAD }}" + VERSION=$(echo $PAYLOAD | jq -r .version) + PR_NAME=$(echo $PAYLOAD | jq -r .prName) + SOURCE_BRANCH=$(echo $PAYLOAD | jq -r .sourceBranch) + TARGET_BRANCH=$(echo $PAYLOAD | jq -r .targetBranch) + + VERSION_NEW="${{ github.event.client_payload.TRIGGERING_PAYLOAD.version }}" + + echo "version direct access: $VERSION_NEW" + + # check if pr with source branch already exists + if ! gh pr view $SOURCE_BRANCH + then + gh pr create --title "$PR_NAME" --body "" --base $TARGET_BRANCH --head $SOURCE_BRANCH + fi + gh pr checkout $SOURCE_BRANCH + + sed -i -E "s|extra\[\"cfLobPlatformVersion\"\] = \".*\"|extra\[\"cfLobPlatformVersion\"\] = \"test-version\"|g" build.gradle.kts + + git add build.gradle.kts + git commit -m "chore: bump platform version" + git push diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..e93c71f2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,109 @@ +name: e2e Tests + +on: + pull_request: +env: + REEVE_DB_MIGRATIONS_REPO: cardano-foundation/cf-reeve-db-migrations + REEVE_DB_MIGRATIONS_REF: main + REEVE_DB_MIGRATIONS_PATH: cf-application/src/main/resources/db/migration/postgresql/cf-reeve-db-migrations + GITLAB_MAVEN_REGISTRY_URL: ${{ secrets.GITLAB_MAVEN_REGISTRY_URL }} + +jobs: + postman: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Checkout cf-reeve-db-migrations + uses: actions/checkout@v4 + with: + repository: ${{ env.REEVE_DB_MIGRATIONS_REPO }} + ref: ${{ env.REEVE_DB_MIGRATIONS_REF }} + ssh-key: ${{ secrets.CF_REEVE_DB_MIGRATIONS_SSH_DEPLOY_KEY }} + path: ${{ env.REEVE_DB_MIGRATIONS_PATH }} + + - name: Set up Docker Compose + run: | + docker compose build --build-arg GITLAB_MAVEN_REGISTRY_URL="${GITLAB_MAVEN_REGISTRY_URL}" + docker compose up -d + timeout 150 docker compose logs -f api || true + - name: Services health-check API + run: | + timeout 180 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:9000/swagger-ui/index.html)" != "200" ]]; do sleep 2;echo "."; done' + echo "API is up" + docker ps + - name: Services health-check Keycloak + run: | + timeout 180 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8080/realms/master)" != "200" ]]; do sleep 2;echo "."; done' + echo "Keycloak is up" + docker ps + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install Newman (Postman CLI) + run: npm install -g newman + - name: Run Postman Collection with Environment + run: | + newman run postman/Reeve_Integration.postman_collection.json \ + --environment postman/Reeve_env.postman_environment.json + + playwright: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Checkout cf-reeve-db-migrations + uses: actions/checkout@v4 + with: + repository: ${{ env.REEVE_DB_MIGRATIONS_REPO }} + ref: ${{ env.REEVE_DB_MIGRATIONS_REF }} + ssh-key: ${{ secrets.CF_REEVE_DB_MIGRATIONS_SSH_DEPLOY_KEY }} + path: ${{ env.REEVE_DB_MIGRATIONS_PATH }} + + - name: Set up Docker Compose + run: | + docker compose build --build-arg GITLAB_MAVEN_REGISTRY_URL="${GITLAB_MAVEN_REGISTRY_URL}" + docker compose up -d + timeout 150 docker compose logs -f api || true + + - name: Services health-check API + run: | + timeout 180 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:9000/swagger-ui/index.html)" != "200" ]]; do sleep 2;echo "."; done' + echo "API is up" + docker ps + + - name: Services health-check Keycloak + run: | + timeout 180 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8080/realms/master)" != "200" ]]; do sleep 2;echo "."; done' + echo "Keycloak is up" + docker ps + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 24 + + - name: Install Playwright dependencies + working-directory: playwright + run: | + npm ci + npx playwright install --with-deps + + - name: Create .env file + working-directory: playwright + run: | + echo "API_URL=http://localhost:9000/api/v1" >> .env + echo "LOGIN_URL=http://localhost:8080" >> .env + echo "MANAGER_USER=${{ secrets.PLAYWRIGHT_USER }}" >> .env + echo "MANAGER_PASSWORD=${{ secrets.PLAYWRIGHT_PASSWORD }}" >> .env + echo "API_LOG_REQUEST=false" >> .env + echo "ORGANIZATION_ID=${{ secrets.PLAYWRIGHT_ORGANIZATION_ID }}" >> .env + echo "CI=true" >> .env + + - name: Run Playwright e2e tests + working-directory: playwright + run: npm run test diff --git a/build.gradle.kts b/build.gradle.kts index f922d301..68e70919 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,7 +55,7 @@ subprojects { extra["springBootVersion"] = "3.3.3" extra["springCloudVersion"] = "2023.0.0" extra["jMoleculesVersion"] = "2023.1.0" - extra["cfLobPlatformVersion"] = "1.3.0-release-1.3.0-cb5d279-GHRUN19708586929" + extra["cfLobPlatformVersion"] = "1.3.0-PR495-a1d91c3-GHRUN19827668643" dependencies { compileOnly("org.projectlombok:lombok:1.18.32") diff --git a/playwright/.gitignore b/playwright/.gitignore new file mode 100644 index 00000000..ae6d623e --- /dev/null +++ b/playwright/.gitignore @@ -0,0 +1,28 @@ +/node_modules +/.pnp +.pnp.js + +# testing +/coverage +.auth/ +.logs/ +.reports/ +/resources/ +allure-results +**/.features-gen/**/*.spec.js + +# production +/build + +# misc +.DS_Store +../.env + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/resources + diff --git a/playwright/README.md b/playwright/README.md new file mode 100644 index 00000000..bc0a6318 --- /dev/null +++ b/playwright/README.md @@ -0,0 +1,54 @@ +# ๐Ÿงช Reeve API Automation Framework + +This project uses [Playwright](https://playwright.dev/) with BDD-style tests using [playwright-bdd](https://github.com/folke/playwright-bdd). + +## ๐Ÿ“ฆ Installation + +To install all dependencies, including Playwright and playwright-bdd: + +- Install Playwright and Playwright-BDD +``` +npm i -D @playwright/test playwright-bdd +``` +``` +npx playwright install +``` +- Install only Playwright-BDD +``` +npm i -D playwright-bdd +``` +## Env file to run in local + +1. Create a `.env` file at the root of the playwright folder. +2. Use next structure as example. +3. Ask a team member for the required environment variables & corresponding values for the API, KEYCLOAK and extra application necessary variables. + +``` +API_URL= +LOGIN_URL= +MANAGER_USER= +MANAGER_PASSWORD= +API_LOG_REQUEST= +ORGANIZATION_ID= +``` + +## โš™๏ธ Test run in local: + +### Run all tests in feature files + npm test +### You can run a specific .feature file by passing it as an argument: + npm test "your-feature-file.feature" + +## ๐Ÿ“‚ Test Structure + +The test suite follows a BDD-style structure using [Gherkin](https://cucumber.io/docs/gherkin/) +feature files and corresponding step definitions. + +### ๐Ÿ”ธ Folder layout +- `tests/` + - `e2e/`: Contains `.feature` files written in Gherkin syntax. Each file describes user scenarios using Given, When, and Then steps. + - `test-scenarios.feature` + - `other-tests.feature` + - `steps/`: Contains the step definitions โ€” the TypeScript code that implements the behavior described in the feature files. + - `test-scenarios.steps.ts` + diff --git a/playwright/api/api-helpers/batches-status-codes.ts b/playwright/api/api-helpers/batches-status-codes.ts new file mode 100644 index 00000000..1a5ef86e --- /dev/null +++ b/playwright/api/api-helpers/batches-status-codes.ts @@ -0,0 +1,8 @@ +export enum BatchesStatusCodes { + APPROVE = "APPROVE", + PENDING = "PENDING", + INVALID = "INVALID", + PUBLISH = "PUBLISH", + PUBLISHED = "PUBLISHED" + +} \ No newline at end of file diff --git a/playwright/api/api-helpers/enpoints.ts b/playwright/api/api-helpers/enpoints.ts new file mode 100644 index 00000000..cc4048b9 --- /dev/null +++ b/playwright/api/api-helpers/enpoints.ts @@ -0,0 +1,42 @@ +import * as process from "process"; + +export class Reeve { + static readonly BASE_URL = process.env.API_URL as string; + static readonly LOGIN_URL = process.env.LOGIN_URL as string; + + static SignIn = class { + public static get Base() { + return `${Reeve.LOGIN_URL}/realms/reeve-master/protocol/openid-connect/token`; + } + }; + static Transactions = class { + public static get Types() { + return `${Reeve.BASE_URL}/transaction-types` + } + public static get Extraction() { + return `${Reeve.BASE_URL}/extraction` + } + public static get Validation() { + return `${Reeve.Transactions.Extraction}/validation` + } + } + static Organization = class { + public static get Base() { + return `${Reeve.BASE_URL}/organisations` + } + public static get EventCodes() { + return `${Reeve.Organization.Base}/:orgId/event-codes` + } + public static get ChartOfAccounts() { + return `${Reeve.Organization.Base}/:orgId/chart-of-accounts` + } + } + static Batches = class { + public static get Batches() { + return `${Reeve.BASE_URL}/batches` + } + public static get BatchById() { + return `${Reeve.Batches.Batches}/:batchId` + } + } +} \ No newline at end of file diff --git a/playwright/api/api-helpers/http-status-codes.ts b/playwright/api/api-helpers/http-status-codes.ts new file mode 100644 index 00000000..92cab7ee --- /dev/null +++ b/playwright/api/api-helpers/http-status-codes.ts @@ -0,0 +1,6 @@ +export enum HttpStatusCodes { + success = 200, + RequestAccepted = 202, + BadRequest = 400, + Unauthorized= 401 +} \ No newline at end of file diff --git a/playwright/api/base.api.ts b/playwright/api/base.api.ts new file mode 100644 index 00000000..f03a328a --- /dev/null +++ b/playwright/api/base.api.ts @@ -0,0 +1,88 @@ +import {APIRequestContext, APIResponse} from "@playwright/test"; + +import {log} from "../utils/logger"; + +const returnLoggedResponse = async ( + response: APIResponse, + endpoint: string, + payload?: object, + isBodyNotSecret = true +) => { + log.info(`Request URL: ${endpoint}`); + if (typeof payload !== "undefined" && isBodyNotSecret) { + log.info(`Request params/body:\n${JSON.stringify(payload, null, 2)}`); + } + log.info(`Response status: ${response.status()}`); + if (response.headers()["content-type"]?.includes("application/json") && isBodyNotSecret) { + log.info(`Response body:\n${JSON.stringify(await response.json(), null, 2)}`); + } + return response; +}; + +export const postForm = async ( + request: APIRequestContext, + endpoint: string, + form?: { [key: string]: string }, + headers?: { [key: string]: string }, + isBodyNotSecret = true +) => + returnLoggedResponse( + await request.post(endpoint, { + form, + headers + }), + endpoint, + form, + isBodyNotSecret + ); +export const getData = async ( + request: APIRequestContext, + endpoint: string, + params?: { [key: string]: string | number | boolean }, + headers?: { [key: string]: string }, + isBodyNotSecret = true +) => + returnLoggedResponse( + await request.get(endpoint, { + headers, + params + }), + endpoint, + params, + isBodyNotSecret + ); +export const postFormData = async ( + request: APIRequestContext, + endpoint: string, + multipart?: {[key: string]: any}, + headers?: {[key: string]: string}, + isBodyNotSecret = true +) => { + return returnLoggedResponse( + await request.post(endpoint, { + headers, + multipart + }), + endpoint, + multipart, + isBodyNotSecret + ); +} +export const postData = async ( + request: APIRequestContext, + endpoint: string, + data?: {[key: string]: any}, + headers?: {[key: string]: string}, + params?: { [key: string]: string | number | boolean }, + isBodyNotSecret = true +) => { + return returnLoggedResponse( + await request.post(endpoint,{ + headers, + data + }), + endpoint, + data, + isBodyNotSecret + ) +} \ No newline at end of file diff --git a/playwright/api/dtos/batchDto.ts b/playwright/api/dtos/batchDto.ts new file mode 100644 index 00000000..6c381124 --- /dev/null +++ b/playwright/api/dtos/batchDto.ts @@ -0,0 +1,98 @@ + +export interface BatchStatistics { + batchId: string; + invalid: number; + pending: number; + approve: number; + publish: number; + published: number; + total: number; +} + +export interface FilteringParameters { + transactionTypes: string[]; + from: string; + to: string; + accountingPeriodFrom: string; + accountingPeriodTo: string; + transactionNumbers: string[]; +} + +export interface TransactionItem { + id: string; + accountDebitCode: string; + accountDebitName: string; + accountDebitRefCode: string; + accountCreditCode: string; + accountCreditName: string; + accountCreditRefCode: string; + amountFcy: number; + amountLcy: number; + fxRate: number; + costCenterCustomerCode: string; + costCenterName: string; + parentCostCenterCustomerCode: string; + parentCostCenterName: string; + projectCustomerCode: string; + projectName: string; + parentProjectCustomerCode: string; + parentProjectName: string; + accountEventCode: string; + accountEventName: string; + documentNum: string; + documentCurrencyCustomerCode: string; + vatCustomerCode: string; + vatRate: number; + counterpartyCustomerCode: string; + counterpartyType: string; + counterpartyName: string; +} + +export interface Violation { + severity: string; + source: string; + transactionItemId: string; + code: string; + bag: { + customerCode: string; + transactionNumber: string; + }; +} + +export interface Transaction { + id: string; + internalTransactionNumber: string; + entryDate: string; + transactionType: string; + dataSource: string; + status: string; + statistic: string; + validationStatus: string; + ledgerDispatchStatus: string; + transactionApproved: boolean; + ledgerDispatchApproved: boolean; + amountTotalLcy: number; + itemRejection: boolean; + reconciliationSource: string; + reconciliationSink: string; + reconciliationFinalStatus: string; + reconciliationRejectionCode: string[]; + itemCount: number; + items: TransactionItem[]; + violations: Violation[]; +} + +export interface BatchResponse { + id: string; + createdAt: string; + updatedAt: string; + createdBy: string; + updateBy: string; + organisationId: string; + status: string; + batchStatistics: BatchStatistics; + filteringParameters: FilteringParameters; + transactions: Transaction[]; + details: Record; + totalTransactionsCount: number; +} \ No newline at end of file diff --git a/playwright/api/dtos/batchsDto.ts b/playwright/api/dtos/batchsDto.ts new file mode 100644 index 00000000..2e1fd048 --- /dev/null +++ b/playwright/api/dtos/batchsDto.ts @@ -0,0 +1,34 @@ +interface BatchStatistics { + batchId: string; + invalid: number; + pending: number; + approve: number; + publish: number; + published: number; + total: number; +} + +interface FilteringParameters { + transactionTypes: string[]; + from: string; + to: string; + accountingPeriodFrom: string; + accountingPeriodTo: string; + transactionNumbers: string[] | number[]; +} +export interface Batch { + id: string; + createdAt: string; + updatedAt: string; + createdBy: string; + updateBy: string; + organisationId: string; + status: string; + batchStatistics: BatchStatistics; + filteringParameters: FilteringParameters; +} + +export interface BatchData { + total: number; + batchs: Batch[]; +} \ No newline at end of file diff --git a/playwright/api/dtos/chartOfAccountsDto.ts b/playwright/api/dtos/chartOfAccountsDto.ts new file mode 100644 index 00000000..122315b0 --- /dev/null +++ b/playwright/api/dtos/chartOfAccountsDto.ts @@ -0,0 +1,25 @@ +export interface ChartOfAccountsDto { + customerCode: string; + eventRefCode: string; + name: string; + subType: number; + type: number; + active: boolean; + error: any; +} + +export interface AccountRefCodePair { + accountCode: string; + referenceCode: string; + accountName: string; +} + +export interface AccountCodeAndNamePair { + accountCode: string; + accountName: string; +} + +export interface DebitAndCreditAccounts { + debitAccounts: AccountCodeAndNamePair[]; + creditAccounts: AccountCodeAndNamePair[]; +} \ No newline at end of file diff --git a/playwright/api/dtos/eventCodesDto.ts b/playwright/api/dtos/eventCodesDto.ts new file mode 100644 index 00000000..55153647 --- /dev/null +++ b/playwright/api/dtos/eventCodesDto.ts @@ -0,0 +1,32 @@ +export interface EventCodesDto { + organisationId: string; + debitReferenceCode: string; + creditReferenceCode: string; + customerCode: string; + description: string; + active: boolean; + error?: ErrorDetails; +} + +export interface ErrorDetails { + instance: string; + type: string; + parameters?: { + additionalProp1?: any; + additionalProp2?: any; + additionalProp3?: any; + }; + title: string; + status: StatusInfo; + detail: string; +} + +export interface StatusInfo { + reasonPhrase: string; + statusCode: number; +} + +export interface ReferenceCodePair { + debitReferenceCode: string; + creditReferenceCode: string; +} \ No newline at end of file diff --git a/playwright/api/dtos/transactionItemCsvDto.ts b/playwright/api/dtos/transactionItemCsvDto.ts new file mode 100644 index 00000000..594bb012 --- /dev/null +++ b/playwright/api/dtos/transactionItemCsvDto.ts @@ -0,0 +1,22 @@ +export interface TransactionItemCsvDto { + TxNumber?: string; + TxDate?: string; + TxType?: string; + FxRate?: string; + AmountLcyDebit?: string; + AmountLcyCredit?: string; + AmountFcyDebit?: string; + AmountFcyCredit?: string; + DebitCode?: string; + DebitName?: string; + CreditCode?: string; + CreditName?: string; + ProjectCode?: string; + DocumentName?: string; + TxCurrency?: string; + VatRate?: string; + VatCode?: string; + TxCostCenter?: string; + CounterParty?: string; + CounterpartyName?: string; +} \ No newline at end of file diff --git a/playwright/api/dtos/transactionTypesDto.ts b/playwright/api/dtos/transactionTypesDto.ts new file mode 100644 index 00000000..404115a4 --- /dev/null +++ b/playwright/api/dtos/transactionTypesDto.ts @@ -0,0 +1,4 @@ +export interface TransactionTypeDto { + id: string; + title: string; +} \ No newline at end of file diff --git a/playwright/api/reeve-api/reeve.api.ts b/playwright/api/reeve-api/reeve.api.ts new file mode 100644 index 00000000..781fa68a --- /dev/null +++ b/playwright/api/reeve-api/reeve.api.ts @@ -0,0 +1,169 @@ +import {APIRequestContext} from "@playwright/test"; + +import * as BaseApi from "../base.api"; +import * as Endpoints from "../api-helpers/enpoints"; +import {getDateInThePast} from "../../utils/dateGenerator"; +import * as fs from "fs"; +import * as path from "node:path"; + +export function reeveApi(request: APIRequestContext) { + let logApiResponse = process.env.API_LOG_REQUEST == "true" + const loginReeve = async (userName: string, password: string) => { + return BaseApi.postForm( + request, + Endpoints.Reeve.SignIn.Base, + { + grant_type: "password", + client_id: "webclient", + username: userName, + password: password + }, + { + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/x-www-form-urlencoded" + }, + logApiResponse + ); + }; + + const transactionTypes = async (authToken: string) => { + return BaseApi.getData( + request, + Endpoints.Reeve.Transactions.Types, + {}, + { + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/x-www-form-urlencoded", + Authorization: authToken + }, + logApiResponse + ) + } + + const eventCodes = async (organizationId: string, authToken: string) => { + return BaseApi.getData( + request, + Endpoints.Reeve.Organization.EventCodes.replace(":orgId", organizationId), + {}, + { + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/x-www-form-urlencoded", + Authorization: authToken + }, + logApiResponse + ) + } + + const chartOfAccounts = async (organizationId: string, authToken: string) => { + return BaseApi.getData( + request, + Endpoints.Reeve.Organization.ChartOfAccounts.replace(":orgId", organizationId), + {}, + { + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/x-www-form-urlencoded", + Authorization: authToken + }, + logApiResponse + ) + } + + const validateTransactionCsvFile = async (organizationId: string, authToken: string, transactionFilePath: string) => { + return BaseApi.postFormData( + request, + Endpoints.Reeve.Transactions.Validation, + { + organisationId: organizationId, + extractorType: 'CSV', + dateFrom: getDateInThePast(6, false), + dateTo: getDateInThePast(2, false), + file: { + name: path.basename(transactionFilePath), + mimeType: 'text/csv', + buffer: fs.readFileSync(transactionFilePath), + } + }, + { + "Accept-Encoding": "gzip, deflate, br", + 'Authorization': authToken + }, + logApiResponse + ); + } + + const importTransactionCsvFile = async (organizationId: string, authToken: string, transactionFilePath: string) => { + return BaseApi.postFormData( + request, + Endpoints.Reeve.Transactions.Extraction, + { + organisationId: organizationId, + extractorType: 'CSV', + dateFrom: getDateInThePast(6, false), + dateTo: getDateInThePast(2, false), + file: { + name: path.basename(transactionFilePath), + mimeType: 'text/csv', + buffer: fs.readFileSync(transactionFilePath), + } + }, + { + "Accept-Encoding": "gzip, deflate, br", + 'Authorization': authToken + }, + logApiResponse + ); + } + const batchesByStatus = async (organizationId: string, authToken: string, status: string) => { + return BaseApi.postData( + request, + Endpoints.Reeve.Batches.Batches, + { + organisationId: organizationId, + batchStatistics: [status] + }, + { + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/json", + Authorization: authToken + }, + { + page: 0, + size: 100 + }, + logApiResponse + ) + } + const batchById = async (authToken: string, batchId: string) => { + return BaseApi.postData( + request, + Endpoints.Reeve.Batches.BatchById.replace(":batchId",batchId), + {}, + { + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/json", + Authorization: authToken + }, + { + page: 0, + size: 100 + }, + logApiResponse + ) + } + return { + loginReeve, + transactionTypes, + eventCodes, + chartOfAccounts, + validateTransactionCsvFile, + importTransactionCsvFile, + batchesByStatus, + batchById + }; +} \ No newline at end of file diff --git a/playwright/api/reeve-api/reeve.service.ts b/playwright/api/reeve-api/reeve.service.ts new file mode 100644 index 00000000..53abe89f --- /dev/null +++ b/playwright/api/reeve-api/reeve.service.ts @@ -0,0 +1,90 @@ +import {APIRequestContext, APIResponse, expect} from "@playwright/test"; +import {reeveApi} from "./reeve.api"; +import {Batch, BatchData} from "../dtos/batchsDto"; +import {BatchesStatusCodes} from "../api-helpers/batches-status-codes"; +import {BatchResponse} from "../dtos/batchDto"; +import {log} from "../../utils/logger"; + +let managerUser = process.env.MANAGER_USER as string; +let managerPassword = process.env.MANAGER_PASSWORD as string; +let organizationId = process.env.ORGANIZATION_ID as string; +export async function reeveService(request: APIRequestContext) { + const loginToReeve = async (userName: string, password: string) => { + return await reeveApi(request).loginReeve(userName, password); + }; + + const loginManager = async () => { + return await reeveApi(request).loginReeve(managerUser, managerPassword); + } + + const getTransactionTypes = async (authToken: string) => { + return await reeveApi(request).transactionTypes(authToken); + } + + const getEventCodes = async (authToken: string) => { + return await reeveApi(request).eventCodes(organizationId, authToken); + } + + const getChartOfAccounts = async (authToken: string) => { + return await reeveApi(request).chartOfAccounts(organizationId, authToken); + } + + const validateTransactionCsvFile = async (authToken: string, transactionFile: string) => { + return await reeveApi(request).validateTransactionCsvFile(organizationId, authToken, transactionFile); + } + + const importTransactionCsvFile = async (authToken: string, transactionFile: string) => { + return await reeveApi(request).importTransactionCsvFile(organizationId,authToken, transactionFile) + } + + const getBatchesByStatus = async (authToken: string, status: string) => { + return await reeveApi(request).batchesByStatus(organizationId, authToken, status); + } + + const getNewBatch = async (authToken: string, status: string, txNumber: string) => { + let batchesResponse: APIResponse; + let batchesAfterImport: BatchData; + let batchId: BatchResponse; + await expect.poll(async () => { + batchesResponse = await (await reeveService(request)).getBatchesByStatus(authToken, + status); + batchesAfterImport = await batchesResponse.json() + let batchesIdAfterImport = batchesAfterImport.batchs.map(batch => batch.id); + batchId = await findBatchWithTxNumber(batchesIdAfterImport,txNumber,authToken); + return batchId + },{ + message: "The new Batch was not created: ", + intervals: [1_000, 2_000, 10_000], + timeout: 280_000 + }).not.toBeNull(); + return batchId + } + + const getBatchById = async (authToken: string, batchId: string) => { + return await reeveApi(request).batchById(authToken, batchId) + } + + const findBatchWithTxNumber = async (batchesIdAfterImport: string[], txNumber: string, authToken: string) => { + for (const batchId of batchesIdAfterImport) { + const batchDetailsResponse: BatchResponse = await (await getBatchById(authToken, batchId)).json(); + if (batchDetailsResponse.transactions[0].internalTransactionNumber == txNumber){ + return batchDetailsResponse + } + } + return null; + } + + return { + loginToReeve, + loginManager, + getTransactionTypes, + getEventCodes, + getChartOfAccounts, + validateTransactionCsvFile, + importTransactionCsvFile, + getBatchesByStatus, + getNewBatch, + getBatchById + }; + +} \ No newline at end of file diff --git a/playwright/helpers/transaction-pending-status.ts b/playwright/helpers/transaction-pending-status.ts new file mode 100644 index 00000000..94e148be --- /dev/null +++ b/playwright/helpers/transaction-pending-status.ts @@ -0,0 +1,5 @@ +export enum TransactionPendingStatus { + VAT_DATA_NOT_FOUND = "VAT_DATA_NOT_FOUND", + COST_CENTER_DATA_NOT_FOUND = "COST_CENTER_DATA_NOT_FOUND", + CHART_OF_ACCOUNT_NOT_FOUND = "CHART_OF_ACCOUNT_NOT_FOUND" +} \ No newline at end of file diff --git a/playwright/helpers/transactionCSVProperties.ts b/playwright/helpers/transactionCSVProperties.ts new file mode 100644 index 00000000..5220e3b5 --- /dev/null +++ b/playwright/helpers/transactionCSVProperties.ts @@ -0,0 +1,24 @@ + + +export enum TxCSVHeader { + TxNumber = "Transaction Number", + TxDate = "Transaction Date", + TxType = "Transaction Type", + FxRate = "Fx Rate", + AmountLcyDebit = "AmountLCY Debit", + AmountLcyCredit = "AmountLCY Credit", + AmountFcyDebit = "AmountFCY Debit", + AmountFcyCredit = "AmountFCY Credit", + DebitCode = "Debit Code", + DebitName = "Debit Name", + CreditCode = "Credit Code", + CreditName = "Credit Name", + ProjectCode = "Project Code", + DocumentName = "Document Name", + TxCurrency = "Currency", + VatRate = "VAT Rate", + VatCode = "VAT Code", + TxCostCenter = "CostCenterCode", + CounterParty = "Counterparty Code", + CounterpartyName = "Counterparty Name" +} \ No newline at end of file diff --git a/playwright/helpers/transactionsBuilder.ts b/playwright/helpers/transactionsBuilder.ts new file mode 100644 index 00000000..0468e450 --- /dev/null +++ b/playwright/helpers/transactionsBuilder.ts @@ -0,0 +1,228 @@ +import {saveCSV} from "../utils/csvFileGenerator"; +import * as fs from "fs"; +import {log} from "../utils/logger"; +import {APIRequestContext, expect} from "@playwright/test"; +import {reeveService} from "../api/reeve-api/reeve.service"; +import {HttpStatusCodes} from "../api/api-helpers/http-status-codes"; +import {TransactionTypeDto} from "../api/dtos/transactionTypesDto"; +import {getDateInThePast} from "../utils/dateGenerator"; +import {TransactionItemCsvDto} from "../api/dtos/transactionItemCsvDto"; +import {EventCodesDto, ReferenceCodePair} from "../api/dtos/eventCodesDto"; +import { + AccountCodeAndNamePair, + AccountRefCodePair, + ChartOfAccountsDto, + DebitAndCreditAccounts +} from "../api/dtos/chartOfAccountsDto"; +import {TransactionPendingStatus} from "./transaction-pending-status"; + +export async function transactionsBuilder(request: APIRequestContext, authToken: string) { + const createCSVTransactionReadyToApprove = async (transactionDataToImport: TransactionItemCsvDto[]) => { + const columns = await getTransactionCSVHeaders(); + const rows = await createValidTransactionData(transactionDataToImport) + const fileName = "Approve-" + Math.random().toString(36).substring(2, 2 + 8) + ".csv"; + return await saveCSV(columns, rows, fileName); + } + const createCSVTransactionPending = async (transactionDataToImport: TransactionItemCsvDto[], pendingReason: string) => { + const columns = await getTransactionCSVHeaders(); + const rows = await createPendingTransactionData(transactionDataToImport, pendingReason); + const fileName = "Pending-" + Math.random().toString(36).substring(2, 2 + 8) + ".csv"; + return await saveCSV(columns, rows, fileName) + } + const getTransactionCSVHeaders = async () => { + try { + const headers = await fs.promises.readFile('../playwright/utils/transactionCSVHeaders.txt', 'utf-8') + return headers + .split(',') + .map(header => header.trim()) + } catch (error) { + log.error("Error trying to read file: ", error); + } + } + + /** + * Create a transaction with just two transactions items, + * txNumber Random short hash + * documentName Random short hash + * txType organization transaction type requested through API + * debitTxItem accounts are requested through API in base of organization event codes + * creditTxItem accounts are requested through API in base of organization event codes + */ + const createValidTransactionData = async (transactionDataToImport: TransactionItemCsvDto[]) => { + const transactionCommonData = await getTransactionCommonData() + const amountForTxItem = (Math.floor(Math.random() * 100000) + 1).toString(); + const eventCodes = await getEventCodes(); + const debitAndCreditAccounts = await getDebitAndCreditAccounts(eventCodes); + const debitTxItem = await createTransactionItem(transactionCommonData, amountForTxItem, + true, debitAndCreditAccounts); + const creditTxItem = await createTransactionItem(transactionCommonData, amountForTxItem, + false, debitAndCreditAccounts); + transactionDataToImport.push(debitTxItem); + transactionDataToImport.push(creditTxItem); + const rows: string[][] = []; + rows.push(Object.values(debitTxItem)); + rows.push(Object.values(creditTxItem)) + return rows + } + const createPendingTransactionData = async (transactionDataToImport: TransactionItemCsvDto[], pendingReason: string) => { + const transactionCommonData = await getTransactionCommonData() + const amountForTxItem = (Math.floor(Math.random() * 100000) + 1).toString(); + const eventCodes = await getEventCodes(); + const debitAndCreditAccounts = await getDebitAndCreditAccounts(eventCodes); + const debitTxItem = await createTransactionItem(transactionCommonData, amountForTxItem, + true, debitAndCreditAccounts) + await setPendingReason(debitTxItem, pendingReason); + const creditTxItem = await createTransactionItem(transactionCommonData, amountForTxItem, + false, debitAndCreditAccounts); + transactionDataToImport.push(debitTxItem); + transactionDataToImport.push(creditTxItem); + const rows: string[][] = []; + rows.push(Object.values(debitTxItem)); + rows.push(Object.values(creditTxItem)) + return rows + } + const getTransactionCommonData = async () => { + const txNumber = "TEST-" + Math.random().toString(36).substring(2, 2 + 8); + const txDate = getDateInThePast(2, true); + const txType = await getTransactionType(); + const documentName = "TEST-" + Math.random().toString(36).substring(2, 2 + 8); + const transactionItemCommonData: TransactionItemCsvDto = { + TxNumber: txNumber, + TxDate: txDate, + TxType: txType, + DocumentName: documentName + } + return transactionItemCommonData + } + const setPendingReason = async (transactionItem: TransactionItemCsvDto, pendingReason: string) => { + if(pendingReason == TransactionPendingStatus.COST_CENTER_DATA_NOT_FOUND){ + transactionItem.TxCostCenter = Math.random().toString(36).substring(2, 2 + 8); + } + if(pendingReason == TransactionPendingStatus.VAT_DATA_NOT_FOUND){ + transactionItem.VatCode = Math.random().toString(36).substring(2, 2 + 8); + } + if(pendingReason == TransactionPendingStatus.CHART_OF_ACCOUNT_NOT_FOUND){ + transactionItem.DebitCode = Math.random().toString(36).substring(2, 2 + 8); + } + } + + const getTransactionType = async () => { + const transactionTypeResponse = await (await reeveService(request)) + .getTransactionTypes(authToken); + expect(transactionTypeResponse.status()).toEqual(HttpStatusCodes.success); + const transactionTypes: TransactionTypeDto[] = await (transactionTypeResponse.json()); + const randomTxType = Math.floor(Math.random() * (transactionTypes.length - 1)); + return (transactionTypes[randomTxType].id) + } + + const getEventCodes = async () => { + const eventCodesResponse = await (await reeveService(request)).getEventCodes(authToken); + expect(eventCodesResponse.status()).toEqual(HttpStatusCodes.success); + const eventCodes: EventCodesDto[] = await (eventCodesResponse.json()); + const referenceCodes: ReferenceCodePair[] = eventCodes.map(eventCode => ({ + debitReferenceCode: eventCode.debitReferenceCode, + creditReferenceCode: eventCode.creditReferenceCode + })); + return referenceCodes; + } + + /** + * Get two lists of accounts that has an event code + * for the combination of debit and credit accounts + * @param eventCodes array of organization's event codes + * + */ + const getDebitAndCreditAccounts = async (eventCodes: ReferenceCodePair[]) => { + const chartOfAccounts: AccountRefCodePair[] = await getChartOfAccounts(); + let index = 0; + let accountsMatch: boolean = false; + let debitAccounts: AccountCodeAndNamePair[] | null; + let creditAccounts: AccountCodeAndNamePair[] | null; + while (accountsMatch == false) { + if (eventCodes[index].debitReferenceCode != eventCodes[index].creditReferenceCode) { + debitAccounts = chartOfAccounts.filter(chartOfAccount => + chartOfAccount.referenceCode === eventCodes[index].debitReferenceCode) + .map(chartOfAccount => ({ + accountCode: chartOfAccount.accountCode, + accountName: chartOfAccount.accountName + })); + if (debitAccounts.length >= 1) { + creditAccounts = chartOfAccounts.filter(chartOfAccount => + chartOfAccount.referenceCode === eventCodes[index].creditReferenceCode + && chartOfAccount.accountCode !== debitAccounts[0].accountCode) + .map(chartOfAccount => ({ + accountCode: chartOfAccount.accountCode, + accountName: chartOfAccount.accountName + })); + } + if (creditAccounts != null) { + accountsMatch = true; + } + } + index++; + } + const debitAndCreditAccounts: DebitAndCreditAccounts = { + debitAccounts: debitAccounts, + creditAccounts: creditAccounts + } + return debitAndCreditAccounts + } + + const getChartOfAccounts = async () => { + const chartOfAccountsResponse = await (await reeveService(request)).getChartOfAccounts(authToken); + expect(chartOfAccountsResponse.status()).toEqual(HttpStatusCodes.success); + const chartOfAccounts: AccountRefCodePair[] = (await (chartOfAccountsResponse).json()) + .map(chartOfAccount => ({ + accountCode: chartOfAccount.customerCode, + referenceCode: chartOfAccount.eventRefCode, + accountName: chartOfAccount.name + })) + return chartOfAccounts + } + + const createTransactionItem = async (transactionItemCommonData: TransactionItemCsvDto, amount: string, + isDebit: boolean, debitAndCreditAccounts: DebitAndCreditAccounts) => { + let randomIndexDebit = Math.floor(Math.random() * debitAndCreditAccounts.debitAccounts.length) + let randomIndexCredit = Math.floor(Math.random() * debitAndCreditAccounts.creditAccounts.length) + const transactionItem: TransactionItemCsvDto = { + TxNumber: transactionItemCommonData.TxNumber, + TxDate: transactionItemCommonData.TxDate, + TxType: transactionItemCommonData.TxType, + FxRate: "1", + AmountLcyDebit: "", + AmountLcyCredit: "", + AmountFcyDebit: "", + AmountFcyCredit: "", + DebitCode: "", + DebitName: "", + CreditCode: "", + CreditName: "", + ProjectCode: "", + DocumentName: transactionItemCommonData.DocumentName, + TxCurrency: "CHF", + VatRate: "", + VatCode: "", + TxCostCenter: "", + CounterParty: "", + CounterpartyName: "", + } + if (isDebit) { + transactionItem.AmountLcyDebit = amount; + transactionItem.AmountFcyDebit = amount; + } else { + transactionItem.AmountLcyCredit = amount; + transactionItem.AmountFcyCredit = amount; + } + transactionItem.DebitCode = debitAndCreditAccounts.debitAccounts[randomIndexDebit].accountCode; + transactionItem.DebitName = debitAndCreditAccounts.debitAccounts[randomIndexDebit].accountName; + transactionItem.CreditCode = debitAndCreditAccounts.creditAccounts[randomIndexCredit].accountCode; + transactionItem.CreditName = debitAndCreditAccounts.creditAccounts[randomIndexCredit].accountName; + return transactionItem + } + + return { + createReadyToApproveTransaction: createCSVTransactionReadyToApprove, + createCSVTransactionPending + } + +} \ No newline at end of file diff --git a/playwright/package-lock.json b/playwright/package-lock.json new file mode 100644 index 00000000..aaf01ffa --- /dev/null +++ b/playwright/package-lock.json @@ -0,0 +1,890 @@ +{ + "name": "playwright", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "playwright", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@faker-js/faker": "^9.9.0", + "dotenv": "^17.2.1", + "log4js": "^6.9.1" + }, + "devDependencies": { + "@playwright/test": "^1.56.1", + "@types/node": "^24.3.0", + "playwright-bdd": "^8.4.1" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cucumber/cucumber-expressions": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-18.0.1.tgz", + "integrity": "sha512-NSid6bI+7UlgMywl5octojY5NXnxR9uq+JisjOrO52VbFsQM6gTWuQFE8syI10KnIBEdPzuEUSVEeZ0VFzRnZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-match-indices": "1.0.2" + } + }, + "node_modules/@cucumber/gherkin": { + "version": "32.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-32.2.0.tgz", + "integrity": "sha512-X8xuVhSIqlUjxSRifRJ7t0TycVWyX58fygJH3wDNmHINLg9sYEkvQT0SO2G5YlRZnYc11TIFr4YPenscvdlBIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=19.1.4 <28" + } + }, + "node_modules/@cucumber/gherkin-utils": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-9.2.0.tgz", + "integrity": "sha512-3nmRbG1bUAZP3fAaUBNmqWO0z0OSkykZZotfLjyhc8KWwDSOrOmMJlBTd474lpA8EWh4JFLAX3iXgynBqBvKzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/gherkin": "^31.0.0", + "@cucumber/messages": "^27.0.0", + "@teppeis/multimaps": "3.0.0", + "commander": "13.1.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "gherkin-utils": "bin/gherkin-utils" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-31.0.0.tgz", + "integrity": "sha512-wlZfdPif7JpBWJdqvHk1Mkr21L5vl4EfxVUOS4JinWGf3FLRV6IKUekBv5bb5VX79fkDcfDvESzcQ8WQc07Wgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=19.1.4 <=26" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-26.0.1.tgz", + "integrity": "sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "10.0.0", + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2", + "uuid": "10.0.0" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@cucumber/html-formatter": { + "version": "21.14.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-21.14.0.tgz", + "integrity": "sha512-vQqbmQZc0QiN4c+cMCffCItpODJlOlYtPG7pH6We096dBOa7u0ttDMjT6KrMAnQlcln54rHL46r408IFpuznAw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@cucumber/messages": ">=18" + } + }, + "node_modules/@cucumber/junit-xml-formatter": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.7.1.tgz", + "integrity": "sha512-AzhX+xFE/3zfoYeqkT7DNq68wAQfBcx4Dk9qS/ocXM2v5tBv6eFQ+w8zaSfsktCjYzu4oYRH/jh4USD1CYHfaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/query": "^13.0.2", + "@teppeis/multimaps": "^3.0.0", + "luxon": "^3.5.0", + "xmlbuilder": "^15.1.1" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/messages": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-27.2.0.tgz", + "integrity": "sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/uuid": "10.0.0", + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2", + "uuid": "11.0.5" + } + }, + "node_modules/@cucumber/query": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-13.6.0.tgz", + "integrity": "sha512-tiDneuD5MoWsJ9VKPBmQok31mSX9Ybl+U4wqDoXeZgsXHDURqzM3rnpWVV3bC34y9W6vuFxrlwF/m7HdOxwqRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@teppeis/multimaps": "3.0.0", + "lodash.sortby": "^4.7.0" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/tag-expressions": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-6.2.0.tgz", + "integrity": "sha512-KIF0eLcafHbWOuSDWFw0lMmgJOLdDRWjEL1kfXEWrqHmx2119HxVAr35WuEd9z542d3Yyg+XNqSr+81rIKqEdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@faker-js/faker": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz", + "integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@teppeis/multimaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-3.0.0.tgz", + "integrity": "sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "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/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.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==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-bdd": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/playwright-bdd/-/playwright-bdd-8.4.1.tgz", + "integrity": "sha512-2KM6yHKjpfCKVv0j8lhJkSLbhgfX2yTZLPM+Q9WnnBk/1oa3bmXaHoyokX5Sby2NU/culwC6pErdQAmVfxFnAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/cucumber-expressions": "18.0.1", + "@cucumber/gherkin": "^32.1.2", + "@cucumber/gherkin-utils": "^9.2.0", + "@cucumber/html-formatter": "^21.11.0", + "@cucumber/junit-xml-formatter": "^0.7.1", + "@cucumber/messages": "^27.2.0", + "@cucumber/tag-expressions": "^6.2.0", + "cli-table3": "0.6.5", + "commander": "^13.1.0", + "fast-glob": "^3.3.3", + "mime-types": "^3.0.1", + "xmlbuilder": "15.1.1" + }, + "bin": { + "bddgen": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/vitalets" + }, + "peerDependencies": { + "@playwright/test": ">=1.44" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regexp-match-indices": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", + "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "regexp-tree": "^0.1.11" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uuid": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + } + } +} diff --git a/playwright/package.json b/playwright/package.json new file mode 100644 index 00000000..d2c695f3 --- /dev/null +++ b/playwright/package.json @@ -0,0 +1,22 @@ +{ + "name": "playwright", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "npx bddgen && npx playwright test --project='api-tests'" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.56.1", + "@types/node": "^24.3.0", + "playwright-bdd": "^8.4.1" + }, + "dependencies": { + "@faker-js/faker": "^9.9.0", + "dotenv": "^17.2.1", + "log4js": "^6.9.1" + } +} diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts new file mode 100644 index 00000000..c93d5d1a --- /dev/null +++ b/playwright/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test'; +import {defineBddConfig} from "playwright-bdd"; + +import "dotenv/config"; + + +const testDir = defineBddConfig({ + features: './tests/e2e', + steps: './tests/steps' +}); +export default defineConfig({ + /* Indicates where the test steps definition are */ + testDir, + + + timeout: 120_000, + + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Configure projects for major browsers */ + projects: [ + { + name: "api-tests", + testDir, + } + ], +}); diff --git a/playwright/tests/e2e/Import-transactions-CSV.feature b/playwright/tests/e2e/Import-transactions-CSV.feature new file mode 100644 index 00000000..e720cf59 --- /dev/null +++ b/playwright/tests/e2e/Import-transactions-CSV.feature @@ -0,0 +1,30 @@ +Feature: Users can import transactions into Reeve with a CSV file, system validates the structure file + and import the transactions to be processed by the validation rules + + Scenario: Import ready to approve transaction + Given Manager user wants to import a transaction with a CSV file + And the manager creates the CSV file with all the required fields + And system get the validation request + When system get import request + Then the transaction data should be imported with ready to approve status + + Scenario: Import transaction in pending status by unknown cost center + Given Manager user wants to import a transaction with a CSV file + And the cost center data in the CSV file doesn't exist in the system + And system get the validation request + When system get import request + Then the system should create the transaction with pending status by "COST_CENTER_DATA_NOT_FOUND" + + Scenario: Import transaction in pending status by unknown VAT code + Given Manager user wants to import a transaction with a CSV file + And the vat code data in the CSV file doesn't exist in the system + And system get the validation request + When system get import request + Then the system should create the transaction with pending status by "VAT_DATA_NOT_FOUND" + + Scenario: Import transaction in pending status by unknown Chart of account code + Given Manager user wants to import a transaction with a CSV file + And the chart of account code data in the CSV file doesn't exist in the system + And system get the validation request + When system get import request + Then the system should create the transaction with pending status by "CHART_OF_ACCOUNT_NOT_FOUND" \ No newline at end of file diff --git a/playwright/tests/e2e/login.feature b/playwright/tests/e2e/login.feature new file mode 100644 index 00000000..87855363 --- /dev/null +++ b/playwright/tests/e2e/login.feature @@ -0,0 +1,11 @@ +Feature: Login and authentication process tests + + Scenario: Manager user can login with its credentials + Given Manager user wants to login into Reeve + When system get the login request + Then system should return success login response with authorization token + + Scenario: Manager user can not login with invalid credentials + Given Manager user wants to login into Reeve with wrong credentials + When system get the login request + Then system should reject access \ No newline at end of file diff --git a/playwright/tests/steps/importTransactionsCSV.steps.ts b/playwright/tests/steps/importTransactionsCSV.steps.ts new file mode 100644 index 00000000..cd279575 --- /dev/null +++ b/playwright/tests/steps/importTransactionsCSV.steps.ts @@ -0,0 +1,76 @@ +import {APIResponse, expect} from '@playwright/test'; +import {createBdd} from 'playwright-bdd' +import {reeveService} from "../../api/reeve-api/reeve.service"; +import {HttpStatusCodes} from "../../api/api-helpers/http-status-codes"; +import {transactionsBuilder} from "../../helpers/transactionsBuilder"; +import {BatchesStatusCodes} from "../../api/api-helpers/batches-status-codes"; +import {TransactionItemCsvDto} from "../../api/dtos/transactionItemCsvDto"; +import {BatchResponse} from "../../api/dtos/batchDto"; +import {transactionValidator} from "../../validators/transactionValidator"; +import {TransactionPendingStatus} from "../../helpers/transaction-pending-status"; +import {deleteFile} from "../../utils/csvFileGenerator"; + +const {Given, When, Then} = createBdd(); +let authToken: string +let transactionCSVFile: string; +let transactionDataToImport: TransactionItemCsvDto[] = []; +Given(/^Manager user wants to import a transaction with a CSV file$/, async ({request}) => { + const loginResponse = await (await reeveService(request)).loginManager() + expect(loginResponse.status()).toEqual(HttpStatusCodes.success) + authToken = (await loginResponse.json()).token_type + " " + (await loginResponse.json()).access_token; +}); +Given(/^the manager creates the CSV file with all the required fields$/, async ({request}) => { + transactionCSVFile = await (await transactionsBuilder(request, authToken)) + .createReadyToApproveTransaction(transactionDataToImport); +}); +Given(/^system get the validation request$/, async ({request}) => { + const validateResponse = await (await reeveService(request)).validateTransactionCsvFile(authToken, + transactionCSVFile); + expect(validateResponse.status()).toEqual(HttpStatusCodes.success); +}); +When(/^system get import request$/, async ({request}) => { + const importTxCsvResponse = await (await reeveService(request)).importTransactionCsvFile(authToken, + transactionCSVFile); + expect(importTxCsvResponse.status()).toEqual(HttpStatusCodes.RequestAccepted); + await deleteFile(transactionCSVFile) +}); +Then(/^the transaction data should be imported with ready to approve status$/, async ({request}) => { + const newBatchAfterImport = await (await reeveService(request)).getNewBatch(authToken, + BatchesStatusCodes.APPROVE, transactionDataToImport[0].TxNumber); + const batchDetailsResponse = await (await reeveService(request)).getBatchById(authToken, + newBatchAfterImport.id); + expect(batchDetailsResponse.status()).toEqual(HttpStatusCodes.success); + let importedBatchDetails: BatchResponse = await batchDetailsResponse.json() + await (await transactionValidator()).validateImportedTxWithStatus(transactionDataToImport, importedBatchDetails, + BatchesStatusCodes.APPROVE); +}); +Given(/^the cost center data in the CSV file doesn't exist in the system$/, async ({request}) => { + transactionCSVFile = await (await transactionsBuilder(request, authToken)) + .createCSVTransactionPending(transactionDataToImport, TransactionPendingStatus.COST_CENTER_DATA_NOT_FOUND); +}); +Then(/^the system should create the transaction with pending status by "([^"]*)"$/, async ({request}, reason) => { + const newBatchAfterImport = await (await reeveService(request)).getNewBatch(authToken, + BatchesStatusCodes.PENDING, transactionDataToImport[0].TxNumber); + await (await transactionValidator()).validateImportedTxWithStatus(transactionDataToImport, newBatchAfterImport, + BatchesStatusCodes.PENDING); + if(reason == TransactionPendingStatus.COST_CENTER_DATA_NOT_FOUND){ + await (await transactionValidator()).validatePendingCondition(newBatchAfterImport, + TransactionPendingStatus.COST_CENTER_DATA_NOT_FOUND) + } + if(reason == TransactionPendingStatus.VAT_DATA_NOT_FOUND){ + await (await transactionValidator()).validatePendingCondition(newBatchAfterImport, + TransactionPendingStatus.VAT_DATA_NOT_FOUND) + } + if(reason == TransactionPendingStatus.CHART_OF_ACCOUNT_NOT_FOUND){ + await (await transactionValidator()).validatePendingCondition(newBatchAfterImport, + TransactionPendingStatus.CHART_OF_ACCOUNT_NOT_FOUND) + } +}); +Given(/^the vat code data in the CSV file doesn't exist in the system$/, async ({request}) => { + transactionCSVFile = await (await transactionsBuilder(request, authToken)) + .createCSVTransactionPending(transactionDataToImport, TransactionPendingStatus.VAT_DATA_NOT_FOUND); +}); +Given(/^the chart of account code data in the CSV file doesn't exist in the system$/, async ({request}) => { + transactionCSVFile = await (await transactionsBuilder(request, authToken)) + .createCSVTransactionPending(transactionDataToImport, TransactionPendingStatus.CHART_OF_ACCOUNT_NOT_FOUND); +}); \ No newline at end of file diff --git a/playwright/tests/steps/login.steps.ts b/playwright/tests/steps/login.steps.ts new file mode 100644 index 00000000..286d4698 --- /dev/null +++ b/playwright/tests/steps/login.steps.ts @@ -0,0 +1,34 @@ +import {APIResponse, expect} from '@playwright/test'; +import {faker} from "@faker-js/faker"; +import {createBdd} from 'playwright-bdd'; +import {reeveApi} from "../../api/reeve-api/reeve.api"; +import {reeveService} from "../../api/reeve-api/reeve.service"; +import {log} from "../../utils/logger"; +import {HttpStatusCodes} from "../../api/api-helpers/http-status-codes"; + +const {Given, When, Then} = createBdd(); + +let userName: string; +let password: string; +let loginResponse: APIResponse; +Given(/^Manager user wants to login into Reeve$/, async ({page}) => { + userName = process.env.MANAGER_USER as string; + password = process.env.MANAGER_PASSWORD as string; +}); +When(/^system get the login request$/, async ({request}) => { + loginResponse = await (await reeveService(request)).loginToReeve(userName, password) +}); +Then(/^system should return success login response with authorization token$/, async ({page}) => { + expect(loginResponse.status()).toEqual(HttpStatusCodes.success) + const authToken = (await loginResponse.json()).access_token + const tokenType = (await loginResponse.json()).token_type + expect(authToken).toBeDefined() + expect(tokenType).toContain("Bearer") +}); +Given(/^Manager user wants to login into Reeve with wrong credentials$/, async () => { + userName = faker.internet.userAgent() + password = faker.string.sample() +}); +Then(/^system should reject access$/, async () => { + expect(loginResponse.status()).toEqual(HttpStatusCodes.Unauthorized) +}); \ No newline at end of file diff --git a/playwright/utils/csvFileGenerator.ts b/playwright/utils/csvFileGenerator.ts new file mode 100644 index 00000000..da0af26a --- /dev/null +++ b/playwright/utils/csvFileGenerator.ts @@ -0,0 +1,58 @@ +import * as fs from "fs"; +import * as path from "path"; +import {unlink} from "node:fs/promises"; + +/** + * Create CSV data + * @param columns Array of column names + * @param rows Array of rows (each row is an array of values in order) + * @param filename Output CSV filename + */ +export async function saveCSV( + columns: string[], + rows: any[][], + filename: string +): Promise { + const folderPath = "./resources"; + + if (columns.length === 0) { + throw new Error("You must provide at least one column."); + } + + // Escape values that contain quotes, commas or newlines + const escape = (value: any): string => { + if (value == null) return ""; + const valueAsString = String(value); + if (/[",\n]/.test(valueAsString)) { + return `"${valueAsString.replace(/"/g, '""')}"`; + } + return valueAsString; + }; + + // Build CSV content + const header = columns.join(","); + const data = rows.map(r => + columns.map((_, i) => escape(r[i] ?? "")).join(",") + ); + const csvContent = [header, ...data].join("\n"); + + // Ensure folder exists + fs.mkdirSync(folderPath, { recursive: true }); + + // Save file + const fullPath = path.join(folderPath, filename); + fs.writeFileSync(fullPath, csvContent, "utf-8"); + + //return file + return fullPath; +} + +export async function deleteFile(fullPath: string): Promise { + try { + await unlink(fullPath); + console.log(`โœ“ File successfully deleted`); + } catch (error) { + console.error(`โœ— Error trying to delete:`, error); + throw error; + } +} diff --git a/playwright/utils/dateGenerator.ts b/playwright/utils/dateGenerator.ts new file mode 100644 index 00000000..1ffd1a5f --- /dev/null +++ b/playwright/utils/dateGenerator.ts @@ -0,0 +1,13 @@ + +export function getDateInThePast(monthsInPast: number, usFormat: boolean){ + const date = new Date(); + date.setMonth(date.getMonth() - monthsInPast); + + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = date.getFullYear(); + if(usFormat==true){ + return `${day}/${month}/${year}`; + } + return `${year}-${month}-${day}` +} \ No newline at end of file diff --git a/playwright/utils/logger.ts b/playwright/utils/logger.ts new file mode 100644 index 00000000..756408ec --- /dev/null +++ b/playwright/utils/logger.ts @@ -0,0 +1,16 @@ +import { configure, getLogger } from "log4js"; + +configure({ + appenders: { + app: { type: "file", filename: "./.logs/test-run.log" }, + out: { type: "stdout" } + }, + categories: { + default: { + appenders: ["app", "out"], + level: "debug" + } + } +}); + +export const log = getLogger(); \ No newline at end of file diff --git a/playwright/utils/transactionCSVHeaders.txt b/playwright/utils/transactionCSVHeaders.txt new file mode 100644 index 00000000..b0c604e4 --- /dev/null +++ b/playwright/utils/transactionCSVHeaders.txt @@ -0,0 +1 @@ +Transaction Number,Transaction Date,Transaction Type,Fx Rate,AmountLCY Debit,AmountLCY Credit,AmountFCY Debit,AmountFCY Credit,Debit Code,Debit Name,Credit Code,Credit Name,Project Code,Document Name,Currency,VAT Rate,VAT Code,Cost Center Code,Counterparty Code,Counterparty Name \ No newline at end of file diff --git a/playwright/validators/transactionValidator.ts b/playwright/validators/transactionValidator.ts new file mode 100644 index 00000000..6c00e0d2 --- /dev/null +++ b/playwright/validators/transactionValidator.ts @@ -0,0 +1,68 @@ +import {TransactionItemCsvDto} from "../api/dtos/transactionItemCsvDto"; +import {BatchResponse, TransactionItem} from "../api/dtos/batchDto"; +import {expect} from "@playwright/test"; +import {log} from "../utils/logger"; +import {BatchesStatusCodes} from "../api/api-helpers/batches-status-codes"; + +export async function transactionValidator() { + const validateImportedTxWithStatus = async (transactionCsvData: TransactionItemCsvDto[], + importedBatchDetails: BatchResponse, transactionStatus: string) => { + const debitCsvItem: TransactionItemCsvDto = await extractCsvTxItem(transactionCsvData, true); + const creditCsvItem: TransactionItemCsvDto = await extractCsvTxItem(transactionCsvData, false); + const txItems = importedBatchDetails.transactions[0].items + const txDebitItem = txItems.find(tx => + tx.accountDebitCode == debitCsvItem.DebitCode) + const txCreditItem = txItems.find(tx => + tx.accountCreditCode == creditCsvItem.CreditCode) + if(transactionStatus == BatchesStatusCodes.APPROVE){ + expect(importedBatchDetails.batchStatistics.approve, "Imported batch should have transaction in ready to approve status ") + .toEqual(importedBatchDetails.totalTransactionsCount) + } + if(transactionStatus == BatchesStatusCodes.PENDING){ + expect(importedBatchDetails.batchStatistics.pending, "Imported batch should have transaction in pending status ") + .toEqual(importedBatchDetails.totalTransactionsCount) + } + expect(importedBatchDetails.transactions[0].items.length, "The sent transaction items are not the same imported in the system ") + .toEqual(transactionCsvData.length) + expect(importedBatchDetails.transactions[0].internalTransactionNumber, "The transaction number is not the same that was sent") + .toEqual(transactionCsvData[0].TxNumber) + expect(importedBatchDetails.transactions[0].transactionType,"The transaction type is not the same that was sent") + .toEqual(transactionCsvData[0].TxType) + await validateTxItem(txDebitItem, debitCsvItem, true); + await validateTxItem(txCreditItem, creditCsvItem, false); + } + const validateTxItem = async (txItem: TransactionItem, txCsvItem: TransactionItemCsvDto, isDebit: boolean) => { + expect(txItem.accountDebitCode, "The sent debit code is not the same imported in the system") + .toEqual(txCsvItem.DebitCode); + expect(txItem.accountCreditCode, "The sent credit code is not the same imported in the system") + .toEqual(txCsvItem.CreditCode); + expect(txItem.documentNum, "The sent document number is not the same imported in the system") + .toEqual(txCsvItem.DocumentName) + if(isDebit == true){ + expect(txItem.amountLcy, "The LCY amount in debit item is not the same imported in the system") + .toEqual(parseFloat(txCsvItem.AmountLcyDebit)) + }else { + expect(Math.abs(txItem.amountLcy), "The LCY amount in credit item is not the same imported in the system") + .toEqual(parseFloat(txCsvItem.AmountLcyCredit)) + } + } + const validatePendingCondition = async (importedBatchDetails: BatchResponse, expectedReason: string) => { + expect(importedBatchDetails.transactions[0].violations[0].code, "The expected pending reason is wrong") + .toEqual(expectedReason) + } + const extractCsvTxItem = async (transactionCsvData: TransactionItemCsvDto[], isDebit: boolean) => { + return transactionCsvData.find(tx => { + let amount: number + if(isDebit == true){ + amount = parseFloat(tx.AmountLcyDebit); + return !isNaN(amount) && amount > 0; + } + amount = parseFloat(tx.AmountLcyCredit); + return !isNaN(amount) && amount > 0 + }) + } + return { + validateImportedTxWithStatus, + validatePendingCondition + } +} \ No newline at end of file