diff --git a/README.md b/README.md index a45e707..1741fae 100644 --- a/README.md +++ b/README.md @@ -44,17 +44,21 @@ This project uses custom Docker images built from the following Dockerfiles: ### Create environment file -Create a `.env` file in the root directory of the project with your environment variables: +Before starting the services, you **must** create a `.env` file in the root directory of the project. This file is required for Docker Compose to provide all necessary environment variables to the containers. + +Copy the example file: ```bash cp .env.example .env ``` -Then edit the `.env` file to set your specific configuration values. +> **Note:** +> - The `.env.example` file contains all required variables for onboarding and local development. Edit the `.env` file to set your specific configuration values as needed. +> - If you do not create a `.env` file, `docker-compose` will fail to start some services due to missing environment variables. ### Starting the services -You can start the services in two ways, depending on your environment: +After creating and editing your `.env` file, you can start the services in two ways, depending on your environment: #### 1. Development diff --git a/docker-compose.yml b/docker-compose.yml index 95552b7..f423a9e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -159,6 +159,8 @@ services: - ./workers/main:/app/main - ./workers/common:/app/common - /app/main/node_modules + env_file: + - .env networks: - app-network develop: diff --git a/sonar-project.properties b/sonar-project.properties index dbeda9a..94582fa 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,4 +1,4 @@ sonar.projectKey=speedandfunction_automatization sonar.organization=speedandfunction sonar.javascript.lcov.reportPaths=workers/main/coverage/lcov.info -sonar.exclusions=**/src/__tests__/**,**/src/dist/** +sonar.exclusions=docker-compose.yml,**/*.test.ts,**/src/dist/** diff --git a/workers/common/utils.ts b/workers/common/utils.ts deleted file mode 100644 index a62be87..0000000 --- a/workers/common/utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { validationResult } from '../main/src/configs'; -import {logger} from "../main"; - -export const formatValidationIssues = (issues: { path: (string | number)[]; message: string }[]): string => - issues - .map(({ path, message }) => `Missing or invalid environment variable: ${path.join('.') || '(unknown variable)'} (${message})`) - .join('\n'); - -export function validateEnv() { - if (!validationResult.success) { - console.error(formatValidationIssues(validationResult.error.issues)); - process.exit(1); - } -} - -/** - * Logs a worker error in a consistent format. - * @param workerName - The name of the workflow - * @param error - The error object - */ -export function logWorkerError(workerName: string, error: unknown) { - logger.error( - `Error in ${workerName} workerName: ${error instanceof Error ? error.message : String(error)}`, - ); -} - -/** - * Logs a workflow error in a consistent format. - * @param workflowName - The name of the workflow - * @param error - The error object - */ -export function logWorkflowError(workflowName: string, error: unknown) { - logger.error( - `Error in ${workflowName} workflow: ${error instanceof Error ? error.message : String(error)}`, - ); -} \ No newline at end of file diff --git a/workers/main/package-lock.json b/workers/main/package-lock.json index 80767f6..34ae283 100644 --- a/workers/main/package-lock.json +++ b/workers/main/package-lock.json @@ -16,7 +16,7 @@ "zod": "3.25.17" }, "devDependencies": { - "@eslint/js": "9.27.0", + "@eslint/js": "8.57.1", "@temporalio/testing": "1.11.8", "@types/node": "22.15.21", "@vitest/coverage-v8": "3.1.3", @@ -32,7 +32,7 @@ "source-map-support": "^0.5.21", "ts-node": "10.9.1", "typescript": "5.8.3", - "typescript-eslint": "8.32.1", + "typescript-eslint": "8.33.0", "uuid": "11.1.0", "vite": "6.3.5", "vitest": "3.1.3" @@ -639,7 +639,6 @@ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -654,7 +653,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -665,7 +663,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -678,7 +675,6 @@ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -688,7 +684,6 @@ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -701,7 +696,6 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -725,7 +719,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -736,7 +729,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -745,16 +737,12 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, - "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@eslint/object-schema": { @@ -762,7 +750,6 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -772,7 +759,6 @@ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" @@ -815,7 +801,6 @@ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } @@ -825,7 +810,6 @@ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" @@ -839,7 +823,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -867,7 +850,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -1038,7 +1020,6 @@ "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" @@ -1052,7 +1033,6 @@ "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" } @@ -1062,7 +1042,6 @@ "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" @@ -1894,17 +1873,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1918,7 +1896,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -1928,22 +1906,20 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -1958,15 +1934,32 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1976,15 +1969,30 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2001,11 +2009,10 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2015,14 +2022,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2042,16 +2050,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2066,13 +2073,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2622,7 +2628,6 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2645,7 +2650,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2730,8 +2734,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", @@ -2919,7 +2922,6 @@ "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" }, @@ -3062,7 +3064,6 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -3742,7 +3743,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4020,7 +4020,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -4045,6 +4044,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4074,7 +4085,6 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", @@ -4185,7 +4195,6 @@ "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", @@ -4202,7 +4211,6 @@ "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" }, @@ -4214,8 +4222,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -4244,7 +4251,6 @@ "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" } @@ -4269,7 +4275,6 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, - "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -4282,7 +4287,6 @@ "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" }, @@ -4312,7 +4316,6 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, - "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -4325,8 +4328,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/for-each": { "version": "0.3.5", @@ -4552,7 +4554,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -4599,8 +4600,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/has-bigints": { "version": "1.1.0", @@ -4734,7 +4734,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } @@ -4744,7 +4743,6 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, - "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -5010,7 +5008,6 @@ "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" } @@ -5298,7 +5295,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -5310,8 +5306,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -5322,8 +5317,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -5350,7 +5344,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -5521,7 +5514,6 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } @@ -5531,7 +5523,6 @@ "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" @@ -5545,7 +5536,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5881,7 +5871,6 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -6086,7 +6075,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -6109,8 +6097,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/randombytes": { "version": "2.1.0", @@ -6207,7 +6194,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -6226,7 +6212,6 @@ "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" @@ -6291,7 +6276,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -6885,7 +6869,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -7097,7 +7080,6 @@ "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" }, @@ -7125,7 +7107,6 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18.12" }, @@ -7300,15 +7281,14 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7420,7 +7400,6 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } diff --git a/workers/main/package.json b/workers/main/package.json index 3da92b8..4fd6471 100644 --- a/workers/main/package.json +++ b/workers/main/package.json @@ -5,10 +5,11 @@ "scripts": { "test": "vitest run", "coverage": "vitest run --coverage", - "eslint": "eslint . --ext .ts" + "eslint": "eslint . --ext .ts", + "build": "tsc" }, "devDependencies": { - "@eslint/js": "9.27.0", + "@eslint/js": "8.57.1", "@types/node": "22.15.21", "@temporalio/testing": "1.11.8", "@vitest/coverage-v8": "3.1.3", @@ -24,7 +25,7 @@ "source-map-support": "^0.5.21", "ts-node": "10.9.1", "typescript": "5.8.3", - "typescript-eslint": "8.32.1", + "typescript-eslint": "8.33.0", "uuid": "11.1.0", "vite": "6.3.5", "vitest": "3.1.3" diff --git a/workers/main/src/__tests__/weeklyFinancialReports.test.ts b/workers/main/src/__tests__/weeklyFinancialReports.test.ts deleted file mode 100644 index 573accb..0000000 --- a/workers/main/src/__tests__/weeklyFinancialReports.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -import * as utils from '../../../common/utils'; -import { weeklyFinancialReportsWorkflow } from '../workflows'; - -describe('weeklyFinancialReportsWorkflow', () => { - it('should return the report string with default parameters', async () => { - const result = await weeklyFinancialReportsWorkflow(); - - expect(typeof result).toBe('string'); - expect(result.length).toBeGreaterThan(0); - }); - - it('should return the report string for a custom period', async () => { - const result = await weeklyFinancialReportsWorkflow({ - period: 'Q1 2025', - }); - - expect(result.startsWith('Weekly Financial Report')).toBe(true); - expect(result).toContain('Period: Q1 2025'); - }); - - it('should log and rethrow errors', async () => { - const logSpy = vi - .spyOn(utils, 'logWorkflowError') - .mockImplementation(() => {}); - const originalToLocaleString = Number.prototype.toLocaleString.bind( - Number.prototype, - ); - - Number.prototype.toLocaleString = () => { - throw new Error('Test error'); - }; - - await expect(weeklyFinancialReportsWorkflow()).rejects.toThrow( - 'Test error', - ); - expect(logSpy).toHaveBeenCalledWith( - 'Weekly Financial Reports', - expect.any(Error), - ); - - Number.prototype.toLocaleString = originalToLocaleString; - logSpy.mockRestore(); - }); -}); diff --git a/workers/main/src/activities/fetchFinancialData.ts b/workers/main/src/activities/fetchFinancialData.ts deleted file mode 100644 index ab56ea7..0000000 --- a/workers/main/src/activities/fetchFinancialData.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface FinancialData { - period: string; - contractType: string; - revenue: number; - cogs: number; - margin: number; - marginality: number; - effectiveRevenue: number; - effectiveMargin: number; - effectiveMarginality: number; -} - -/** - * Fetches financial data for a given period from an external source or database. - * @param period - The period to fetch data for (e.g., 'Q1 2025', 'current') - */ -export async function fetchFinancialData( - period: string = 'current', -): Promise { - // TODO: Replace this stub with actual data fetching logic (e.g., DB query, API call) - return { - period: period, - contractType: 'T&M', - revenue: 120000, - cogs: 80000, - margin: 40000, - marginality: 33.3, - effectiveRevenue: 110000, - effectiveMargin: 35000, - effectiveMarginality: 31.8, - }; -} diff --git a/workers/main/src/activities/financial/factory.test.ts b/workers/main/src/activities/financial/factory.test.ts new file mode 100644 index 0000000..36d79df --- /dev/null +++ b/workers/main/src/activities/financial/factory.test.ts @@ -0,0 +1,105 @@ +import type { Pool } from 'mysql2/promise'; +import { describe, expect, it } from 'vitest'; +import { beforeEach, vi } from 'vitest'; + +import { RedminePool } from '../../common/RedminePool'; +import { ProjectUnitRepository } from '../../repositories/financial/ProjectUnitRepository'; +import { FinancialReportService } from '../../services/financial/FinancialReportService'; +import { + createFinancialReportService, + createProjectUnitRepository, +} from './factory'; + +vi.mock('../../common/RedminePool'); + +const mockExecute = vi.fn(); + +beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(RedminePool.prototype, 'getPool').mockReturnValue({ + execute: mockExecute, + } as unknown as Pool); +}); + +describe('factory', () => { + it('createProjectUnitRepository returns ProjectUnitRepository instance', () => { + const repo = createProjectUnitRepository(); + + expect(repo).toBeInstanceOf(ProjectUnitRepository); + }); + + it('createFinancialReportService returns FinancialReportService instance', () => { + const service = createFinancialReportService(); + + expect(service).toBeInstanceOf(FinancialReportService); + }); + + it('ProjectUnitRepository instance has getProjectUnits method', () => { + const repo = createProjectUnitRepository(); + + expect(typeof repo.getProjectUnits).toBe('function'); + }); + + it('FinancialReportService instance has getWeeklyReport method', () => { + const service = createFinancialReportService(); + + expect(typeof service.getWeeklyReport).toBe('function'); + }); +}); + +describe('factory (behavioral)', () => { + it('ProjectUnitRepository.getProjectUnits returns project units', async () => { + const mockRows = [ + { group_id: 1, group_name: 'A', project_id: 2, project_name: 'B' }, + ]; + + mockExecute.mockResolvedValueOnce([mockRows]); + const repo = createProjectUnitRepository(); + const result = await repo.getProjectUnits(); + + expect(result).toEqual(mockRows); + expect(mockExecute).toHaveBeenCalled(); + }); + + it('ProjectUnitRepository.getProjectUnits returns empty array if no data', async () => { + mockExecute.mockResolvedValueOnce([[]]); + const repo = createProjectUnitRepository(); + const result = await repo.getProjectUnits(); + + expect(result).toEqual([]); + }); + + it('ProjectUnitRepository.getProjectUnits throws if db fails', async () => { + mockExecute.mockRejectedValueOnce(new Error('fail')); + const repo = createProjectUnitRepository(); + + await expect(repo.getProjectUnits()).rejects.toThrow('fail'); + }); + + it('FinancialReportService.getWeeklyReport returns project units', async () => { + const mockRows = [ + { group_id: 1, group_name: 'A', project_id: 2, project_name: 'B' }, + ]; + + mockExecute.mockResolvedValueOnce([mockRows]); + const service = createFinancialReportService(); + const result = await service.getWeeklyReport(); + + expect(result).toEqual(mockRows); + }); + + it('FinancialReportService.getWeeklyReport returns empty array if no data', async () => { + mockExecute.mockResolvedValueOnce([[]]); + const service = createFinancialReportService(); + const result = await service.getWeeklyReport(); + + expect(result).toEqual([]); + }); + + it('FinancialReportService.getWeeklyReport throws if repo fails', async () => { + mockExecute.mockRejectedValueOnce(new Error('fail')); + const service = createFinancialReportService(); + + await expect(service.getWeeklyReport()).rejects.toThrow('fail'); + }); +}); diff --git a/workers/main/src/activities/financial/factory.ts b/workers/main/src/activities/financial/factory.ts new file mode 100644 index 0000000..7c88584 --- /dev/null +++ b/workers/main/src/activities/financial/factory.ts @@ -0,0 +1,10 @@ +import { ProjectUnitRepository } from '../../repositories/financial/ProjectUnitRepository'; +import { FinancialReportService } from '../../services/financial/FinancialReportService'; + +export function createProjectUnitRepository() { + return new ProjectUnitRepository(); +} + +export function createFinancialReportService() { + return new FinancialReportService(createProjectUnitRepository()); +} diff --git a/workers/main/src/activities/financial/getProjectUnits.activity.test.ts b/workers/main/src/activities/financial/getProjectUnits.activity.test.ts new file mode 100644 index 0000000..36c643e --- /dev/null +++ b/workers/main/src/activities/financial/getProjectUnits.activity.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { FinancialReportService } from '../../services/financial/FinancialReportService'; +import * as factory from './factory'; +import { getProjectUnits } from './getProjectUnits.activity'; + +describe('getProjectUnits activity', () => { + it('returns weekly report from service', async () => { + const mockReport = [ + { group_id: 1, group_name: 'A', project_id: 2, project_name: 'B' }, + ]; + + const mockService: Partial = { + getWeeklyReport: vi.fn().mockResolvedValue(mockReport), + }; + + vi.spyOn(factory, 'createFinancialReportService').mockReturnValue( + mockService as FinancialReportService, + ); + const result = await getProjectUnits(); + + expect(result).toEqual(mockReport); + }); + + it('returns empty array if no data', async () => { + const mockService: Partial = { + getWeeklyReport: vi.fn().mockResolvedValue([]), + }; + + vi.spyOn(factory, 'createFinancialReportService').mockReturnValue( + mockService as FinancialReportService, + ); + const result = await getProjectUnits(); + + expect(result).toEqual([]); + }); + + it('throws if service fails', async () => { + const mockService: Partial = { + getWeeklyReport: vi.fn().mockRejectedValue(new Error('fail')), + }; + + vi.spyOn(factory, 'createFinancialReportService').mockReturnValue( + mockService as FinancialReportService, + ); + await expect(getProjectUnits()).rejects.toThrow('fail'); + }); +}); diff --git a/workers/main/src/activities/financial/getProjectUnits.activity.ts b/workers/main/src/activities/financial/getProjectUnits.activity.ts new file mode 100644 index 0000000..f98c990 --- /dev/null +++ b/workers/main/src/activities/financial/getProjectUnits.activity.ts @@ -0,0 +1,4 @@ +import { createFinancialReportService } from './factory'; + +export const getProjectUnits = async () => + createFinancialReportService().getWeeklyReport(); diff --git a/workers/main/src/activities/financial/index.ts b/workers/main/src/activities/financial/index.ts new file mode 100644 index 0000000..963c7ab --- /dev/null +++ b/workers/main/src/activities/financial/index.ts @@ -0,0 +1 @@ +export * from './getProjectUnits.activity'; diff --git a/workers/main/src/activities/index.ts b/workers/main/src/activities/index.ts new file mode 100644 index 0000000..1aab94a --- /dev/null +++ b/workers/main/src/activities/index.ts @@ -0,0 +1 @@ +export * from './weeklyFinancialReports'; diff --git a/workers/main/src/activities/weeklyFinancialReports/index.ts b/workers/main/src/activities/weeklyFinancialReports/index.ts new file mode 100644 index 0000000..aca17e3 --- /dev/null +++ b/workers/main/src/activities/weeklyFinancialReports/index.ts @@ -0,0 +1 @@ +export * from './redmine'; diff --git a/workers/main/src/activities/weeklyFinancialReports/mocks/redmine.ts b/workers/main/src/activities/weeklyFinancialReports/mocks/redmine.ts new file mode 100644 index 0000000..4756c48 --- /dev/null +++ b/workers/main/src/activities/weeklyFinancialReports/mocks/redmine.ts @@ -0,0 +1,31 @@ +import { vi } from 'vitest'; + +import type { ProjectUnit } from '../../../common/types'; +import type { RedmineRepository } from '../../../services/redmine/RedmineRepository'; + +export function createMockProjectUnit( + overrides: Partial = {}, +): ProjectUnit { + return { + group_id: 1, + group_name: 'Group A', + project_id: 10, + project_name: 'Project X', + user_id: 100, + username: 'John Doe', + spent_on: '2024-06-01', + total_hours: 8, + ...overrides, + }; +} + +export function createMockRedmineRepository( + units: ProjectUnit[] = [], + error?: Error, +): RedmineRepository { + return { + getProjectUnits: error + ? vi.fn().mockRejectedValue(error) + : vi.fn().mockResolvedValue(units), + } as unknown as RedmineRepository; +} diff --git a/workers/main/src/activities/weeklyFinancialReports/redmine.test.ts b/workers/main/src/activities/weeklyFinancialReports/redmine.test.ts new file mode 100644 index 0000000..8df1623 --- /dev/null +++ b/workers/main/src/activities/weeklyFinancialReports/redmine.test.ts @@ -0,0 +1,78 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, + type Mock, + vi, +} from 'vitest'; + +import type { ProjectUnit } from '../../common/types'; +import { RedmineService } from '../../services/redmine/RedmineService'; +import { + createMockProjectUnit, + createMockRedmineRepository, +} from './mocks/redmine'; +import { createRedmineService, getProjectUnits } from './redmine'; + +describe('getProjectUnits', () => { + let service: RedmineService; + let repo: ReturnType; + + beforeEach(() => { + repo = createMockRedmineRepository(); + service = new RedmineService(repo); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('calls RedmineService.getProjectUnits', async () => { + const spy = vi.spyOn(service, 'getProjectUnits' as const); + const result = await getProjectUnits(service); + + expect(spy).toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + it('returns correct structure for project units', async () => { + const mockUnits: ProjectUnit[] = [createMockProjectUnit()]; + + (repo.getProjectUnits as Mock).mockResolvedValueOnce(mockUnits); + const result = await getProjectUnits(service); + + expect(result).toEqual(mockUnits); + expect(result[0]).toHaveProperty('group_id'); + expect(result[0]).toHaveProperty('group_name'); + expect(result[0]).toHaveProperty('project_id'); + expect(result[0]).toHaveProperty('project_name'); + expect(result[0]).toHaveProperty('user_id'); + expect(result[0]).toHaveProperty('username'); + expect(result[0]).toHaveProperty('spent_on'); + expect(result[0]).toHaveProperty('total_hours'); + }); + + it('returns empty array if no units found', async () => { + (repo.getProjectUnits as Mock).mockResolvedValueOnce([]); + const result = await getProjectUnits(service); + + expect(result).toEqual([]); + }); + + it('throws if RedmineService.getProjectUnits fails', async () => { + const error = new Error('DB error'); + + (repo.getProjectUnits as Mock).mockRejectedValueOnce(error); + await expect(getProjectUnits(service)).rejects.toThrow('DB error'); + }); +}); + +describe('createRedmineService', () => { + it('creates a RedmineService instance', () => { + const service = createRedmineService(); + + expect(service).toBeInstanceOf(RedmineService); + }); +}); diff --git a/workers/main/src/activities/weeklyFinancialReports/redmine.ts b/workers/main/src/activities/weeklyFinancialReports/redmine.ts new file mode 100644 index 0000000..97a30b8 --- /dev/null +++ b/workers/main/src/activities/weeklyFinancialReports/redmine.ts @@ -0,0 +1,13 @@ +import { RedminePool } from '../../common/RedminePool'; +import { redmineDatabaseConfig } from '../../configs/redmineDatabase'; +import { RedmineRepository } from '../../services/redmine/RedmineRepository'; +import { RedmineService } from '../../services/redmine/RedmineService'; + +export function createRedmineService() { + const repo = new RedmineRepository(new RedminePool(redmineDatabaseConfig)); + + return new RedmineService(repo); +} + +export const getProjectUnits = async (service: RedmineService) => + service.getProjectUnits(); diff --git a/workers/main/src/common/RedminePool.test.ts b/workers/main/src/common/RedminePool.test.ts new file mode 100644 index 0000000..854b5e0 --- /dev/null +++ b/workers/main/src/common/RedminePool.test.ts @@ -0,0 +1,74 @@ +import type { Pool } from 'mysql2/promise'; +import * as mysql from 'mysql2/promise'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { RedminePool } from './RedminePool'; + +const mockPool: Partial = { + end: vi.fn().mockResolvedValue(undefined), +}; + +// Mock mysql2/promise globally +vi.mock('mysql2/promise', () => ({ + createPool: vi.fn(), +})); + +describe('RedminePool', () => { + const credentials = { + host: 'localhost', + user: 'user', + password: 'pass', + database: 'db', + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('creates pool on construction', () => { + (mysql.createPool as unknown as ReturnType).mockReturnValue( + mockPool as Pool, + ); + new RedminePool(credentials); + expect(mysql.createPool).toHaveBeenCalledWith(credentials); + }); + + it('getPool returns the pool', () => { + (mysql.createPool as unknown as ReturnType).mockReturnValue( + mockPool as Pool, + ); + const pool = new RedminePool(credentials); + + expect(pool.getPool()).toBe(mockPool); + }); + + it('endPool ends the pool and sets poolEnded', async () => { + (mysql.createPool as unknown as ReturnType).mockReturnValue( + mockPool as Pool, + ); + const pool = new RedminePool(credentials); + + await pool.endPool(); + expect(mockPool.end).toHaveBeenCalled(); + // После завершения poolEnded = true, getPool создаёт новый пул + (mysql.createPool as unknown as ReturnType).mockReturnValue( + mockPool as Pool, + ); + pool.getPool(); + expect(mysql.createPool).toHaveBeenCalledTimes(2); // первый раз в конструкторе, второй раз после endPool + }); + + it('getPool recreates pool if ended', async () => { + (mysql.createPool as unknown as ReturnType).mockReturnValue( + mockPool as Pool, + ); + const pool = new RedminePool(credentials); + + await pool.endPool(); + (mysql.createPool as unknown as ReturnType).mockReturnValue( + mockPool as Pool, + ); + pool.getPool(); + expect(mysql.createPool).toHaveBeenCalledTimes(2); + }); +}); diff --git a/workers/main/src/common/RedminePool.ts b/workers/main/src/common/RedminePool.ts new file mode 100644 index 0000000..b56b9e4 --- /dev/null +++ b/workers/main/src/common/RedminePool.ts @@ -0,0 +1,33 @@ +import * as mysql from 'mysql2/promise'; +import { Pool, PoolOptions } from 'mysql2/promise'; + +export class RedminePool { + private pool: Pool | null = null; + private credentials: PoolOptions; + private poolEnded = false; + + constructor(credentials: PoolOptions) { + this.credentials = credentials; + this.createPool(); + } + + private createPool() { + this.pool = mysql.createPool(this.credentials); + this.poolEnded = false; + } + + public getPool(): Pool { + if (!this.pool || this.poolEnded) { + this.createPool(); + } + + return this.pool!; + } + + async endPool() { + if (this.pool && !this.poolEnded) { + await this.pool.end(); + this.poolEnded = true; + } + } +} diff --git a/workers/main/src/common/types.ts b/workers/main/src/common/types.ts new file mode 100644 index 0000000..9fbdb1c --- /dev/null +++ b/workers/main/src/common/types.ts @@ -0,0 +1,10 @@ +export interface ProjectUnit { + group_id: number; + group_name: string; + project_id: number; + project_name: string; + user_id: number; + username: string; + spent_on: string; + total_hours: number; +} diff --git a/workers/main/src/__tests__/utils.test.ts b/workers/main/src/common/utils.test.ts similarity index 94% rename from workers/main/src/__tests__/utils.test.ts rename to workers/main/src/common/utils.test.ts index 2a56244..d628838 100644 --- a/workers/main/src/__tests__/utils.test.ts +++ b/workers/main/src/common/utils.test.ts @@ -4,8 +4,8 @@ vi.mock('../common/../configs', () => ({ validationResult: { success: true }, })); -import * as configs from '../common/../configs'; -import { validateEnv } from '../common/utils'; +import * as configs from '../configs'; +import { validateEnv } from './utils'; type ValidationResult = { success: boolean; diff --git a/workers/main/src/__tests__/index.test.ts b/workers/main/src/index.test.ts similarity index 74% rename from workers/main/src/__tests__/index.test.ts rename to workers/main/src/index.test.ts index b03f648..3c6022e 100644 --- a/workers/main/src/__tests__/index.test.ts +++ b/workers/main/src/index.test.ts @@ -1,8 +1,7 @@ import { describe, expect, it } from 'vitest'; import { vi } from 'vitest'; -import * as utils from '../../../common/utils'; -import { handleRunError, run } from '../index'; +import { handleRunError, logger, run } from './index'; vi.mock('@temporalio/worker', () => ({ DefaultLogger: class { @@ -26,13 +25,13 @@ describe('run', () => { describe('handleRunError', () => { it('should log the error and throw the error', () => { - const logSpy = vi - .spyOn(utils, 'logWorkerError') - .mockImplementation(() => {}); const error = new Error('test error'); + const logSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); expect(() => handleRunError(error)).toThrow(error); - expect(logSpy).toHaveBeenCalledWith('main', error); + expect(logSpy).toHaveBeenCalledWith( + `Error in main worker: ${error.message}`, + ); logSpy.mockRestore(); }); }); diff --git a/workers/main/src/index.ts b/workers/main/src/index.ts index 178664b..e62eeca 100644 --- a/workers/main/src/index.ts +++ b/workers/main/src/index.ts @@ -1,6 +1,6 @@ import { DefaultLogger, NativeConnection, Worker } from '@temporalio/worker'; -import { logWorkerError, validateEnv } from '../../common/utils'; +import { validateEnv } from './common/utils'; import { temporalConfig } from './configs/temporal'; import { workerConfig } from './configs/worker'; @@ -37,10 +37,12 @@ export async function run(): Promise { } } -export function handleRunError(err: unknown): never { - logWorkerError('main', err); +export function handleRunError(error: unknown): never { + logger.error( + `Error in main worker: ${error instanceof Error ? error.message : String(error)}`, + ); setTimeout(() => process.exit(1), 100); - throw err; + throw error; } export function mainEntry() { diff --git a/workers/main/src/repositories/financial/IProjectUnitRepository.ts b/workers/main/src/repositories/financial/IProjectUnitRepository.ts new file mode 100644 index 0000000..122f65c --- /dev/null +++ b/workers/main/src/repositories/financial/IProjectUnitRepository.ts @@ -0,0 +1,5 @@ +import { ProjectUnit } from '../../common/types'; + +export interface IProjectUnitRepository { + getProjectUnits(): Promise; +} diff --git a/workers/main/src/repositories/financial/ProjectUnitRepository.test.ts b/workers/main/src/repositories/financial/ProjectUnitRepository.test.ts new file mode 100644 index 0000000..94e7ffc --- /dev/null +++ b/workers/main/src/repositories/financial/ProjectUnitRepository.test.ts @@ -0,0 +1,52 @@ +import type { Pool } from 'mysql2/promise'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { RedminePool } from '../../common/RedminePool'; +import { ProjectUnit } from '../../common/types'; +import { ProjectUnitRepository } from './ProjectUnitRepository'; + +vi.mock('../../common/RedminePool'); + +const mockExecute = vi.fn(); + +beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(RedminePool.prototype, 'getPool').mockReturnValue({ + execute: mockExecute, + } as unknown as Pool); +}); + +describe('ProjectUnitRepository', () => { + it('returns project units from db', async () => { + const mockRows: ProjectUnit[] = [ + { + group_id: 1, + group_name: 'Group', + project_id: 2, + project_name: 'Project', + }, + ]; + + mockExecute.mockResolvedValueOnce([mockRows]); + const repo = new ProjectUnitRepository(); + const result = await repo.getProjectUnits(); + + expect(result).toEqual(mockRows); + expect(mockExecute).toHaveBeenCalled(); + }); + + it('returns empty array if no data', async () => { + mockExecute.mockResolvedValueOnce([[]]); + const repo = new ProjectUnitRepository(); + const result = await repo.getProjectUnits(); + + expect(result).toEqual([]); + }); + + it('throws if db fails', async () => { + mockExecute.mockRejectedValueOnce(new Error('fail')); + const repo = new ProjectUnitRepository(); + + await expect(repo.getProjectUnits()).rejects.toThrow('fail'); + }); +}); diff --git a/workers/main/src/repositories/financial/ProjectUnitRepository.ts b/workers/main/src/repositories/financial/ProjectUnitRepository.ts new file mode 100644 index 0000000..fa83001 --- /dev/null +++ b/workers/main/src/repositories/financial/ProjectUnitRepository.ts @@ -0,0 +1,49 @@ +import { Pool } from 'mysql2/promise'; + +import { RedminePool } from '../../common/RedminePool'; +import { ProjectUnit } from '../../common/types'; +import { redmineDatabaseConfig } from '../../configs/redmineDatabase'; +import { IProjectUnitRepository } from './IProjectUnitRepository'; + +export class ProjectUnitRepository implements IProjectUnitRepository { + private pool: Pool; + constructor() { + this.pool = new RedminePool(redmineDatabaseConfig).getPool(); + } + private getProjectUnitsQuery() { + return `SELECT + group_id, + group_name, + project_id, + project_name, + user_id, + username, + spent_on, + SUM(total_hours) AS total_hours +FROM ( + SELECT + g.id AS group_id, + g.lastname AS group_name, + p.id AS project_id, + p.name AS project_name, + te.user_id AS user_id, + CONCAT(u.firstname, ' ', u.lastname) AS username, + te.spent_on AS spent_on, + te.hours AS total_hours + FROM users AS g + JOIN members AS m ON m.user_id = g.id + JOIN projects AS p ON p.id = m.project_id + JOIN time_entries te ON te.project_id = p.id + JOIN users AS u ON u.id = te.user_id + WHERE te.spent_on >= CURDATE() - INTERVAL 7 DAY +) t +GROUP BY group_id, group_name, project_id, project_name, user_id, username, spent_on +ORDER BY group_name ASC, project_name ASC, username ASC, spent_on ASC`; + } + async getProjectUnits(): Promise { + const query = this.getProjectUnitsQuery(); + const [rows] = await this.pool.execute(query); + + return rows as ProjectUnit[]; + } +} diff --git a/workers/main/src/services/financial/FinancialReportService.test.ts b/workers/main/src/services/financial/FinancialReportService.test.ts new file mode 100644 index 0000000..415e392 --- /dev/null +++ b/workers/main/src/services/financial/FinancialReportService.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { IProjectUnitRepository } from '../../repositories/financial/IProjectUnitRepository'; +import { FinancialReportService } from './FinancialReportService'; + +describe('FinancialReportService', () => { + it('returns weekly report from repo', async () => { + const repo: Partial = { + getProjectUnits: vi + .fn() + .mockResolvedValue([ + { group_id: 1, group_name: 'A', project_id: 2, project_name: 'B' }, + ]), + }; + const service = new FinancialReportService(repo as IProjectUnitRepository); + const result = await service.getWeeklyReport(); + + expect(result).toEqual([ + { group_id: 1, group_name: 'A', project_id: 2, project_name: 'B' }, + ]); + }); + + it('returns empty array if repo returns none', async () => { + const repo: Partial = { + getProjectUnits: vi.fn().mockResolvedValue([]), + }; + const service = new FinancialReportService(repo as IProjectUnitRepository); + const result = await service.getWeeklyReport(); + + expect(result).toEqual([]); + }); + + it('throws if repo fails', async () => { + const repo: Partial = { + getProjectUnits: vi.fn().mockRejectedValue(new Error('fail')), + }; + const service = new FinancialReportService(repo as IProjectUnitRepository); + + await expect(service.getWeeklyReport()).rejects.toThrow('fail'); + }); +}); diff --git a/workers/main/src/services/financial/FinancialReportService.ts b/workers/main/src/services/financial/FinancialReportService.ts new file mode 100644 index 0000000..7f0246a --- /dev/null +++ b/workers/main/src/services/financial/FinancialReportService.ts @@ -0,0 +1,9 @@ +import type { ProjectUnit } from '../../common/types'; +import { IProjectUnitRepository } from '../../repositories/financial/IProjectUnitRepository'; + +export class FinancialReportService { + constructor(private repo: IProjectUnitRepository) {} + async getWeeklyReport(): Promise { + return this.repo.getProjectUnits(); + } +} diff --git a/workers/main/src/services/redmine/IRedmineRepository.ts b/workers/main/src/services/redmine/IRedmineRepository.ts new file mode 100644 index 0000000..4507360 --- /dev/null +++ b/workers/main/src/services/redmine/IRedmineRepository.ts @@ -0,0 +1,5 @@ +import type { ProjectUnit } from '../../common/types'; + +export interface IRedmineRepository { + getProjectUnits(): Promise; +} diff --git a/workers/main/src/services/redmine/RedmineRepository.test.ts b/workers/main/src/services/redmine/RedmineRepository.test.ts new file mode 100644 index 0000000..981a053 --- /dev/null +++ b/workers/main/src/services/redmine/RedmineRepository.test.ts @@ -0,0 +1,69 @@ +import type { Pool } from 'mysql2/promise'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { IPoolProvider, RedmineRepository } from './RedmineRepository'; + +// Mock Pool and IPoolProvider +const mockQuery = vi.fn(); +const mockPool: Partial = { + query: mockQuery, +}; + +const mockPoolProvider: IPoolProvider = { + getPool: () => mockPool as Pool, +}; + +describe('RedmineRepository', () => { + let repo: RedmineRepository; + + beforeEach(() => { + vi.clearAllMocks(); + repo = new RedmineRepository(mockPoolProvider); + }); + + it('getProjectUnits returns rows from pool', async () => { + const mockRows = [ + { + group_id: 1, + group_name: 'A', + project_id: 2, + project_name: 'B', + user_id: 3, + username: 'User X', + spent_on: '2024-06-01', + total_hours: 5, + }, + ]; + + mockQuery.mockResolvedValueOnce([mockRows]); + const result = await repo.getProjectUnits(); + + expect(result).toEqual([ + { + group_id: 1, + group_name: 'A', + project_id: 2, + project_name: 'B', + user_id: 3, + username: 'User X', + spent_on: '2024-06-01', + total_hours: 5, + }, + ]); + expect(mockQuery).toHaveBeenCalled(); + }); + + it('getProjectUnits returns empty array if no rows', async () => { + mockQuery.mockResolvedValueOnce([[]]); + const result = await repo.getProjectUnits(); + + expect(result).toEqual([]); + }); + + it('getProjectUnits throws error if query fails', async () => { + mockQuery.mockRejectedValueOnce(new Error('DB error')); + await expect(repo.getProjectUnits()).rejects.toThrow( + 'RedmineRepository.getProjectUnits failed: DB error', + ); + }); +}); diff --git a/workers/main/src/services/redmine/RedmineRepository.ts b/workers/main/src/services/redmine/RedmineRepository.ts new file mode 100644 index 0000000..05f17a3 --- /dev/null +++ b/workers/main/src/services/redmine/RedmineRepository.ts @@ -0,0 +1,82 @@ +import { Pool, RowDataPacket } from 'mysql2/promise'; + +import { ProjectUnit } from '../../common/types'; +import { IRedmineRepository } from './IRedmineRepository'; + +interface ProjectUnitRow extends RowDataPacket { + group_id: number; + group_name: string; + project_id: number; + project_name: string; + user_id: number; + username: string; + spent_on: string; + total_hours: number; +} + +const PROJECT_UNITS_QUERY = `SELECT + group_id, + group_name, + project_id, + project_name, + user_id, + username, + spent_on, + SUM(total_hours) AS total_hours +FROM ( + SELECT + g.id AS group_id, + g.lastname AS group_name, + p.id AS project_id, + p.name AS project_name, + te.user_id AS user_id, + CONCAT(u.firstname, ' ', u.lastname) AS username, + te.spent_on AS spent_on, + te.hours AS total_hours + FROM users AS g + JOIN members AS m ON m.user_id = g.id + JOIN projects AS p ON p.id = m.project_id + JOIN time_entries te ON te.project_id = p.id + JOIN users AS u ON u.id = te.user_id + WHERE te.spent_on >= CURDATE() - INTERVAL 7 DAY +) t +GROUP BY group_id, group_name, project_id, project_name, user_id, username, spent_on +ORDER BY group_name ASC, project_name ASC, username ASC, spent_on ASC`; + +export interface IPoolProvider { + getPool(): Pool; +} + +export class RedmineRepository implements IRedmineRepository { + private readonly pool: Pool; + + constructor(poolProvider: IPoolProvider) { + this.pool = poolProvider.getPool(); + } + + async getProjectUnits(): Promise { + try { + const [rows] = + await this.pool.query(PROJECT_UNITS_QUERY); + + if (!Array.isArray(rows)) { + throw new Error('Query did not return an array'); + } + + return rows.map((row) => ({ + group_id: Number(row.group_id), + group_name: String(row.group_name), + project_id: Number(row.project_id), + project_name: String(row.project_name), + user_id: Number(row.user_id), + username: String(row.username), + spent_on: String(row.spent_on), + total_hours: Number(row.total_hours), + })); + } catch (error) { + throw new Error( + `RedmineRepository.getProjectUnits failed: ${(error as Error).message}`, + ); + } + } +} diff --git a/workers/main/src/services/redmine/RedmineService.ts b/workers/main/src/services/redmine/RedmineService.ts new file mode 100644 index 0000000..7c693b7 --- /dev/null +++ b/workers/main/src/services/redmine/RedmineService.ts @@ -0,0 +1,10 @@ +import type { ProjectUnit } from '../../common/types'; +import { IRedmineRepository } from './IRedmineRepository'; + +export class RedmineService { + constructor(private repo: IRedmineRepository) {} + + async getProjectUnits(): Promise { + return this.repo.getProjectUnits(); + } +} diff --git a/workers/main/src/workflows/financial/FinancialReportFormatter.ts b/workers/main/src/workflows/financial/FinancialReportFormatter.ts new file mode 100644 index 0000000..7b933d9 --- /dev/null +++ b/workers/main/src/workflows/financial/FinancialReportFormatter.ts @@ -0,0 +1,7 @@ +import { ProjectUnit } from '../../common/types'; + +export function formatFinancialReport(units: ProjectUnit[]) { + const reportTitle = 'Weekly Financial Report'; + + return `${reportTitle}\n${JSON.stringify(units, null, 2)}`; +} diff --git a/workers/main/src/workflows/financial/index.test.ts b/workers/main/src/workflows/financial/index.test.ts new file mode 100644 index 0000000..c842930 --- /dev/null +++ b/workers/main/src/workflows/financial/index.test.ts @@ -0,0 +1,67 @@ +import { TestWorkflowEnvironment } from '@temporalio/testing'; +import { DefaultLogger, LogEntry, Runtime, Worker } from '@temporalio/worker'; +import { v4 as uuidv4 } from 'uuid'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +import { weeklyFinancialReportsWorkflow } from './weeklyFinancialReports.workflow'; + +const mockProjectUnits = [ + { + group_id: 1, + group_name: 'Engineering', + project_id: 101, + project_name: 'Project Alpha', + }, + { + group_id: 2, + group_name: 'QA', + project_id: 102, + project_name: 'Project Beta', + }, +]; + +describe('weeklyFinancialReportsWorkflow', () => { + let testEnv: TestWorkflowEnvironment; + + beforeAll(async () => { + Runtime.install({ + logger: new DefaultLogger('WARN', (entry: LogEntry) => + // eslint-disable-next-line no-console + console.log(`[${entry.level}]`, entry.message), + ), + }); + + testEnv = await TestWorkflowEnvironment.createTimeSkipping(); + }); + + afterAll(async () => { + await testEnv?.teardown(); + }); + + it('generates a report with mocked activities', async () => { + const { client, nativeConnection } = testEnv; + const mockActivities = { + getProjectUnits: async () => mockProjectUnits, + }; + + const taskQueue = `test-${uuidv4()}`; + + const worker = await Worker.create({ + connection: nativeConnection, + taskQueue, + workflowsPath: require.resolve('./index.ts'), + activities: mockActivities, + }); + + const result = await worker.runUntil( + client.workflow.execute(weeklyFinancialReportsWorkflow, { + workflowId: uuidv4(), + taskQueue, + }), + ); + + expect(result).toContain('Weekly Financial Report'); + expect(result).toContain('Engineering'); + expect(result).toContain('Project Alpha'); + }); +}); diff --git a/workers/main/src/workflows/financial/index.ts b/workers/main/src/workflows/financial/index.ts new file mode 100644 index 0000000..c3fd4e2 --- /dev/null +++ b/workers/main/src/workflows/financial/index.ts @@ -0,0 +1 @@ +export * from './weeklyFinancialReports.workflow'; diff --git a/workers/main/src/workflows/financial/weeklyFinancialReports.workflow.test.ts b/workers/main/src/workflows/financial/weeklyFinancialReports.workflow.test.ts new file mode 100644 index 0000000..8b99b5b --- /dev/null +++ b/workers/main/src/workflows/financial/weeklyFinancialReports.workflow.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { vi } from 'vitest'; + +import { formatFinancialReport } from './FinancialReportFormatter'; + +describe('formatFinancialReport', () => { + it('should format report with given project units', () => { + const result = formatFinancialReport([ + { + group_id: 1, + group_name: 'Engineering', + project_id: 101, + project_name: 'Project Alpha', + }, + ]); + + expect(result).toContain('Weekly Financial Report'); + expect(result).toContain('Engineering'); + expect(result).toContain('Project Alpha'); + }); +}); + +vi.mock('@temporalio/workflow', () => ({ + proxyActivities: () => ({ + getProjectUnits: vi.fn().mockResolvedValue([ + { + group_id: 1, + group_name: 'Engineering', + project_id: 101, + project_name: 'Project Alpha', + }, + ]), + }), +})); + +import { weeklyFinancialReportsWorkflow } from './weeklyFinancialReports.workflow'; + +describe('weeklyFinancialReportsWorkflow', () => { + it('should return formatted report from workflow', async () => { + const result = await weeklyFinancialReportsWorkflow(); + + expect(result).toContain('Weekly Financial Report'); + expect(result).toContain('Engineering'); + expect(result).toContain('Project Alpha'); + }); +}); diff --git a/workers/main/src/workflows/financial/weeklyFinancialReports.workflow.ts b/workers/main/src/workflows/financial/weeklyFinancialReports.workflow.ts new file mode 100644 index 0000000..704b160 --- /dev/null +++ b/workers/main/src/workflows/financial/weeklyFinancialReports.workflow.ts @@ -0,0 +1,14 @@ +import { proxyActivities } from '@temporalio/workflow'; + +import type * as activities from '../../activities/financial'; +import { formatFinancialReport } from './FinancialReportFormatter'; + +const { getProjectUnits } = proxyActivities({ + startToCloseTimeout: '10 minutes', +}); + +export async function weeklyFinancialReportsWorkflow(): Promise { + const projectUnits = await getProjectUnits(); + + return formatFinancialReport(projectUnits); +} diff --git a/workers/main/src/workflows/index.ts b/workers/main/src/workflows/index.ts index 1aab94a..b8d3a2e 100644 --- a/workers/main/src/workflows/index.ts +++ b/workers/main/src/workflows/index.ts @@ -1 +1 @@ -export * from './weeklyFinancialReports'; +export { weeklyFinancialReportsWorkflow } from './financial/index'; diff --git a/workers/main/src/workflows/weeklyFinancialReports/index.ts b/workers/main/src/workflows/weeklyFinancialReports/index.ts deleted file mode 100644 index 2951d6d..0000000 --- a/workers/main/src/workflows/weeklyFinancialReports/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { logWorkflowError } from '../../../../common/utils'; -import { fetchFinancialData } from '../../activities/fetchFinancialData'; - -export async function weeklyFinancialReportsWorkflow({ - period = 'current', -}: { period?: string } = {}): Promise { - try { - const reportTitle = 'Weekly Financial Report'; - const data = await fetchFinancialData(period); - const report = `Period: ${data.period} -Contract Type: ${data.contractType} -Revenue: $${data.revenue.toLocaleString()} -COGS: $${data.cogs.toLocaleString()} -Margin: $${data.margin.toLocaleString()} -Marginality: ${data.marginality}%\n\nEffective Revenue (last 4 months): $${data.effectiveRevenue.toLocaleString()} -Effective Margin: $${data.effectiveMargin.toLocaleString()} -Effective Marginality: ${data.effectiveMarginality}%`; - - return `${reportTitle}\n${report}`; - } catch (error) { - logWorkflowError('Weekly Financial Reports', error); - throw error; - } -} diff --git a/workers/main/vitest.config.ts b/workers/main/vitest.config.ts index 7f365f6..4fccab1 100644 --- a/workers/main/vitest.config.ts +++ b/workers/main/vitest.config.ts @@ -7,13 +7,17 @@ export default defineConfig({ test: { globals: true, environment: 'node', - include: ['src/__tests__/**/*.test.ts'], + include: ['src/**/*.test.ts'], coverage: { provider: 'v8', reporter: ['text', 'lcov'], all: true, - include: ['src/**/*.ts'], - exclude: ['src/__tests__/**', 'src/dist/**'], + exclude: [ + 'src/dist/**', + 'src/**/*.test.ts', + 'eslint.config.mjs', + 'vitest.config.ts', + ], }, }, });