diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..fcd8b40 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index a55e7a1..79ee123 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..b0c1c68 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c467e19..c7ffa52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { - "name": "nodeapi", - "version": "5.0.0", + "name": "myfin-api", + "version": "1.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "nodeapi", - "version": "5.0.0", + "name": "myfin-api", + "version": "1.2.4", "license": "GPL-3.0", "dependencies": { - "@prisma/client": "^5.2.0", + "@prisma/client": "^5.8.0", "bcrypt": "^5.0.0", "cors": "^2.8.5", "dayjs": "^1.10.6", @@ -40,7 +40,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "nodemon": "^3.0.1", "prettier": "^3.0.1", - "prisma": "^5.2.0", + "prisma": "^5.8.0", "ts-node": "^10.9.1", "typescript": "^5.1.6" }, @@ -58,24 +58,25 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -85,22 +86,22 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -131,31 +132,31 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -163,9 +164,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -187,34 +188,34 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -254,13 +255,13 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -488,21 +489,15 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", + "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -559,13 +554,10 @@ } }, "node_modules/@prisma/client": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.2.0.tgz", - "integrity": "sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.8.0.tgz", + "integrity": "sha512-QxO6C4MaA/ysTIbC+EcAH1aX/YkpymhXtO6zPdk+FvA7+59tNibIYpd+7koPdViLg2iKES4ojsxWNUGNJaEcbA==", "hasInstallScript": true, - "dependencies": { - "@prisma/engines-version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f" - }, "engines": { "node": ">=16.13" }, @@ -578,17 +570,50 @@ } } }, + "node_modules/@prisma/debug": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.8.0.tgz", + "integrity": "sha512-ZqPpkvbovu/kQJ1bvy57NO4dw97fpQGcbQSCtsqlwSE1UNKJP75R3BKxdznk8ZPMY+GJdMRetWNv4oAvSbWn8Q==", + "devOptional": true + }, "node_modules/@prisma/engines": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.2.0.tgz", - "integrity": "sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.8.0.tgz", + "integrity": "sha512-Qhqm9WWLujNEC13AuZlUO14SQ15tNLe5puaz+tOk7UqINqJ3PtqMmuSuzomiw2diGVqZ+HYiSQzlR3+pPucVHA==", "devOptional": true, - "hasInstallScript": true + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.8.0", + "@prisma/engines-version": "5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848", + "@prisma/fetch-engine": "5.8.0", + "@prisma/get-platform": "5.8.0" + } }, "node_modules/@prisma/engines-version": { - "version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f.tgz", - "integrity": "sha512-jsnKT5JIDIE01lAeCj2ghY9IwxkedhKNvxQeoyLs6dr4ZXynetD0vTy7u6wMJt8vVPv8I5DPy/I4CFaoXAgbtg==" + "version": "5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848.tgz", + "integrity": "sha512-cXcoVweYbnv8xRfkWq9oj8BECOdzHUazrSpYCa0ehp5TNz4l5Spa8jbq/VROCTzj3ZncH5D9Q2TmySYTOUeKlw==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.8.0.tgz", + "integrity": "sha512-1CAuE+JoYsPNggMEn6qk0zos06Uc9bYZBJ0VBPHD6R7REL05614koAbOCmn52IaYz3nobb7f25hqW6AY7rLkIw==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.8.0", + "@prisma/engines-version": "5.8.0-37.0a83d8541752d7582de2ebc1ece46519ce72a848", + "@prisma/get-platform": "5.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.8.0.tgz", + "integrity": "sha512-Nk3rhTFZ1LYkFZJnpSvQcLPCaBWgJQfteHII6UEENOOkYlmP0k3FuswND54tzzEr4qs39wOdV9pbXKX9U2lv7A==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.8.0" + } }, "node_modules/@sideway/address": { "version": "4.1.0", @@ -5869,13 +5894,13 @@ } }, "node_modules/prisma": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.2.0.tgz", - "integrity": "sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.8.0.tgz", + "integrity": "sha512-hDKoEqPt2qEUTH5yGO3l27CBnPtwvte0CGMKrpCr9+/A919JghfqJ3qgCGgMbOwdkXUOzdho0RH9tyUF3UhpMw==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.2.0" + "@prisma/engines": "5.8.0" }, "bin": { "prisma": "build/index.js" diff --git a/package.json b/package.json index 630bfb0..1614b44 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,11 @@ "db:deploy": "prisma migrate deploy", "db:generate": "prisma generate", "db:migrate": "prisma migrate dev", + "db:migrate-from-db": "prisma db pull && prisma migrate dev", "db:seed": "prisma db seed" }, "name": "myfin-api", - "version": "1.2.4", + "version": "2.0.0", "description": "NodeJS API for Myfin", "main": "src/server.js", "devDependencies": { @@ -36,7 +37,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "nodemon": "^3.0.1", "prettier": "^3.0.1", - "prisma": "^5.2.0", + "prisma": "^5.8.0", "ts-node": "^10.9.1", "typescript": "^5.1.6" }, @@ -51,7 +52,7 @@ }, "homepage": "https://github.com/aFaneca/myfin#readme", "dependencies": { - "@prisma/client": "^5.2.0", + "@prisma/client": "^5.8.0", "bcrypt": "^5.0.0", "cors": "^2.8.5", "dayjs": "^1.10.6", diff --git a/prisma/migrations/20240113205612_added_tags_removed_balances_refactored_constraints/migration.sql b/prisma/migrations/20240113205612_added_tags_removed_balances_refactored_constraints/migration.sql new file mode 100644 index 0000000..5c487f4 --- /dev/null +++ b/prisma/migrations/20240113205612_added_tags_removed_balances_refactored_constraints/migration.sql @@ -0,0 +1,77 @@ +/* + Warnings: + + - You are about to drop the `balances` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE `balances`; + +-- CreateTable +CREATE TABLE `tags` ( + `tag_id` BIGINT NOT NULL AUTO_INCREMENT, + `description` LONGTEXT NULL, + `name` VARCHAR(255) NOT NULL, + `users_user_id` BIGINT NOT NULL, + + UNIQUE INDEX `uq_name`(`name`), + INDEX `fk_users_user_id`(`users_user_id`), + PRIMARY KEY (`tag_id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `transaction_has_tags` ( + `transactions_transaction_id` BIGINT NOT NULL, + `tags_tag_id` BIGINT NOT NULL, + + INDEX `transaction_has_tags_transactions_transaction_id_fk`(`transactions_transaction_id`), + PRIMARY KEY (`tags_tag_id`, `transactions_transaction_id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateIndex +CREATE INDEX `budgets_has_categories_users_user_id_fk` ON `budgets_has_categories`(`budgets_users_user_id`); + +-- AddForeignKey +ALTER TABLE `accounts` ADD CONSTRAINT `accounts_users_user_id_fk` FOREIGN KEY (`users_user_id`) REFERENCES `users`(`user_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `balances_snapshot` ADD CONSTRAINT `balances_snapshot_accounts_account_id_fk` FOREIGN KEY (`accounts_account_id`) REFERENCES `accounts`(`account_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `budgets` ADD CONSTRAINT `budgets_users_user_id_fk` FOREIGN KEY (`users_user_id`) REFERENCES `users`(`user_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `budgets_has_categories` ADD CONSTRAINT `budgets_has_categories_budgets_budget_id_fk` FOREIGN KEY (`budgets_budget_id`) REFERENCES `budgets`(`budget_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `budgets_has_categories` ADD CONSTRAINT `budgets_has_categories_categories_category_id_fk` FOREIGN KEY (`categories_category_id`) REFERENCES `categories`(`category_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `budgets_has_categories` ADD CONSTRAINT `budgets_has_categories_users_user_id_fk` FOREIGN KEY (`budgets_users_user_id`) REFERENCES `users`(`user_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `categories` ADD CONSTRAINT `categories_users_user_id_fk` FOREIGN KEY (`users_user_id`) REFERENCES `users`(`user_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `entities` ADD CONSTRAINT `entities_users_user_id_fk` FOREIGN KEY (`users_user_id`) REFERENCES `users`(`user_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `invest_asset_evo_snapshot` ADD CONSTRAINT `invest_asset_evo_snapshot_invest_assets_asset_id_fk` FOREIGN KEY (`invest_assets_asset_id`) REFERENCES `invest_assets`(`asset_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `invest_desired_allocations` ADD CONSTRAINT `invest_desired_allocations_users_user_id_fk` FOREIGN KEY (`users_user_id`) REFERENCES `users`(`user_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `invest_transactions` ADD CONSTRAINT `invest_transactions_invest_assets_asset_id_fk` FOREIGN KEY (`invest_assets_asset_id`) REFERENCES `invest_assets`(`asset_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `rules` ADD CONSTRAINT `rules_users_user_id_fk` FOREIGN KEY (`users_user_id`) REFERENCES `users`(`user_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `tags` ADD CONSTRAINT `fk_users_user_id` FOREIGN KEY (`users_user_id`) REFERENCES `users`(`user_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `transaction_has_tags` ADD CONSTRAINT `transaction_has_tags_tags_tag_id_fk` FOREIGN KEY (`tags_tag_id`) REFERENCES `tags`(`tag_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `transaction_has_tags` ADD CONSTRAINT `transaction_has_tags_transactions_transaction_id_fk` FOREIGN KEY (`transactions_transaction_id`) REFERENCES `transactions`(`transaction_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/prisma/migrations/20240113233615_changed_transactions_constraints/migration.sql b/prisma/migrations/20240113233615_changed_transactions_constraints/migration.sql new file mode 100644 index 0000000..72c7b26 --- /dev/null +++ b/prisma/migrations/20240113233615_changed_transactions_constraints/migration.sql @@ -0,0 +1,14 @@ +-- DropIndex +DROP INDEX `fk_transactions_accounts1_idx` ON `transactions`; + +-- CreateIndex +CREATE INDEX `fk_transactions_accounts1_idx` ON `transactions`(`accounts_account_from_id`, `accounts_account_to_id`); + +-- CreateIndex +CREATE INDEX `transactions_accounts_account_to_id_index` ON `transactions`(`accounts_account_to_id`); + +-- AddForeignKey +ALTER TABLE `transactions` ADD CONSTRAINT `transactions_accounts_account_id_fk` FOREIGN KEY (`accounts_account_to_id`) REFERENCES `accounts`(`account_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `transactions` ADD CONSTRAINT `transactions_accounts_account_id_fk2` FOREIGN KEY (`accounts_account_from_id`) REFERENCES `accounts`(`account_id`) ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/prisma/migrations/20240114230347_fixed_uq_constraint_tags/migration.sql b/prisma/migrations/20240114230347_fixed_uq_constraint_tags/migration.sql new file mode 100644 index 0000000..805b15b --- /dev/null +++ b/prisma/migrations/20240114230347_fixed_uq_constraint_tags/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - A unique constraint covering the columns `[name,users_user_id]` on the table `tags` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropIndex +DROP INDEX `uq_name` ON `tags`; + +-- CreateIndex +CREATE UNIQUE INDEX `uq_name` ON `tags`(`name`, `users_user_id`); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8c814fc..bde62ae 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,51 +8,49 @@ datasource db { } model accounts { - account_id BigInt @id @unique(map: "account_id_UNIQUE") @default(autoincrement()) - name String @db.VarChar(255) - type String @db.VarChar(45) - description String? @db.LongText - exclude_from_budgets Boolean - status String @db.VarChar(45) - users_user_id BigInt - current_balance BigInt? @default(0) - created_timestamp BigInt? - updated_timestamp BigInt? - color_gradient String? @db.VarChar(45) + account_id BigInt @id @unique(map: "account_id_UNIQUE") @default(autoincrement()) + name String @db.VarChar(255) + type String @db.VarChar(45) + description String? @db.LongText + exclude_from_budgets Boolean + status String @db.VarChar(45) + users_user_id BigInt + current_balance BigInt? @default(0) + created_timestamp BigInt? + updated_timestamp BigInt? + color_gradient String? @db.VarChar(45) + users users @relation(fields: [users_user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction, map: "accounts_users_user_id_fk") + balances_snapshot balances_snapshot[] + transactions_transactions_accounts_account_to_idToaccounts transactions[] @relation("transactions_accounts_account_to_idToaccounts") + transactions_transactions_accounts_account_from_idToaccounts transactions[] @relation("transactions_accounts_account_from_idToaccounts") @@unique([name, users_user_id], map: "name_UNIQUE") @@index([users_user_id], map: "fk_accounts_users1_idx") } -model balances { - balance_id BigInt @id @default(autoincrement()) - date_timestamp BigInt - amount Float - accounts_account_id BigInt - - @@index([accounts_account_id], map: "fk_balances_accounts1_idx") -} - model balances_snapshot { accounts_account_id BigInt month Int year Int - balance BigInt @default(0) + balance BigInt @default(0) created_timestamp BigInt updated_timestamp BigInt? + accounts accounts @relation(fields: [accounts_account_id], references: [account_id], onDelete: NoAction, onUpdate: NoAction, map: "balances_snapshot_accounts_account_id_fk") @@id([accounts_account_id, month, year]) @@index([accounts_account_id], map: "fk_balances_snapshot_accounts1_idx") } model budgets { - budget_id BigInt @unique(map: "budget_id_UNIQUE") @default(autoincrement()) - month Int - year Int - observations String? @db.LongText - is_open Boolean - initial_balance BigInt? - users_user_id BigInt + budget_id BigInt @unique(map: "budget_id_UNIQUE") @default(autoincrement()) + month Int + year Int + observations String? @db.LongText + is_open Boolean + initial_balance BigInt? + users_user_id BigInt + users users @relation(fields: [users_user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction, map: "budgets_users_user_id_fk") + budgets_has_categories budgets_has_categories[] @@id([budget_id, users_user_id]) @@unique([month, year, users_user_id], map: "uq_month_year_user") @@ -63,25 +61,31 @@ model budgets_has_categories { budgets_budget_id BigInt budgets_users_user_id BigInt categories_category_id BigInt - planned_amount_credit BigInt @default(0) - current_amount BigInt @default(0) - planned_amount_debit BigInt @default(0) + planned_amount_credit BigInt @default(0) + current_amount BigInt @default(0) + planned_amount_debit BigInt @default(0) + budgets budgets @relation(fields: [budgets_budget_id], references: [budget_id], onDelete: NoAction, onUpdate: NoAction, map: "budgets_has_categories_budgets_budget_id_fk") + categories categories @relation(fields: [categories_category_id], references: [category_id], onDelete: NoAction, onUpdate: NoAction, map: "budgets_has_categories_categories_category_id_fk") + users users @relation(fields: [budgets_users_user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction, map: "budgets_has_categories_users_user_id_fk") @@id([budgets_budget_id, budgets_users_user_id, categories_category_id]) @@index([budgets_budget_id, budgets_users_user_id], map: "fk_budgets_has_categories_budgets1_idx") @@index([categories_category_id], map: "fk_budgets_has_categories_categories1_idx") + @@index([budgets_users_user_id], map: "budgets_has_categories_users_user_id_fk") } model categories { - category_id BigInt @id @default(autoincrement()) - name String @db.VarChar(255) - type String @db.Char(1) - users_user_id BigInt - description String? @db.LongText - color_gradient String? @db.VarChar(45) - status String @default("Ativa") @db.VarChar(45) - exclude_from_budgets Int @default(0) @db.TinyInt - transactions transactions[] + category_id BigInt @id @default(autoincrement()) + name String @db.VarChar(255) + type String @db.Char(1) + users_user_id BigInt + description String? @db.LongText + color_gradient String? @db.VarChar(45) + status String @default("Ativa") @db.VarChar(45) + exclude_from_budgets Int @default(0) @db.TinyInt + budgets_has_categories budgets_has_categories[] + users users @relation(fields: [users_user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction, map: "categories_users_user_id_fk") + transactions transactions[] @@unique([users_user_id, type, name], map: "uq_name_type_user_id") @@index([users_user_id], map: "fk_category_users_idx") @@ -91,6 +95,7 @@ model entities { entity_id BigInt @id @unique(map: "entity_id_UNIQUE") @default(autoincrement()) name String @db.VarChar(255) users_user_id BigInt + users users @relation(fields: [users_user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction, map: "entities_users_user_id_fk") transactions transactions[] @@unique([name, users_user_id], map: "name_UNIQUE") @@ -101,13 +106,14 @@ model entities { model invest_asset_evo_snapshot { month Int year Int - units Decimal @db.Decimal(16, 6) + units Decimal @db.Decimal(16, 6) invested_amount BigInt current_value BigInt invest_assets_asset_id BigInt created_at BigInt updated_at BigInt withdrawn_amount BigInt + invest_assets invest_assets @relation(fields: [invest_assets_asset_id], references: [asset_id], onDelete: NoAction, onUpdate: NoAction, map: "invest_asset_evo_snapshot_invest_assets_asset_id_fk") @@id([month, year, invest_assets_asset_id]) @@unique([month, year, invest_assets_asset_id], map: "uq_month_year_invest_assets_asset_id") @@ -115,15 +121,17 @@ model invest_asset_evo_snapshot { } model invest_assets { - asset_id BigInt @id @unique(map: "asset_id_UNIQUE") @default(autoincrement()) - name String @db.VarChar(75) - ticker String? @db.VarChar(45) - units Decimal @db.Decimal(16, 6) - type String @db.VarChar(75) - broker String? @db.VarChar(45) - created_at BigInt - updated_at BigInt? - users_user_id BigInt + asset_id BigInt @id @unique(map: "asset_id_UNIQUE") @default(autoincrement()) + name String @db.VarChar(75) + ticker String? @db.VarChar(45) + units Decimal @db.Decimal(16, 6) + type String @db.VarChar(75) + broker String? @db.VarChar(45) + created_at BigInt + updated_at BigInt? + users_user_id BigInt + invest_asset_evo_snapshot invest_asset_evo_snapshot[] + invest_transactions invest_transactions[] @@unique([name, type, users_user_id], map: "users_user_id_type_name_unique") @@index([users_user_id], map: "fk_invest_assets_users1_idx") @@ -134,6 +142,7 @@ model invest_desired_allocations { type String @unique(map: "type_UNIQUE") @db.VarChar(75) alloc_percentage Float? @db.Float users_user_id BigInt + users users @relation(fields: [users_user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction, map: "invest_desired_allocations_users_user_id_fk") @@id([desired_allocations_id, type]) @@index([users_user_id], map: "fk_invest_desired_allocations_users1_idx") @@ -150,6 +159,7 @@ model invest_transactions { invest_assets_asset_id BigInt created_at BigInt updated_at BigInt + invest_assets invest_assets @relation(fields: [invest_assets_asset_id], references: [asset_id], onDelete: NoAction, onUpdate: NoAction, map: "invest_transactions_invest_assets_asset_id_fk") @@index([invest_assets_asset_id], map: "fk_invest_transactions_invest_assets1_idx") } @@ -173,40 +183,75 @@ model rules { assign_type String? @db.VarChar(45) users_user_id BigInt assign_is_essential Boolean @default(false) + users users @relation(fields: [users_user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction, map: "rules_users_user_id_fk") @@id([rule_id, users_user_id]) @@index([users_user_id], map: "fk_rules_users1_idx") } model transactions { - transaction_id BigInt @id @unique(map: "transaction_id_UNIQUE") @default(autoincrement()) - date_timestamp BigInt - amount BigInt - type String @db.Char(1) - description String? @db.LongText - entities_entity_id BigInt? - accounts_account_from_id BigInt? - accounts_account_to_id BigInt? - categories_category_id BigInt? - is_essential Boolean @default(false) - entities entities? @relation(fields: [entities_entity_id], references: [entity_id], onUpdate: NoAction, map: "transactions_ibfk_1") - categories categories? @relation(fields: [categories_category_id], references: [category_id], onUpdate: NoAction, map: "transactions_ibfk_2") - - @@index([accounts_account_from_id], map: "fk_transactions_accounts1_idx") + transaction_id BigInt @id @unique(map: "transaction_id_UNIQUE") @default(autoincrement()) + date_timestamp BigInt + amount BigInt + type String @db.Char(1) + description String? @db.LongText + entities_entity_id BigInt? + accounts_account_from_id BigInt? + accounts_account_to_id BigInt? + categories_category_id BigInt? + is_essential Boolean @default(false) + transaction_has_tags transaction_has_tags[] + accounts_transactions_accounts_account_to_idToaccounts accounts? @relation("transactions_accounts_account_to_idToaccounts", fields: [accounts_account_to_id], references: [account_id], onDelete: NoAction, onUpdate: NoAction, map: "transactions_accounts_account_id_fk") + accounts_transactions_accounts_account_from_idToaccounts accounts? @relation("transactions_accounts_account_from_idToaccounts", fields: [accounts_account_from_id], references: [account_id], onDelete: NoAction, onUpdate: NoAction, map: "transactions_accounts_account_id_fk2") + entities entities? @relation(fields: [entities_entity_id], references: [entity_id], onUpdate: NoAction, map: "transactions_ibfk_1") + categories categories? @relation(fields: [categories_category_id], references: [category_id], onUpdate: NoAction, map: "transactions_ibfk_2") + + @@index([accounts_account_from_id, accounts_account_to_id], map: "fk_transactions_accounts1_idx") @@index([categories_category_id], map: "fk_transactions_categories1_idx") @@index([entities_entity_id], map: "fk_transactions_entities2_idx") + @@index([accounts_account_to_id], map: "transactions_accounts_account_to_id_index") } model users { - user_id BigInt @id @default(autoincrement()) - username String @unique(map: "username_UNIQUE") @db.VarChar(45) - password String @db.MediumText - email String @unique(map: "email_UNIQUE") @db.VarChar(45) - sessionkey String? @db.MediumText - sessionkey_mobile String? @db.MediumText - trustlimit Int? - trustlimit_mobile Int? - last_update_timestamp BigInt @default(0) + user_id BigInt @id @default(autoincrement()) + username String @unique(map: "username_UNIQUE") @db.VarChar(45) + password String @db.MediumText + email String @unique(map: "email_UNIQUE") @db.VarChar(45) + sessionkey String? @db.MediumText + sessionkey_mobile String? @db.MediumText + trustlimit Int? + trustlimit_mobile Int? + last_update_timestamp BigInt @default(0) + accounts accounts[] + budgets budgets[] + budgets_has_categories budgets_has_categories[] + categories categories[] + entities entities[] + invest_desired_allocations invest_desired_allocations[] + rules rules[] + tags tags[] +} + +model tags { + tag_id BigInt @id @default(autoincrement()) + description String? @db.LongText + name String @db.VarChar(255) + users_user_id BigInt + users users @relation(fields: [users_user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction, map: "fk_users_user_id") + transaction_has_tags transaction_has_tags[] + + @@unique([name, users_user_id], map: "uq_name") + @@index([users_user_id], map: "fk_users_user_id") +} + +model transaction_has_tags { + transactions_transaction_id BigInt + tags_tag_id BigInt + tags tags @relation(fields: [tags_tag_id], references: [tag_id], onDelete: NoAction, onUpdate: NoAction, map: "transaction_has_tags_tags_tag_id_fk") + transactions transactions @relation(fields: [transactions_transaction_id], references: [transaction_id], onDelete: NoAction, onUpdate: NoAction, map: "transaction_has_tags_transactions_transaction_id_fk") + + @@id([tags_tag_id, transactions_transaction_id]) + @@index([transactions_transaction_id], map: "transaction_has_tags_transactions_transaction_id_fk") } enum invest_transactions_type { diff --git a/src/config/prisma.ts b/src/config/prisma.ts index b4736b0..5e72f3b 100644 --- a/src/config/prisma.ts +++ b/src/config/prisma.ts @@ -1,5 +1,5 @@ -import { Prisma, PrismaClient } from "@prisma/client"; -import { DefaultArgs } from "@prisma/client/runtime/library.js"; +import { Prisma, PrismaClient } from '@prisma/client'; +import { DefaultArgs } from '@prisma/client/runtime/library.js'; // Fix for BigInt not being serializable // eslint-disable-next-line no-extend-native @@ -26,7 +26,7 @@ export const performDatabaseRequest = async ( transactionConfig ); } - return transactionBody(prisma); + return transactionBody(prismaClient); }; export default { diff --git a/src/controllers/statsController.ts b/src/controllers/statsController.ts index a338ff9..c4545d6 100644 --- a/src/controllers/statsController.ts +++ b/src/controllers/statsController.ts @@ -57,11 +57,12 @@ const getCategoryExpensesEvoSchema = joi .object({ cat_id: joi.number(), ent_id: joi.number(), + tag_id: joi.number(), }) - .xor('cat_id', 'ent_id') + .xor('cat_id', 'ent_id', 'tag_id') .unknown(true); -const getCategoryEntityExpensesEvolution = async ( +const getCategoryEntityTagExpensesEvolution = async ( req: Request, res: Response, next: NextFunction @@ -72,8 +73,10 @@ const getCategoryEntityExpensesEvolution = async ( let data = undefined; if (input.cat_id) { data = await StatsService.getCategoryExpensesEvolution(sessionData.userId, input.cat_id); - } else { + } else if(input.ent_id) { data = await StatsService.getEntityExpensesEvolution(sessionData.userId, input.ent_id); + } else { + data = await StatsService.getTagExpensesEvolution(sessionData.userId, input.tag_id); } res.json(data); } catch (err) { @@ -86,11 +89,12 @@ const getCategoryIncomeEvoSchema = joi .object({ cat_id: joi.number(), ent_id: joi.number(), + tag_id: joi.number(), }) - .xor('cat_id', 'ent_id') + .xor('cat_id', 'ent_id', 'tag_id') .unknown(true); -const getCategoryEntityIncomeEvolution = async ( +const getCategoryEntityTagIncomeEvolution = async ( req: Request, res: Response, next: NextFunction @@ -101,8 +105,10 @@ const getCategoryEntityIncomeEvolution = async ( let data; if (input.cat_id) { data = await StatsService.getCategoryIncomeEvolution(sessionData.userId, input.cat_id); - } else { + } else if(input.ent_id) { data = await StatsService.getEntityIncomeEvolution(sessionData.userId, input.ent_id); + } else { + data = await StatsService.getTagIncomeEvolution(sessionData.userId, input.tag_id); } res.json(data); } catch (err) { @@ -140,7 +146,7 @@ export default { getExpensesIncomeDistributionForMonth, getUserCounterStats, getMonthlyPatrimonyProjections, - getCategoryEntityExpensesEvolution, - getCategoryEntityIncomeEvolution, + getCategoryEntityTagExpensesEvolution, + getCategoryEntityTagIncomeEvolution, getYearByYearIncomeExpenseDistribution, }; \ No newline at end of file diff --git a/src/controllers/tagController.ts b/src/controllers/tagController.ts new file mode 100644 index 0000000..209b34f --- /dev/null +++ b/src/controllers/tagController.ts @@ -0,0 +1,102 @@ +// READ +import CommonsController from './commonsController.js'; +import Logger from '../utils/Logger.js'; +import APIError from '../errorHandling/apiError.js'; +import { NextFunction, Request, Response } from 'express'; +import TagService from '../services/tagService.js'; +import joi from 'joi'; +import { MYFIN } from '../consts.js'; + +const getFilteredTagsByPageSchema = joi + .object({ + page_size: joi.number().default(MYFIN.DEFAULT_TRANSACTIONS_FETCH_LIMIT).min(1).max(300), + query: joi.string().empty('').default(''), + }) + .unknown(true); + +const getAllTagsForUser = async (req: Request, res: Response, next: NextFunction) => { + try { + const sessionData = await CommonsController.checkAuthSessionValidity(req); + const input = await getFilteredTagsByPageSchema.validateAsync(req.query); + const list = await TagService.getFilteredTagsForUserByPage( + sessionData.userId, + Number(req.params.page), + input.page_size, + input.query + ); + + res.json(list); + } catch (err) { + Logger.addLog(err); + next(err || APIError.internalServerError()); + } +}; + +// CREATE + +const createTagSchema = joi.object({ + name: joi.string().trim().required(), + description: joi.string().trim().optional().allow(""), +}); + +const createTag = async (req: Request, res: Response, next: NextFunction) => { + try { + const sessionData = await CommonsController.checkAuthSessionValidity(req); + const input = await createTagSchema.validateAsync(req.body); + await TagService.createTag({ + users_user_id: sessionData.userId, + name: input.name, + description: input.description, + }); + res.json('Tag successfully created!'); + } catch (err) { + Logger.addLog(err); + next(err || APIError.internalServerError()); + } +}; + +// DELETE + +const deleteTag = async (req: Request, res: Response, next: NextFunction) => { + try { + const sessionData = await CommonsController.checkAuthSessionValidity(req); + const tagId = req.params.id; + + await TagService.deleteTag(sessionData.userId, BigInt(tagId)); + + res.json('Tag successfully deleted!'); + } catch (err) { + Logger.addLog(err); + next(err || APIError.internalServerError()); + } +}; + +// UPDATE +const updateTagSchema = joi.object({ + new_name: joi.string().trim().required(), + new_description: joi.string().trim().optional().allow(""), +}); + +const updateTag = async (req: Request, res: Response, next: NextFunction) => { + try { + const sessionData = await CommonsController.checkAuthSessionValidity(req); + const input = await updateTagSchema.validateAsync(req.body); + const tagId = req.params.id; + + await TagService.updateTag(sessionData.userId, BigInt(tagId), { + name: input.new_name, + description: input.new_description, + }); + res.json('Tag successfully updated!'); + } catch (err) { + Logger.addLog(err); + next(err || APIError.internalServerError()); + } +}; + +export default { + getAllTagsForUser, + createTag, + deleteTag, + updateTag, +}; diff --git a/src/controllers/transactionController.ts b/src/controllers/transactionController.ts index c819d7b..8f016f9 100644 --- a/src/controllers/transactionController.ts +++ b/src/controllers/transactionController.ts @@ -1,9 +1,9 @@ -import {NextFunction, Request, Response} from 'express'; +import { NextFunction, Request, Response } from 'express'; import APIError from '../errorHandling/apiError.js'; import Logger from '../utils/Logger.js'; import CommonsController from './commonsController.js'; import TransactionService from '../services/transactionService.js'; -import {MYFIN} from '../consts.js'; +import { MYFIN } from '../consts.js'; import joi from 'joi'; import AccountService from '../services/accountService.js'; import CategoryService from '../services/categoryService.js'; @@ -78,13 +78,17 @@ const createTransactionSchema = joi.object({ category_id: joi.number().empty(''), date_timestamp: joi.number().required(), is_essential: joi.boolean().required(), + tags: joi.any().optional(), // ex: ["tag 1","tag 2",<_new_or_existing_tag_name>] }); const createTransaction = async (req, res, next) => { try { const sessionData = await CommonsController.checkAuthSessionValidity(req); const trx = await createTransactionSchema.validateAsync(req.body); - await TransactionService.createTransaction(sessionData.userId, trx); + await TransactionService.createTransaction(sessionData.userId, { + ...trx, + tags: JSON.parse(trx.tags), + }); res.json(`Transaction successfully created`); } catch (err) { Logger.addLog(err); @@ -104,6 +108,7 @@ const updateTransactionSchema = joi.object({ new_date_timestamp: joi.number().required(), new_is_essential: joi.boolean().required(), transaction_id: joi.number().required(), + tags: joi.any().optional(), // ex: ["tag 1","tag 2",<_new_or_existing_tag_name>] /* SPLIT TRX */ is_split: joi.boolean().default(false), split_amount: joi.number().empty('').optional(), @@ -114,13 +119,18 @@ const updateTransactionSchema = joi.object({ split_account_to: joi.number().empty('').optional(), split_description: joi.string().empty('').trim().optional(), split_is_essential: joi.boolean().empty('').default(false).optional(), + split_tags: joi.any().optional(), // ex: ["tag 1","tag 2",<_new_or_existing_tag_name>] }); const updateTransaction = async (req, res, next) => { try { const sessionData = await CommonsController.checkAuthSessionValidity(req); const trx = await updateTransactionSchema.validateAsync(req.body); - await TransactionService.updateTransaction(sessionData.userId, trx); + await TransactionService.updateTransaction(sessionData.userId, { + ...trx, + tags: JSON.parse(trx.tags), + split_tags: JSON.parse(trx.tags), + }); res.json(`Transaction successfully updated`); } catch (err) { Logger.addLog(err); diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index bc005ff..5071574 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -84,10 +84,10 @@ const changeUserPassword = async (req: Request, res: Response, next: NextFunctio } }; -const getUserCategoriesAndEntities = async (req: Request, res: Response, next: NextFunction) => { +const getUserCategoriesEntitiesTags = async (req: Request, res: Response, next: NextFunction) => { try { const sessionData = await CommonsController.checkAuthSessionValidity(req); - const data = await UserService.getUserCategoriesAndEntities(sessionData.userId); + const data = await UserService.getUserCategoriesEntitiesTags(sessionData.userId); res.json(data); } catch (err) { Logger.addLog(err); @@ -111,6 +111,6 @@ export default { attemptLogin, checkSessionValidity, changeUserPassword, - getUserCategoriesAndEntities, + getUserCategoriesAndEntities: getUserCategoriesEntitiesTags, autoPopulateDemoData, }; diff --git a/src/routes/router.ts b/src/routes/router.ts index 842aeaa..8115a85 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -10,25 +10,28 @@ import StatsController from '../controllers/statsController.js'; import InvestAssetsController from '../controllers/investAssetsController.js'; import InvestTransactionsController from '../controllers/investTransactionsController.js'; import { Express } from 'express-serve-static-core'; +import TagController from "../controllers/tagController.js"; const router = (app: Express) => { - // USERS ROUTES + //region USERS ROUTES const userRouter = express.Router(); - userRouter.get('/categoriesAndEntities', UserController.getUserCategoriesAndEntities); + userRouter.get('/categoriesEntitiesTags', UserController.getUserCategoriesAndEntities); const usersRouter = express.Router(); usersRouter.post('/', UserController.createOne); usersRouter.put('/changePW/', UserController.changeUserPassword); usersRouter.post('/demo/', UserController.autoPopulateDemoData); + //endregion - // AUTH ROUTES + //region AUTH ROUTES const authRoutes = express.Router(); authRoutes.post('/', UserController.attemptLogin); const validityRoutes = express.Router(); validityRoutes.post('/', UserController.checkSessionValidity); + //endregion - // ACCOUNTS ROUTES + //region ACCOUNTS ROUTES const accountsRoutes = express.Router(); accountsRoutes.post('/', AccountController.createAccount); accountsRoutes.get('/', AccountController.getAllAccountsForUser); @@ -39,8 +42,9 @@ const router = (app: Express) => { '/recalculate-balance/all', AccountController.recalculateAllUserAccountsBalances ); + //endregion - // BUDGETS ROUTES + //region BUDGETS ROUTES const budgetRoutes = express.Router(); budgetRoutes.get('/', BudgetController.getAllBudgetsForUser); budgetRoutes.get('/filteredByPage/:page', BudgetController.getFilteredBudgetsForUserByPage); @@ -52,29 +56,33 @@ const router = (app: Express) => { budgetRoutes.delete('/', BudgetController.removeBudget); budgetRoutes.get('/list/summary', BudgetController.getBudgetsListForUser); budgetRoutes.put('/:id', BudgetController.updateBudgetCategoryPlannedValues); + //endregion - // CATEGORIES ROUTES + //region CATEGORIES ROUTES const catRoutes = express.Router(); catRoutes.get('/', CategoryController.getAllCategoriesForUser); catRoutes.post('/', CategoryController.createCategory); catRoutes.delete('/', CategoryController.deleteCategory); catRoutes.put('/', CategoryController.updateCategory); + //endregion - // ENTITIES ROUTES + //region ENTITIES ROUTES const entityRoutes = express.Router(); entityRoutes.get('/', EntityController.getAllEntitiesForUser); entityRoutes.post('/', EntityController.createEntity); entityRoutes.delete('/', EntityController.deleteEntity); entityRoutes.put('/', EntityController.updateEntity); + //endregion - // RULES ROUTES + //region RULES ROUTES const ruleRoutes = express.Router(); ruleRoutes.get('/', RuleController.getAllRulesForUser); ruleRoutes.post('/', RuleController.createRule); ruleRoutes.delete('/', RuleController.deleteRule); ruleRoutes.put('/', RuleController.updateRule); + //endregion - // STATS ROUTES + //region STATS ROUTES const statRoutes = express.Router(); statRoutes.get( '/dashboard/month-expenses-income-distribution', @@ -87,15 +95,16 @@ const router = (app: Express) => { statRoutes.get('/userStats', StatsController.getUserCounterStats); statRoutes.get( '/category-expenses-evolution', - StatsController.getCategoryEntityExpensesEvolution + StatsController.getCategoryEntityTagExpensesEvolution ); - statRoutes.get('/category-income-evolution', StatsController.getCategoryEntityIncomeEvolution); + statRoutes.get('/category-income-evolution', StatsController.getCategoryEntityTagIncomeEvolution); statRoutes.get( '/year-by-year-income-expense-distribution', StatsController.getYearByYearIncomeExpenseDistribution ); + //endregion - // TRANSACTIONS ROUTES + //region TRANSACTIONS ROUTES const trxRoutes = express.Router(); trxRoutes.get('/', TransactionController.getTransactionsForUser); trxRoutes.get('/filteredByPage/:page', TransactionController.getFilteredTrxByPage); @@ -111,8 +120,9 @@ const router = (app: Express) => { trxRoutes.post('/import/step0', TransactionController.importTransactionsStep0); trxRoutes.post('/import/step1', TransactionController.importTransactionsStep1); trxRoutes.post('/import/step2', TransactionController.importTransactionsStep2); + //endregion - // INVEST ASSET ROUTES + //region INVEST ASSET ROUTES const investAssetRoutes = express.Router(); investAssetRoutes.get('/', InvestAssetsController.getAllAssetsForUser); investAssetRoutes.post('/', InvestAssetsController.createAsset); @@ -122,14 +132,24 @@ const router = (app: Express) => { investAssetRoutes.get('/summary', InvestAssetsController.getAllAssetsSummaryForUser); investAssetRoutes.get('/stats', InvestAssetsController.getAssetStatsForUser); investAssetRoutes.get('/:id', InvestAssetsController.getAssetDetailsForUser); + //endregion - // INVEST TRANSACTION ROUTES + //region INVEST TRANSACTION ROUTES const investTrxRoutes = express.Router(); investTrxRoutes.get('/', InvestTransactionsController.getAllTransactionsForUser); investTrxRoutes.get('/filteredByPage/:page', InvestTransactionsController.getFilteredTrxByPage); investTrxRoutes.post('/', InvestTransactionsController.createTransaction); investTrxRoutes.delete('/:id', InvestTransactionsController.deleteTransaction); investTrxRoutes.put('/:id', InvestTransactionsController.updateTransaction); + //endregion + + //region TAG ROUTES + const tagRoutes = express.Router(); + tagRoutes.get('/filteredByPage/:page', TagController.getAllTagsForUser); + tagRoutes.post('/', TagController.createTag); + tagRoutes.delete('/:id', TagController.deleteTag); + tagRoutes.put('/:id', TagController.updateTag); + //endregion app.use('/users', usersRouter); app.use('/user', userRouter); @@ -144,6 +164,7 @@ const router = (app: Express) => { app.use('/stats', statRoutes); app.use('/invest/assets', investAssetRoutes); app.use('/invest/trx', investTrxRoutes); + app.use('/tags', tagRoutes); }; export default router; diff --git a/src/services/accountService.ts b/src/services/accountService.ts index 0939238..fb446eb 100644 --- a/src/services/accountService.ts +++ b/src/services/accountService.ts @@ -3,6 +3,7 @@ import ConvertUtils from '../utils/convertUtils.js'; import {MYFIN} from '../consts.js'; import DateTimeUtils from '../utils/DateTimeUtils.js'; import UserService from './userService.js'; +import {Prisma} from "@prisma/client"; const Account = prisma.accounts; const Transaction = prisma.transactions; @@ -66,7 +67,7 @@ const accountService = { data: accountObj, }); }, - getAccountsForUser: async (userId: bigint, selectConfig = undefined, dbClient = prisma) => + getAccountsForUser: async (userId: bigint, selectConfig: Prisma.accountsSelect = undefined, dbClient = prisma) => dbClient.accounts.findMany({ where: { users_user_id: userId, diff --git a/src/services/categoryService.ts b/src/services/categoryService.ts index 843eabd..9bf9594 100644 --- a/src/services/categoryService.ts +++ b/src/services/categoryService.ts @@ -8,6 +8,17 @@ import ConvertUtils from "../utils/convertUtils.js"; const BudgetHasCategories = prisma.budgets_has_categories; +interface Category { + category_id?: bigint; + name?: string; + description?: string; + color_gradient?: string; + status?: string; + type?: string; + exclude_from_budgets?: number; + users_user_id?: bigint; +} + /** * Fetches all categories associated with ***userId***. * @param userId - user id @@ -23,8 +34,18 @@ const getAllCategoriesForUser = async ( where: {users_user_id: userId}, select: selectAttributes, }); -const createCategory = async (category: Prisma.categoriesCreateInput, dbClient = prisma) => { - return dbClient.categories.create({data: category}); +const createCategory = async (category: Category, dbClient = prisma) => { + return dbClient.categories.create({ + data: { + name: category.name, + description: category.description, + color_gradient: category.color_gradient, + status: category.status, + exclude_from_budgets: category.exclude_from_budgets, + type: category.type, + users_user_id: category.users_user_id, + } + }); }; const deleteCategory = async (userId: bigint, categoryId: number, dbClient = prisma) => { diff --git a/src/services/entityService.ts b/src/services/entityService.ts index fa9aeaf..4b9e5ec 100644 --- a/src/services/entityService.ts +++ b/src/services/entityService.ts @@ -1,94 +1,102 @@ -import { prisma } from "../config/prisma.js"; -import { Prisma } from "@prisma/client"; +import {prisma} from "../config/prisma.js"; +import {Prisma} from "@prisma/client"; import DateTimeUtils from "../utils/DateTimeUtils.js"; +interface Entity { + entity_id?: bigint; + name?: string; + users_user_id?: bigint; +} + /** - * Fetches all categories associated with ***userId***. + * Fetches all entities associated with ***userId***. * @param userId - user id - * @param selectAttributes - category attributes to be returned. *Undefined* will return them all. + * @param selectAttributes - entity attributes to be returned. *Undefined* will return them all. * @param dbClient - the db client */ const getAllEntitiesForUser = async ( - userId: bigint, - selectAttributes = undefined, - dbClient = prisma + userId: bigint, + selectAttributes = undefined, + dbClient = prisma ): Promise> => - dbClient.entities.findMany({ - where: { users_user_id: userId }, - select: selectAttributes, - }); + dbClient.entities.findMany({ + where: {users_user_id: userId}, + select: selectAttributes, + }); -const createEntity = async (entity: Prisma.entitiesCreateInput, dbClient = prisma) => { - return dbClient.entities.create({ data: entity }); +const createEntity = async (entity: Entity, dbClient = prisma) => { + return dbClient.entities.create({ + data: {name: entity.name, users_user_id: entity.users_user_id} + }); }; const deleteEntity = async (userId: bigint, entityId: number, dbClient = prisma) => - dbClient.entities.delete({ - where: { - users_user_id: userId, - entity_id: entityId, - }, - }); + dbClient.entities.delete({ + where: { + users_user_id: userId, + entity_id: entityId, + }, + }); const updateEntity = async (userId: bigint, entityId: number, entity: Prisma.entitiesUpdateInput, dbClient = prisma) => - dbClient.entities.update({ - where: { - users_user_id: userId, - entity_id: entityId, - }, - data: { name: entity.name }, - }); + dbClient.entities.update({ + where: { + users_user_id: userId, + entity_id: entityId, + }, + data: {name: entity.name}, + }); const getCountOfUserEntities = async (userId, dbClient = prisma) => - dbClient.entities.count({ - where: { users_user_id: userId }, - }); + dbClient.entities.count({ + where: {users_user_id: userId}, + }); const buildSqlForExcludedAccountsList = (excludedAccs) => { - if (!excludedAccs || excludedAccs.length === 0) { - return ' 1 == 1'; - } - let sql = ' ('; - for (let cnt = 0; cnt < excludedAccs.length; cnt++) { - const acc = excludedAccs[cnt].account_id; - sql += ` '${acc}' `; - - if (cnt !== excludedAccs.length - 1) { - sql += ', '; + if (!excludedAccs || excludedAccs.length === 0) { + return ' 1 == 1'; + } + let sql = ' ('; + for (let cnt = 0; cnt < excludedAccs.length; cnt++) { + const acc = excludedAccs[cnt].account_id; + sql += ` '${acc}' `; + + if (cnt !== excludedAccs.length - 1) { + sql += ', '; + } } - } - sql += ') '; - return sql; + sql += ') '; + return sql; }; const getAmountForEntityInPeriod = async ( - entityId: number | bigint, - fromDate: number, - toDate: number, - includeTransfers = true, - dbClient = prisma + entityId: number | bigint, + fromDate: number, + toDate: number, + includeTransfers = true, + dbClient = prisma ): Promise<{ entity_balance_credit: number; entity_balance_debit: number }> => { - /* Logger.addLog( - `Entity: ${entityId} | fromDate: ${fromDate} | toDate: ${toDate} | includeTransfers: ${includeTransfers}`); */ - let accsExclusionSqlExcerptAccountsTo = ''; - let accsExclusionSqlExcerptAccountsFrom = ''; - let accountsToExcludeListInSQL = ''; - - const listOfAccountsToExclude = await dbClient.accounts.findMany({ - where: { exclude_from_budgets: true }, - }); - if (!listOfAccountsToExclude || listOfAccountsToExclude.length < 1) { - accsExclusionSqlExcerptAccountsTo = ' 1 = 1 '; - accsExclusionSqlExcerptAccountsFrom = ' 1 = 1 '; - } else { - accountsToExcludeListInSQL = buildSqlForExcludedAccountsList(listOfAccountsToExclude); - accsExclusionSqlExcerptAccountsTo = `accounts_account_to_id NOT IN ${accountsToExcludeListInSQL} `; - accsExclusionSqlExcerptAccountsFrom = `accounts_account_from_id NOT IN ${accountsToExcludeListInSQL} `; - } - - if (includeTransfers) { - return dbClient.$queryRaw`SELECT sum(if(type = 'I' OR + /* Logger.addLog( + `Entity: ${entityId} | fromDate: ${fromDate} | toDate: ${toDate} | includeTransfers: ${includeTransfers}`); */ + let accsExclusionSqlExcerptAccountsTo = ''; + let accsExclusionSqlExcerptAccountsFrom = ''; + let accountsToExcludeListInSQL = ''; + + const listOfAccountsToExclude = await dbClient.accounts.findMany({ + where: {exclude_from_budgets: true}, + }); + if (!listOfAccountsToExclude || listOfAccountsToExclude.length < 1) { + accsExclusionSqlExcerptAccountsTo = ' 1 = 1 '; + accsExclusionSqlExcerptAccountsFrom = ' 1 = 1 '; + } else { + accountsToExcludeListInSQL = buildSqlForExcludedAccountsList(listOfAccountsToExclude); + accsExclusionSqlExcerptAccountsTo = `accounts_account_to_id NOT IN ${accountsToExcludeListInSQL} `; + accsExclusionSqlExcerptAccountsFrom = `accounts_account_from_id NOT IN ${accountsToExcludeListInSQL} `; + } + + if (includeTransfers) { + return dbClient.$queryRaw`SELECT sum(if(type = 'I' OR (type = 'T' AND ${accsExclusionSqlExcerptAccountsTo}), amount, 0)) as 'entity_balance_credit', @@ -99,9 +107,9 @@ const getAmountForEntityInPeriod = async ( FROM transactions WHERE date_timestamp between ${fromDate} AND ${toDate} AND entities_entity_id = ${entityId} `; - } + } - return dbClient.$queryRaw`SELECT sum(if(type = 'I', amount, 0)) as 'entity_balance_credit', + return dbClient.$queryRaw`SELECT sum(if(type = 'I', amount, 0)) as 'entity_balance_credit', sum(if(type = 'E', amount, 0)) as 'entity_balance_debit' FROM transactions WHERE date_timestamp between ${fromDate} AND ${toDate} @@ -109,39 +117,60 @@ const getAmountForEntityInPeriod = async ( }; export interface CalculatedEntityAmounts { - entity_balance_credit: number; - entity_balance_debit: number; + entity_balance_credit: number; + entity_balance_debit: number; } const getAmountForEntityInMonth = async ( - entityId: bigint, - month: number, - year: number, - includeTransfers = true, - dbClient = prisma + entityId: bigint, + month: number, + year: number, + includeTransfers = true, + dbClient = prisma +): Promise => { + const nextMonth = month < 12 ? month + 1 : 1; + const nextMonthsYear = month < 12 ? year : year + 1; + const maxDate = DateTimeUtils.getUnixTimestampFromDate( + new Date(nextMonthsYear, nextMonth - 1, 1) + ); + const minDate = DateTimeUtils.getUnixTimestampFromDate(new Date(year, month - 1, 1)); + /* Logger.addLog(`cat id: ${categoryId} | month: ${month} | year: ${year} | minDate: ${minDate} | maxDate: ${maxDate}`); */ + const amounts = await getAmountForEntityInPeriod( + entityId, + minDate, + maxDate, + includeTransfers, + dbClient + ); + return amounts[0]; +}; + +const getAmountForEntityInYear = async ( + entityId: bigint, + year: number, + includeTransfers = true, + dbClient = prisma ): Promise => { - const nextMonth = month < 12 ? month + 1 : 1; - const nextMonthsYear = month < 12 ? year : year + 1; - const maxDate = DateTimeUtils.getUnixTimestampFromDate( - new Date(nextMonthsYear, nextMonth - 1, 1) - ); - const minDate = DateTimeUtils.getUnixTimestampFromDate(new Date(year, month - 1, 1)); - /* Logger.addLog(`cat id: ${categoryId} | month: ${month} | year: ${year} | minDate: ${minDate} | maxDate: ${maxDate}`); */ - const amounts = await getAmountForEntityInPeriod( - entityId, - minDate, - maxDate, - includeTransfers, - dbClient - ); - return amounts[0]; + const maxDate = DateTimeUtils.getUnixTimestampFromDate(new Date(year, 11, 31)); + const minDate = DateTimeUtils.getUnixTimestampFromDate(new Date(year, 0, 1)); + + /* Logger.addLog(`cat id: ${categoryId} | month: ${month} | year: ${year} | minDate: ${minDate} | maxDate: ${maxDate}`); */ + const amounts = await getAmountForEntityInPeriod( + entityId, + minDate, + maxDate, + includeTransfers, + dbClient + ); + return amounts[0]; }; export default { - getAllEntitiesForUser, - createEntity, - deleteEntity, - updateEntity, - getCountOfUserEntities, - getAmountForEntityInMonth, + getAllEntitiesForUser, + createEntity, + deleteEntity, + updateEntity, + getCountOfUserEntities, + getAmountForEntityInMonth, + getAmountForEntityInYear, }; diff --git a/src/services/investAssetService.ts b/src/services/investAssetService.ts index bbdcce0..d942984 100644 --- a/src/services/investAssetService.ts +++ b/src/services/investAssetService.ts @@ -39,12 +39,24 @@ interface CalculatedAssetStats extends CalculatedAssetAmounts { combined_roi_by_year?: number; } +interface InvestAssetEvoSnapshot { + month: number; + year: number; + units: number | string; + invested_amount: bigint | number; + current_value: bigint | number; + invest_assets_asset_id: bigint | number; + created_at: bigint | number; + updated_at: bigint | number; + withdrawn_amount: bigint | number; +} + const getLatestSnapshotForAsset = async ( assetId: bigint, maxMonth = DateTimeUtils.getMonthNumberFromTimestamp(), maxYear = DateTimeUtils.getYearFromTimestamp(), dbClient = prisma -): Promise => { +): Promise => { const result = await dbClient.$queryRaw`SELECT * FROM invest_asset_evo_snapshot WHERE invest_assets_asset_id = ${assetId} diff --git a/src/services/ruleService.ts b/src/services/ruleService.ts index bb1de26..5e9d6ac 100644 --- a/src/services/ruleService.ts +++ b/src/services/ruleService.ts @@ -7,10 +7,29 @@ import CategoryService from './categoryService.js'; import {Prisma} from '@prisma/client'; import {MYFIN} from '../consts.js'; -const Rule = prisma.rules; +interface Rule { + rule_id?: bigint | number; + matcher_description_operator?: string | null; + matcher_description_value?: string | null; + matcher_amount_operator?: string | null; + matcher_amount_value?: bigint | number | null; + matcher_type_operator?: string | null; + matcher_type_value?: string | null; + matcher_account_to_id_operator?: string | null; + matcher_account_to_id_value?: bigint | number | null; + matcher_account_from_id_operator?: string | null; + matcher_account_from_id_value?: bigint | number | null; + assign_category_id?: bigint | number | null; + assign_entity_id?: bigint | number | null; + assign_account_to_id?: bigint | number | null; + assign_account_from_id?: bigint | number | null; + assign_type?: string | null; + assign_is_essential?: boolean; + users_user_id?: bigint; +} const getAllRulesForUser = async (userId: bigint) => { - const rules = await Rule.findMany({ + const rules = await prisma.rules.findMany({ where: {users_user_id: userId} }); @@ -34,7 +53,7 @@ const getAllRulesForUser = async (userId: bigint) => { }; }; -const createRule = async (userId: bigint, rule: Prisma.rulesCreateInput, dbClient = prisma) => +const createRule = async (userId: bigint, rule: Rule, dbClient = prisma) => dbClient.rules.create({ data: { users_user_id: userId, @@ -64,7 +83,7 @@ const updatedRule = async (rule: Prisma.rulesUpdateInput, dbClient = prisma) => where: { rule_id_users_user_id: { rule_id: Number(rule.rule_id), - users_user_id: Number(rule.users_user_id) + users_user_id: Number(rule.users.connect.user_id) } }, data: { @@ -90,7 +109,7 @@ const updatedRule = async (rule: Prisma.rulesUpdateInput, dbClient = prisma) => }; const deleteRule = async (userId: bigint, ruleId: bigint) => - Rule.delete({ + prisma.rules.delete({ where: { rule_id_users_user_id: { rule_id: ruleId, @@ -103,8 +122,6 @@ const getCountOfUserRules = async (userId, dbClient = prisma) => dbClient.rules. where: {users_user_id: userId} }); -type Rule = Prisma.rulesUpdateInput - enum RuleMatcherResult { MATCHED = 0, FAILED = 1, diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 6570a85..1992d1d 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -9,6 +9,7 @@ import AccountService from './accountService.js'; import BudgetService, { BudgetListOrder } from './budgetService.js'; import RuleService from './ruleService.js'; import DateTimeUtils from '../utils/DateTimeUtils.js'; +import TagService, { CalculatedTagAmounts } from "./tagService.js"; const getExpensesIncomeDistributionForMonth = async ( userId: bigint, @@ -86,13 +87,14 @@ export interface UserCounterStats { nr_of_accounts: number; nr_of_budgets: number; nr_of_rules: number; + nr_of_tags: number; } const getUserCounterStats = async ( userId: bigint, dbClient = prisma ): Promise => { - const [trxCount, entityCount, categoryCount, accountCount, budgetCount, ruleCount] = + const [trxCount, entityCount, categoryCount, accountCount, budgetCount, ruleCount, tagCount] = await Promise.all([ TransactionService.getCountOfUserTransactions(userId), CategoryService.getCountOfUserCategories(userId, dbClient), @@ -100,6 +102,7 @@ const getUserCounterStats = async ( AccountService.getCountOfUserAccounts(userId, dbClient), BudgetService.getCountOfUserBudgets(userId, dbClient), RuleService.getCountOfUserRules(userId, dbClient), + TagService.getCountOfUserTags(userId, dbClient), ]); return { @@ -109,6 +112,7 @@ const getUserCounterStats = async ( nr_of_accounts: accountCount as number, nr_of_budgets: budgetCount as number, nr_of_rules: ruleCount as number, + nr_of_tags: tagCount as number, }; }; @@ -263,6 +267,66 @@ const getEntityExpensesEvolution = async (userId: bigint, entityId: bigint, dbCl })); }, dbClient); +const getTagExpensesEvolution = async (userId: bigint, tagId: bigint, dbClient = undefined) => + performDatabaseRequest(async (prismaTx) => { + const currentMonth = DateTimeUtils.getMonthNumberFromTimestamp(); + const currentYear = DateTimeUtils.getYearFromTimestamp(); + const budgetsList = await BudgetService.getBudgetsUntilCertainMonth( + userId, + currentMonth, + currentYear, + BudgetListOrder.DESCENDING, + prismaTx + ); + + const calculatedAmountPromises = []; + for (const budget of budgetsList) { + calculatedAmountPromises.push( + TagService.getAmountForTagInMonth(tagId, budget.month, budget.year, true, prismaTx) + ); + } + const calculatedAmounts: Array = await Promise.all( + calculatedAmountPromises + ); + return calculatedAmounts.map((calculatedAmount, index) => ({ + value: ConvertUtils.convertBigIntegerToFloat( + BigInt(calculatedAmount.tag_balance_debit ?? 0) + ), + month: budgetsList[index].month, + year: budgetsList[index].year, + })); + }, dbClient); + +const getTagIncomeEvolution = async (userId: bigint, tagId: bigint, dbClient = undefined) => + performDatabaseRequest(async (prismaTx) => { + const currentMonth = DateTimeUtils.getMonthNumberFromTimestamp(); + const currentYear = DateTimeUtils.getYearFromTimestamp(); + const budgetsList = await BudgetService.getBudgetsUntilCertainMonth( + userId, + currentMonth, + currentYear, + BudgetListOrder.DESCENDING, + prismaTx + ); + + const calculatedAmountPromises = []; + for (const budget of budgetsList) { + calculatedAmountPromises.push( + TagService.getAmountForTagInMonth(tagId, budget.month, budget.year, true, prismaTx) + ); + } + const calculatedAmounts: Array = await Promise.all( + calculatedAmountPromises + ); + return calculatedAmounts.map((calculatedAmount, index) => ({ + value: ConvertUtils.convertBigIntegerToFloat( + BigInt(calculatedAmount.tag_balance_credit ?? 0) + ), + month: budgetsList[index].month, + year: budgetsList[index].year, + })); + }, dbClient); + const getCategoryIncomeEvolution = async ( userId: bigint, categoryId: bigint, @@ -334,10 +398,25 @@ interface ExpandedCategoryWithYearlyAmounts { category_yearly_income: number; category_yearly_expense: number; } +interface ExpandedEntityWithYearlyAmounts { + entity_id: bigint; + name: string; + entity_yearly_income: number; + entity_yearly_expense: number; +} +interface ExpandedTagWithYearlyAmounts { + tag_id: bigint; + name: string; + description?: string; + tag_yearly_income: number; + tag_yearly_expense: number; +} export interface YearByYearIncomeDistributionOutput { year_of_first_trx?: number; categories?: Array; + entities?: Array; + tags?: Array; } const getYearByYearIncomeExpenseDistribution = async ( @@ -352,6 +431,7 @@ const getYearByYearIncomeExpenseDistribution = async ( prismaTx ); + // Categories const categories = await CategoryService.getAllCategoriesForUser( userId, { @@ -362,9 +442,9 @@ const getYearByYearIncomeExpenseDistribution = async ( prismaTx ); - const calculatedAmountPromises = []; + const calculatedCategoryAmountPromises = []; for (const category of categories) { - calculatedAmountPromises.push( + calculatedCategoryAmountPromises.push( CategoryService.getAmountForCategoryInYear( category.category_id as bigint, year, @@ -374,7 +454,7 @@ const getYearByYearIncomeExpenseDistribution = async ( ); } - const calculatedAmounts = await Promise.all(calculatedAmountPromises); + const calculatedCategoryAmounts = await Promise.all(calculatedCategoryAmountPromises); output.categories = categories.map((category, index) => { return { @@ -382,10 +462,86 @@ const getYearByYearIncomeExpenseDistribution = async ( name: category.name as string, type: category.type as string, category_yearly_income: ConvertUtils.convertBigIntegerToFloat( - calculatedAmounts[index].category_balance_credit + calculatedCategoryAmounts[index].category_balance_credit ) as number, category_yearly_expense: ConvertUtils.convertBigIntegerToFloat( - calculatedAmounts[index].category_balance_debit + calculatedCategoryAmounts[index].category_balance_debit + ) as number, + }; + }); + + // Entities + const entities = await EntityService.getAllEntitiesForUser( + userId, + { + entity_id: true, + name: true, + }, + prismaTx + ); + + const calculatedEntityAmountPromises = []; + for (const entity of entities) { + calculatedEntityAmountPromises.push( + EntityService.getAmountForEntityInYear( + entity.entity_id as bigint, + year, + true, + prismaTx + ) + ); + } + + const calculatedEntityAmounts = await Promise.all(calculatedEntityAmountPromises); + + output.entities = entities.map((entity, index) => { + return { + entity_id: entity.entity_id as bigint, + name: entity.name as string, + entity_yearly_income: ConvertUtils.convertBigIntegerToFloat( + calculatedEntityAmounts[index].entity_balance_credit + ) as number, + entity_yearly_expense: ConvertUtils.convertBigIntegerToFloat( + calculatedEntityAmounts[index].entity_balance_debit + ) as number, + }; + }); + + // Tags + const tags = await TagService.getAllTagsForUser( + userId, + { + tag_id: true, + name: true, + description: true, + }, + prismaTx + ); + + const calculatedTagAmountPromises = []; + for (const tag of tags) { + calculatedTagAmountPromises.push( + TagService.getAmountForTagInYear( + tag.tag_id as bigint, + year, + true, + prismaTx + ) + ); + } + + const calculatedTagAmounts = await Promise.all(calculatedTagAmountPromises); + + output.tags = tags.map((tag, index) => { + return { + tag_id: tag.tag_id as bigint, + name: tag.name as string, + description: tag.description as string, + tag_yearly_income: ConvertUtils.convertBigIntegerToFloat( + calculatedTagAmounts[index].tag_balance_credit + ) as number, + tag_yearly_expense: ConvertUtils.convertBigIntegerToFloat( + calculatedTagAmounts[index].tag_balance_debit ) as number, }; }); @@ -402,4 +558,6 @@ export default { getCategoryIncomeEvolution, getEntityIncomeEvolution, getYearByYearIncomeExpenseDistribution, + getTagIncomeEvolution, + getTagExpensesEvolution, }; diff --git a/src/services/tagService.ts b/src/services/tagService.ts new file mode 100644 index 0000000..b3bb013 --- /dev/null +++ b/src/services/tagService.ts @@ -0,0 +1,336 @@ +import { performDatabaseRequest, prisma } from '../config/prisma.js'; +import TransactionService from './transactionService.js'; +import APIError from '../errorHandling/apiError.js'; +import Logger from '../utils/Logger.js'; +import { CalculatedEntityAmounts } from "./entityService.js"; +import DateTimeUtils from "../utils/DateTimeUtils.js"; + +export interface Tag { + tag_id?: bigint; + name?: string; + description?: string; + users_user_id?: bigint; +} + +/** + * Fetches all tags associated with ***userId***. + * @param userId - user id + * @param selectAttributes - tag attributes to be returned. *Undefined* will return them all. + * @param dbClient - the db client + */ +const getAllTagsForUser = async ( + userId: bigint, + selectAttributes = undefined, + dbClient = prisma +): Promise> => + dbClient.tags.findMany({ + where: { users_user_id: userId }, + select: selectAttributes, + }); + +/** + * Fetches all tags associated with ***userId*** w/ pagination. + * @param userId - user id + * @param page - page number + * @param pageSize - page size + * @param searchQuery - search query + * @param dbClient - the db client + */ +const getFilteredTagsForUserByPage = async ( + userId: bigint, + page: number, + pageSize: number, + searchQuery: string, + dbClient = prisma +) => { + const query = `%${searchQuery}%`; + const offsetValue = page * pageSize; + + // main query for list of results (limited by pageSize and offsetValue) + const mainQuery = dbClient.$queryRaw`SELECT tag_id, name, description + FROM tags + WHERE tags.users_user_id = ${userId} + AND (tags.description LIKE ${query} OR tags.name LIKE ${query}) + ORDER BY tags.name ASC + LIMIT ${pageSize} OFFSET ${offsetValue}`; + + // count of total of filtered results + const countQuery = dbClient.$queryRaw`SELECT count(*) as 'count' + FROM tags + WHERE tags.users_user_id = ${userId} + AND (tags.description LIKE ${query} OR tags.name LIKE ${query})`; + + const totalCountQuery = dbClient.$queryRaw`SELECT count(*) as 'count' + FROM tags + WHERE tags.users_user_id = ${userId}`; + + const [mainQueryResult, countQueryResult, totalCountQueryResult] = await prisma.$transaction([ + mainQuery, + countQuery, + totalCountQuery, + ]); + + return { + results: mainQueryResult, + filtered_count: countQueryResult[0].count, + total_count: totalCountQueryResult[0].count, + }; +}; + +const createTag = async (tag: Tag, dbClient = prisma) => + dbClient.tags.create({ + data: { + name: tag.name, + description: tag.description, + users_user_id: tag.users_user_id, + }, + }); + +const deleteTag = async (userId: bigint, tagId: bigint, dbClient = prisma) => { + return performDatabaseRequest(async (prismaTx) => { + await prismaTx.transaction_has_tags.deleteMany({ + where: { + tags_tag_id: tagId, + }, + }); + + return prismaTx.tags.delete({ + where: { + users_user_id: userId, + tag_id: tagId, + }, + }); + }); +}; + +const updateTag = async (userId: bigint, tagId: bigint, updatedTag: Tag, dbClient = prisma) => + dbClient.tags.update({ + where: { + users_user_id: userId, + tag_id: tagId, + }, + data: { + name: updatedTag.name, + description: updatedTag.description, + }, + }); + +const getCountOfUserTags = async (userId, dbClient = prisma) => + dbClient.tags.count({ + where: { users_user_id: userId }, + }); + +const deleteAllTagsFromTransaction = async (userId, transactionId, dbClient = prisma) => { + const transactionBelongsToUser = await TransactionService.doesTransactionBelongToUser( + userId, + transactionId, + dbClient + ); + + if (!transactionBelongsToUser) { + throw APIError.notAuthorized(); + } + + return dbClient.transaction_has_tags.deleteMany({ + where: { + transactions_transaction_id: transactionId, + }, + }); +}; + +const addTagToTransaction = async ( + userId: bigint, + transactionId: bigint, + tagId: bigint, + dbClient = prisma +) => { + return performDatabaseRequest(async (prismaTx) => { + return prismaTx.transaction_has_tags.create({ + data: { + transactions_transaction_id: transactionId, + tags_tag_id: tagId, + }, + }); + }, dbClient); +}; + +/** + * Associates the specified tag with the transaction. + * @param userId + * @param transactionId + * @param tagName - the name that identifies the desired tag + * @param createIfNeeded - should the tag be created if it doesn't already exist? + * @param dbClient + */ +const addTagToTransactionByName = async ( + userId: bigint, + transactionId: bigint, + tagName: string, + createIfNeeded: boolean, + dbClient = undefined +) => { + return performDatabaseRequest(async (prismaTx) => { + // Check if tag already exists (and add if createIfNeeded=true & it does not exist) + let tag = null; + if (createIfNeeded) { + tag = await prismaTx.tags.upsert({ + where: { + name_users_user_id: { + users_user_id: userId, + name: tagName, + }, + }, + update: {}, + create: { + name: tagName, + description: '', + users_user_id: userId, + }, + }); + } else { + tag = await prismaTx.tags.findFirst({ + where: { + users_user_id: userId, + name: tagName, + }, + }); + } + + if (tag == null) { + throw APIError.notFound('The specified tag could not be found!'); + } + + // Associate it with the specified transaction + return addTagToTransaction(userId, transactionId, tag.tag_id, prismaTx); + }, dbClient); +}; + +const buildSqlForExcludedAccountsList = (excludedAccs) => { + if (!excludedAccs || excludedAccs.length === 0) { + return ' 1 == 1'; + } + let sql = ' ('; + for (let cnt = 0; cnt < excludedAccs.length; cnt++) { + const acc = excludedAccs[cnt].account_id; + sql += ` '${acc}' `; + + if (cnt !== excludedAccs.length - 1) { + sql += ', '; + } + } + sql += ') '; + return sql; +}; + +export interface CalculatedTagAmounts { + tag_balance_credit: number; + tag_balance_debit: number; +} + +const getAmountForTagInPeriod = async( + tagId: bigint, + fromDate: number, + toDate: number, + includeTransfers = true, + dbClient = prisma +) : Promise => { + let accsExclusionSqlExcerptAccountsTo = ''; + let accsExclusionSqlExcerptAccountsFrom = ''; + let accountsToExcludeListInSQL = ''; + + const listOfAccountsToExclude = await dbClient.accounts.findMany({ + where: {exclude_from_budgets: true}, + }); + if (!listOfAccountsToExclude || listOfAccountsToExclude.length < 1) { + accsExclusionSqlExcerptAccountsTo = ' 1 = 1 '; + accsExclusionSqlExcerptAccountsFrom = ' 1 = 1 '; + } else { + accountsToExcludeListInSQL = buildSqlForExcludedAccountsList(listOfAccountsToExclude); + accsExclusionSqlExcerptAccountsTo = `accounts_account_to_id NOT IN ${accountsToExcludeListInSQL} `; + accsExclusionSqlExcerptAccountsFrom = `accounts_account_from_id NOT IN ${accountsToExcludeListInSQL} `; + } + + if (includeTransfers) { + return dbClient.$queryRaw`SELECT sum(if(type = 'I' OR + (type = 'T' AND ${accsExclusionSqlExcerptAccountsTo}), + amount, + 0)) as 'tag_balance_credit', + sum(if(type = 'E' OR + (type = 'T' AND ${accsExclusionSqlExcerptAccountsFrom}), + amount, + 0)) as 'tag_balance_debit' + FROM (SELECT amount, type, accounts_account_from_id, accounts_account_to_id + FROM transactions + INNER JOIN transaction_has_tags + ON transactions.transaction_id = transaction_has_tags.transactions_transaction_id + WHERE transaction_has_tags.tags_tag_id = ${tagId} + AND date_timestamp between ${fromDate} AND ${toDate}) as transactions_tags`; + } + + return dbClient.$queryRaw`SELECT sum(if(type = 'I', amount, 0)) as 'tag_balance_credit', + sum(if(type = 'E', amount, 0)) as 'tag_balance_debit' + FROM (SELECT amount, type, accounts_account_from_id, accounts_account_to_id + FROM transactions + INNER JOIN transaction_has_tags + ON transactions.transaction_id = transaction_has_tags.transactions_transaction_id + WHERE transaction_has_tags.tags_tag_id = ${tagId} + AND date_timestamp between ${fromDate} AND ${toDate}) as transactions_tags`; +} + +const getAmountForTagInMonth = async ( + tagId: bigint, + month: number, + year: number, + includeTransfers = true, + dbClient = prisma +) : Promise => { + const nextMonth = month < 12 ? month + 1 : 1; + const nextMonthsYear = month < 12 ? year : year + 1; + const maxDate = DateTimeUtils.getUnixTimestampFromDate( + new Date(nextMonthsYear, nextMonth - 1, 1) + ); + const minDate = DateTimeUtils.getUnixTimestampFromDate(new Date(year, month - 1, 1)); + /* Logger.addLog(`cat id: ${categoryId} | month: ${month} | year: ${year} | minDate: ${minDate} | maxDate: ${maxDate}`); */ + const amounts = await getAmountForTagInPeriod( + tagId, + minDate, + maxDate, + includeTransfers, + dbClient + ); + return amounts[0]; +} + +const getAmountForTagInYear = async ( + tagId: bigint, + year: number, + includeTransfers = true, + dbClient = prisma +): Promise => { + const maxDate = DateTimeUtils.getUnixTimestampFromDate(new Date(year, 11, 31)); + const minDate = DateTimeUtils.getUnixTimestampFromDate(new Date(year, 0, 1)); + + /* Logger.addLog(`cat id: ${categoryId} | month: ${month} | year: ${year} | minDate: ${minDate} | maxDate: ${maxDate}`); */ + const amounts = await getAmountForTagInPeriod( + tagId, + minDate, + maxDate, + includeTransfers, + dbClient + ); + return amounts[0]; +}; + +export default { + getAllTagsForUser, + getFilteredTagsForUserByPage, + createTag, + deleteTag, + updateTag, + getCountOfUserTags, + deleteAllTagsFromTransaction, + addTagToTransaction, + addTagToTransactionByName, + getAmountForTagInMonth, + getAmountForTagInYear, +}; diff --git a/src/services/transactionService.ts b/src/services/transactionService.ts index 27fd3c1..351d11f 100644 --- a/src/services/transactionService.ts +++ b/src/services/transactionService.ts @@ -1,18 +1,19 @@ -import {performDatabaseRequest, prisma} from '../config/prisma.js'; +import { performDatabaseRequest, prisma } from '../config/prisma.js'; import EntityService from './entityService.js'; import CategoryService from './categoryService.js'; import AccountService from './accountService.js'; import UserService from './userService.js'; import DateTimeUtils from '../utils/DateTimeUtils.js'; -import {MYFIN} from '../consts.js'; +import { MYFIN } from '../consts.js'; import ConvertUtils from '../utils/convertUtils.js'; import APIError from '../errorHandling/apiError.js'; import Logger from '../utils/Logger.js'; import RuleService from './ruleService.js'; +import TagService, { Tag } from './tagService.js'; const getTransactionsForUser = async ( - userId: bigint, - trxLimit: number + userId: bigint, + trxLimit: number ) => prisma.$queryRaw`SELECT transaction_id, transactions.date_timestamp, (transactions.amount / 100) as amount, @@ -42,17 +43,74 @@ const getTransactionsForUser = async ( ORDER BY transactions.date_timestamp DESC LIMIT ${trxLimit}`; +const doesTransactionBelongToUser = async ( + userId: bigint, + transactionId: bigint, + dbClient = prisma +) => { + // Get the transaction + const trx = await dbClient.transactions.findFirst({ + where: { + transaction_id: transactionId, + }, + }); + if (!trx) return false; + const result = await dbClient.accounts.findFirst({ + where: { + OR: [ + { account_id: trx.accounts_account_from_id || -1 }, + { account_id: trx.accounts_account_to_id || -1 }, + ], + }, + }); + + return result !== null; +}; + +const getAllTagsForTransaction = async ( + userId: bigint, + transactionId: bigint, + dbClient = undefined +) => + performDatabaseRequest(async (prismaTx) => { + // Make sure transaction belongs to user + const transactionBelongsToUser = await doesTransactionBelongToUser( + userId, + transactionId, + prismaTx + ); + if (!transactionBelongsToUser) { + throw APIError.notAuthorized(); + } + + const tags = await prismaTx.transaction_has_tags.findMany({ + where: { + transactions_transaction_id: transactionId, + }, + select: { + tags: true, + }, + }); + + let tagsData = []; + if (Array.isArray(tags)) { + tagsData = tags.map((tag) => tag.tags); + } + + return tagsData; + }, dbClient); + const getFilteredTransactionsByForUser = async ( - userId: bigint, - page: number, - pageSize: number, - searchQuery: string + userId: bigint, + page: number, + pageSize: number, + searchQuery: string ) => { - const query = `%${searchQuery}%`; - const offsetValue = page * pageSize; + const query = `%${searchQuery}%`; + const offsetValue = page * pageSize; - // main query for list of results (limited by pageSize and offsetValue) - const mainQuery = prisma.$queryRaw`SELECT transaction_id, + // main query for list of results (limited by pageSize and offsetValue) + const mainQuery = prisma.$queryRaw`SELECT transaction_id, transactions.is_essential, transactions.date_timestamp, (transactions.amount / 100) as amount, @@ -65,7 +123,8 @@ const getFilteredTransactionsByForUser = async ( accounts_account_from_id, acc_to.name as account_to_name, accounts_account_to_id, - acc_from.name as account_from_name + acc_from.name as account_from_name, + GROUP_CONCAT(tags.name) as tag_names FROM transactions LEFT JOIN accounts ON accounts.account_id = transactions.accounts_account_from_id LEFT JOIN categories @@ -75,22 +134,25 @@ const getFilteredTransactionsByForUser = async ( ON acc_to.account_id = transactions.accounts_account_to_id LEFT JOIN accounts acc_from ON acc_from.account_id = transactions.accounts_account_from_id + LEFT JOIN transaction_has_tags ON transaction_has_tags.transactions_transaction_id = transactions.transaction_id + LEFT JOIN tags ON tags.tag_id = transaction_has_tags.tags_tag_id WHERE (acc_to.users_user_id = ${userId} OR acc_from.users_user_id = ${userId}) AND (transactions.description LIKE ${query} OR acc_from.name LIKE ${query} OR acc_to.name LIKE ${query} - OR amount LIKE ${query} + OR (amount / 100) LIKE ${query} OR entities.name LIKE ${query} - OR categories.name LIKE ${query}) + OR categories.name LIKE ${query} + OR tags.name LIKE ${query}) GROUP BY transaction_id ORDER BY transactions.date_timestamp DESC LIMIT ${pageSize} OFFSET ${offsetValue}`; - // count of total of filtered results - const countQuery = prisma.$queryRaw`SELECT count(*) as 'count' - FROM (SELECT transactions.date_timestamp + // count of total of filtered results + const countQuery = prisma.$queryRaw`SELECT count(*) as 'count' + FROM (SELECT transactions.date_timestamp, GROUP_CONCAT(tags.name) as tag_names from transactions LEFT JOIN accounts ON accounts.account_id = transactions.accounts_account_from_id LEFT JOIN categories @@ -100,18 +162,21 @@ const getFilteredTransactionsByForUser = async ( ON acc_to.account_id = transactions.accounts_account_to_id LEFT JOIN accounts acc_from ON acc_from.account_id = transactions.accounts_account_from_id + LEFT JOIN transaction_has_tags ON transaction_has_tags.transactions_transaction_id = transactions.transaction_id + LEFT JOIN tags ON tags.tag_id = transaction_has_tags.tags_tag_id WHERE (acc_to.users_user_id = ${userId} OR acc_from.users_user_id = ${userId}) AND (transactions.description LIKE ${query} OR acc_from.name LIKE ${query} OR acc_to.name LIKE ${query} - OR amount LIKE ${query} + OR (amount / 100) LIKE ${query} OR entities.name LIKE ${query} OR categories.name LIKE - ${query}) + ${query} + OR tags.name LIKE ${query}) GROUP BY transaction_id) trx`; - const totalCountQuery = prisma.$queryRaw`SELECT count(*) as 'count' + const totalCountQuery = prisma.$queryRaw`SELECT count(*) as 'count' FROM (SELECT transactions.date_timestamp from transactions LEFT JOIN accounts ON accounts.account_id = transactions.accounts_account_from_id @@ -125,476 +190,531 @@ const getFilteredTransactionsByForUser = async ( WHERE (acc_to.users_user_id = ${userId} OR acc_from.users_user_id = ${userId}) GROUP BY transaction_id) trx`; - const [mainQueryResult, countQueryResult, totalCountQueryResult] = await prisma.$transaction([ - mainQuery, - countQuery, - totalCountQuery, - ]); - return { - results: mainQueryResult, - filtered_count: countQueryResult[0].count, - total_count: totalCountQueryResult[0].count, - }; -}; -const createTransactionStep0 = async (userId: bigint, dbClient = undefined) => { - const [entities, categories, accounts] = await performDatabaseRequest(async (_) => { - const ents = await EntityService.getAllEntitiesForUser(userId); - const cats = await CategoryService.getAllCategoriesForUser(userId); - const accs = await AccountService.getActiveAccountsForUser(userId); + const [mainQueryResult, countQueryResult, totalCountQueryResult] = await prisma.$transaction([ + mainQuery, + countQuery, + totalCountQuery, + ]); - return [ents, cats, accs]; - }, dbClient); + // Attach associated tags to transaction + const promises = []; - return { - entities: entities, - categories: categories, - accounts: accounts, - }; + if (Array.isArray(mainQueryResult)) { + for (const trx of mainQueryResult) { + trx.tags = await getAllTagsForTransaction(userId, trx.transaction_id); + } + } + + await Promise.all(promises); + + return { + results: mainQueryResult, + filtered_count: countQueryResult[0].count, + total_count: totalCountQueryResult[0].count, + }; +}; +const createTransactionStep0 = async (userId: bigint, dbClient = undefined) => { + const [entities, categories, accounts, tags] = await performDatabaseRequest(async (_) => { + const ents = await EntityService.getAllEntitiesForUser(userId); + const cats = await CategoryService.getAllCategoriesForUser(userId); + const accs = await AccountService.getActiveAccountsForUser(userId); + const tags = await TagService.getAllTagsForUser(userId); + + return [ents, cats, accs, tags]; + }, dbClient); + + return { + entities: entities, + categories: categories, + accounts: accounts, + tags: tags, + }; }; export type CreateTransactionType = { - amount: number; - type: string; - description: string; - entity_id?: bigint; - account_from_id?: bigint; - account_to_id?: bigint; - category_id?: bigint; - date_timestamp: number; - is_essential: boolean; + amount: number; + type: string; + description: string; + entity_id?: bigint; + account_from_id?: bigint; + account_to_id?: bigint; + category_id?: bigint; + date_timestamp: number; + is_essential: boolean; + tags?: Array; }; const createTransaction = async ( - userId: bigint, - trx: CreateTransactionType, - dbClient = undefined + userId: bigint, + trx: CreateTransactionType, + dbClient = undefined ) => { - //Logger.addStringifiedLog(trx); - trx.amount = ConvertUtils.convertFloatToBigInteger(trx.amount); - await performDatabaseRequest(async (prismaTx) => { - const promises = []; - promises.push( - prismaTx.transactions.create({ - data: { - date_timestamp: trx.date_timestamp, - amount: trx.amount, - type: trx.type, - description: trx.description, - entities_entity_id: trx.entity_id, - accounts_account_from_id: trx.account_from_id, - accounts_account_to_id: trx.account_to_id, - categories_category_id: trx.category_id, - is_essential: trx.is_essential, - }, - })); - - promises.push( - UserService.setupLastUpdateTimestamp( - userId, - DateTimeUtils.getCurrentUnixTimestamp(), - prismaTx - )); - - await Promise.all(promises); - - let newBalance; - switch (trx.type) { - case MYFIN.TRX_TYPES.INCOME: - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - trx.account_to_id, - trx.date_timestamp - 1, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance(userId, trx.account_to_id, newBalance, prismaTx); - break; - case MYFIN.TRX_TYPES.EXPENSE: - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - trx.account_from_id, - trx.date_timestamp - 1, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance( - userId, - trx.account_from_id, - newBalance, - prismaTx - ); - break; - case MYFIN.TRX_TYPES.TRANSFER: - default: - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - trx.account_to_id, - trx.date_timestamp - 1, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance(userId, trx.account_to_id, newBalance, prismaTx); - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - trx.account_from_id, - trx.date_timestamp - 1, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance( - userId, - trx.account_from_id, - newBalance, - prismaTx - ); - break; - } - }, dbClient); + //Logger.addStringifiedLog(trx); + trx.amount = ConvertUtils.convertFloatToBigInteger(trx.amount); + await performDatabaseRequest(async (prismaTx) => { + // Add transaction + const addedTrx = await prismaTx.transactions.create({ + data: { + date_timestamp: trx.date_timestamp, + amount: trx.amount, + type: trx.type, + description: trx.description, + entities_entity_id: trx.entity_id, + accounts_account_from_id: trx.account_from_id, + accounts_account_to_id: trx.account_to_id, + categories_category_id: trx.category_id, + is_essential: trx.is_essential, + }, + }); + + // Set last update timestamp + await UserService.setupLastUpdateTimestamp( + userId, + DateTimeUtils.getCurrentUnixTimestamp(), + prismaTx + ); + + // Associate tags with transaction + await Promise.all( + trx.tags?.map(async (tagName) => { + const tag = await TagService.addTagToTransactionByName( + userId, + addedTrx.transaction_id, + tagName, + true, + prismaTx + ); + return tag; + }) || [] + ); + + let newBalance; + switch (trx.type) { + case MYFIN.TRX_TYPES.INCOME: + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + trx.account_to_id, + trx.date_timestamp - 1, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance(userId, trx.account_to_id, newBalance, prismaTx); + break; + case MYFIN.TRX_TYPES.EXPENSE: + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + trx.account_from_id, + trx.date_timestamp - 1, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance( + userId, + trx.account_from_id, + newBalance, + prismaTx + ); + break; + case MYFIN.TRX_TYPES.TRANSFER: + default: + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + trx.account_to_id, + trx.date_timestamp - 1, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance(userId, trx.account_to_id, newBalance, prismaTx); + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + trx.account_from_id, + trx.date_timestamp - 1, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance( + userId, + trx.account_from_id, + newBalance, + prismaTx + ); + break; + } + }, dbClient); }; const createTransactionsInBulk = async ( - userId: bigint, - trxList: Array, - dbClient = undefined + userId: bigint, + trxList: Array, + dbClient = undefined ) => - performDatabaseRequest(async (prismaTx) => { - let importedCnt = 0; - for (const trx of trxList) { - if ( - !trx.date_timestamp || - !trx.amount || - !trx.type || - (!trx.account_from_id && !trx.account_to_id) - ) { - continue; - } - - // Both accounts (if defined) need to belong to the user making the request - if ( - trx.account_from_id && - !(await AccountService.doesAccountBelongToUser(userId, trx.account_from_id)) - ) { - throw APIError.notAuthorized(); - } - - if ( - trx.account_to_id && - !(await AccountService.doesAccountBelongToUser(userId, trx.account_to_id)) - ) { - throw APIError.notAuthorized(); - } - - await createTransaction(userId, trx, prismaTx); - importedCnt++; - } + performDatabaseRequest(async (prismaTx) => { + let importedCnt = 0; + for (const trx of trxList) { + if ( + !trx.date_timestamp || + !trx.amount || + !trx.type || + (!trx.account_from_id && !trx.account_to_id) + ) { + continue; + } + + // Both accounts (if defined) need to belong to the user making the request + if ( + trx.account_from_id && + !(await AccountService.doesAccountBelongToUser(userId, trx.account_from_id)) + ) { + throw APIError.notAuthorized(); + } + + if ( + trx.account_to_id && + !(await AccountService.doesAccountBelongToUser(userId, trx.account_to_id)) + ) { + throw APIError.notAuthorized(); + } + + await createTransaction(userId, trx, prismaTx); + importedCnt++; + } - return importedCnt; - }, dbClient); + return importedCnt; + }, dbClient); const deleteTransaction = async (userId: bigint, transactionId: number, dbClient = undefined) => { - await performDatabaseRequest(async (prismaTx) => { - const trx = await prismaTx.transactions - .findUniqueOrThrow({ - where: { - transaction_id: transactionId, - }, - }) - .catch((err) => { - throw APIError.notFound(`Transaction could not be found.`); - }); - - const oldTimestamp = trx.date_timestamp; - const oldType = trx.type; - const oldAccountTo = trx.accounts_account_to_id; - const oldAccountFrom = trx.accounts_account_from_id; - Logger.addStringifiedLog(trx); - // Make sure account belongs to user - const accountsCount = await prismaTx.accounts.count({ - where: { - // @ts-expect-error expected - account_id: {in: [oldAccountTo || -1, oldAccountFrom || -1]}, - users_user_id: userId, - }, - }); - if (accountsCount === 0) { - throw APIError.notFound(`Account could not be found.`); - } + await performDatabaseRequest(async (prismaTx) => { + const trx = await prismaTx.transactions + .findUniqueOrThrow({ + where: { + transaction_id: transactionId, + }, + }) + .catch((err) => { + throw APIError.notFound(`Transaction could not be found.`); + }); + + const oldTimestamp = trx.date_timestamp; + const oldType = trx.type; + const oldAccountTo = trx.accounts_account_to_id; + const oldAccountFrom = trx.accounts_account_from_id; + Logger.addStringifiedLog(trx); + // Make sure account belongs to user + const accountsCount = await prismaTx.accounts.count({ + where: { + // @ts-expect-error expected + account_id: { in: [oldAccountTo || -1, oldAccountFrom || -1] }, + users_user_id: userId, + }, + }); + if (accountsCount === 0) { + throw APIError.notFound(`Account could not be found.`); + } - // Delete transaction - await prismaTx.transactions.delete({ - where: { - transaction_id: transactionId, - }, - }); - - await UserService.setupLastUpdateTimestamp( - userId, - DateTimeUtils.getCurrentUnixTimestamp(), - prismaTx + // Delete transaction + await prismaTx.transactions.delete({ + where: { + transaction_id: transactionId, + }, + }); + + await UserService.setupLastUpdateTimestamp( + userId, + DateTimeUtils.getCurrentUnixTimestamp(), + prismaTx + ); + + // Rollback the effect of oldAmount + let newBalance; + switch (oldType) { + case MYFIN.TRX_TYPES.INCOME: + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + oldAccountTo, + oldTimestamp - 1n, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx ); - - // Rollback the effect of oldAmount - let newBalance; - switch (oldType) { - case MYFIN.TRX_TYPES.INCOME: - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - oldAccountTo, - oldTimestamp - 1n, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance(userId, oldAccountTo, newBalance, prismaTx); - break; - case MYFIN.TRX_TYPES.EXPENSE: - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - oldAccountFrom, - oldTimestamp - 1n, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance(userId, oldAccountFrom, newBalance, prismaTx); - break; - case MYFIN.TRX_TYPES.TRANSFER: - default: - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - oldAccountTo, - oldTimestamp - 1n, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance(userId, oldAccountTo, newBalance, prismaTx); - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - oldAccountFrom, - oldTimestamp - 1n, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance(userId, oldAccountFrom, newBalance, prismaTx); - break; - } - }, undefined); + await AccountService.setNewAccountBalance(userId, oldAccountTo, newBalance, prismaTx); + break; + case MYFIN.TRX_TYPES.EXPENSE: + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + oldAccountFrom, + oldTimestamp - 1n, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance(userId, oldAccountFrom, newBalance, prismaTx); + break; + case MYFIN.TRX_TYPES.TRANSFER: + default: + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + oldAccountTo, + oldTimestamp - 1n, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance(userId, oldAccountTo, newBalance, prismaTx); + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + oldAccountFrom, + oldTimestamp - 1n, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance(userId, oldAccountFrom, newBalance, prismaTx); + break; + } + }, undefined); }; export type UpdatedTrxType = { - new_amount: number; - new_type: string; - new_description: string; - new_entity_id: bigint; - new_account_from_id: bigint; - new_account_to_id: bigint; - new_category_id: bigint; - new_date_timestamp: number; - new_is_essential: boolean; - transaction_id: bigint; - /* SPLIT TRX */ - is_split: boolean; - split_amount?: number; - split_category?: bigint; - split_entity?: bigint; - split_type?: string; - split_account_from?: bigint; - split_account_to?: bigint; - split_description?: string; - split_is_essential?: boolean; + new_amount: number; + new_type: string; + new_description: string; + new_entity_id: bigint; + new_account_from_id: bigint; + new_account_to_id: bigint; + new_category_id: bigint; + new_date_timestamp: number; + new_is_essential: boolean; + transaction_id: bigint; + tags: Array; + /* SPLIT TRX */ + is_split: boolean; + split_amount?: number; + split_category?: bigint; + split_entity?: bigint; + split_type?: string; + split_account_from?: bigint; + split_account_to?: bigint; + split_description?: string; + split_is_essential?: boolean; + split_tags: Array; }; const updateTransaction = async ( - userId: bigint, - updatedTrx: UpdatedTrxType, - dbClient = undefined + userId: bigint, + updatedTrx: UpdatedTrxType, + dbClient = undefined ) => { - const trx = { - ...updatedTrx, - ...{ - new_amount: ConvertUtils.convertFloatToBigInteger(updatedTrx.new_amount), - }, - }; - /* trx.amount = ConvertUtils.convertFloatToBigInteger(trx.amount); */ - await performDatabaseRequest(async (prismaTx) => { - const outdatedTrx = await prismaTx.transactions.findUniqueOrThrow({ - where: {transaction_id: trx.transaction_id}, - }); - - const oldAmount = Number(outdatedTrx.amount); - const oldType = outdatedTrx.type; - const oldTimestamp = outdatedTrx.date_timestamp; - const oldAccountTo = outdatedTrx.accounts_account_to_id; - const oldAccountFrom = outdatedTrx.accounts_account_from_id; - - // Make sure account(s) belong to user - if (trx.new_account_from_id) { - await AccountService.doesAccountBelongToUser(userId, trx.new_account_from_id, prismaTx).catch((err) => { - throw APIError.notAuthorized(); - }); + const trx = { + ...updatedTrx, + ...{ + new_amount: ConvertUtils.convertFloatToBigInteger(updatedTrx.new_amount), + }, + }; + /* trx.amount = ConvertUtils.convertFloatToBigInteger(trx.amount); */ + await performDatabaseRequest(async (prismaTx) => { + const outdatedTrx = await prismaTx.transactions.findUniqueOrThrow({ + where: { transaction_id: trx.transaction_id }, + }); + + const oldAmount = Number(outdatedTrx.amount); + const oldType = outdatedTrx.type; + const oldTimestamp = outdatedTrx.date_timestamp; + const oldAccountTo = outdatedTrx.accounts_account_to_id; + const oldAccountFrom = outdatedTrx.accounts_account_from_id; + + // Make sure account(s) belong to user + if (trx.new_account_from_id) { + await AccountService.doesAccountBelongToUser(userId, trx.new_account_from_id, prismaTx).catch( + (err) => { + throw APIError.notAuthorized(); } + ); + } - if (trx.new_account_to_id) { - await AccountService.doesAccountBelongToUser(userId, trx.new_account_to_id, prismaTx).catch((err) => { - throw APIError.notAuthorized(); - }); + if (trx.new_account_to_id) { + await AccountService.doesAccountBelongToUser(userId, trx.new_account_to_id, prismaTx).catch( + (err) => { + throw APIError.notAuthorized(); } + ); + } - if (trx.split_account_from) { - await AccountService.doesAccountBelongToUser(userId, trx.split_account_from, prismaTx).catch((err) => { - throw APIError.notAuthorized(); - }); + if (trx.split_account_from) { + await AccountService.doesAccountBelongToUser(userId, trx.split_account_from, prismaTx).catch( + (err) => { + throw APIError.notAuthorized(); } - if (trx.split_account_to) { - await AccountService.doesAccountBelongToUser(userId, trx.split_account_to, prismaTx).catch((err) => { - throw APIError.notAuthorized(); - }); + ); + } + if (trx.split_account_to) { + await AccountService.doesAccountBelongToUser(userId, trx.split_account_to, prismaTx).catch( + (err) => { + throw APIError.notAuthorized(); } + ); + } - await prismaTx.transactions.update({ - where: {transaction_id: trx.transaction_id}, - data: { - date_timestamp: trx.new_date_timestamp, - amount: trx.new_amount, - type: trx.new_type, - description: trx.new_description, - entities_entity_id: trx.new_entity_id, - accounts_account_from_id: trx.new_account_from_id, - accounts_account_to_id: trx.new_account_to_id, - categories_category_id: trx.new_category_id, - is_essential: trx.new_is_essential, - }, - }); - - await UserService.setupLastUpdateTimestamp( - userId, - DateTimeUtils.getCurrentUnixTimestamp(), - prismaTx + await prismaTx.transactions.update({ + where: { transaction_id: trx.transaction_id }, + data: { + date_timestamp: trx.new_date_timestamp, + amount: trx.new_amount, + type: trx.new_type, + description: trx.new_description, + entities_entity_id: trx.new_entity_id, + accounts_account_from_id: trx.new_account_from_id, + accounts_account_to_id: trx.new_account_to_id, + categories_category_id: trx.new_category_id, + is_essential: trx.new_is_essential, + }, + }); + + await UserService.setupLastUpdateTimestamp( + userId, + DateTimeUtils.getCurrentUnixTimestamp(), + prismaTx + ); + + // Remove the effect of outdated amount + let newBalance; + switch (oldType) { + case MYFIN.TRX_TYPES.INCOME: + await AccountService.changeBalance(userId, oldAccountTo, -oldAmount, prismaTx); + await AccountService.recalculateBalanceForAccountIncrementally( + oldAccountTo, + oldTimestamp - 1n, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx ); + break; + case MYFIN.TRX_TYPES.EXPENSE: + await AccountService.changeBalance(userId, oldAccountFrom, -oldAmount, prismaTx); + await AccountService.recalculateBalanceForAccountIncrementally( + oldAccountFrom, + oldTimestamp - 1n, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + break; + case MYFIN.TRX_TYPES.TRANSFER: + default: + await AccountService.changeBalance(userId, oldAccountTo, -oldAmount, prismaTx); + await AccountService.recalculateBalanceForAccountIncrementally( + oldAccountTo, + oldTimestamp - 1n, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.changeBalance(userId, oldAccountTo, -oldAmount, prismaTx); + await AccountService.recalculateBalanceForAccountIncrementally( + oldAccountTo, + oldTimestamp - 1n, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + break; + } - // Remove the effect of outdated amount - let newBalance; - switch (oldType) { - case MYFIN.TRX_TYPES.INCOME: - await AccountService.changeBalance(userId, oldAccountTo, -oldAmount, prismaTx); - await AccountService.recalculateBalanceForAccountIncrementally( - oldAccountTo, - oldTimestamp - 1n, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - break; - case MYFIN.TRX_TYPES.EXPENSE: - await AccountService.changeBalance(userId, oldAccountFrom, -oldAmount, prismaTx); - await AccountService.recalculateBalanceForAccountIncrementally( - oldAccountFrom, - oldTimestamp - 1n, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - break; - case MYFIN.TRX_TYPES.TRANSFER: - default: - await AccountService.changeBalance(userId, oldAccountTo, -oldAmount, prismaTx); - await AccountService.recalculateBalanceForAccountIncrementally( - oldAccountTo, - oldTimestamp - 1n, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.changeBalance(userId, oldAccountTo, -oldAmount, prismaTx); - await AccountService.recalculateBalanceForAccountIncrementally( - oldAccountTo, - oldTimestamp - 1n, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - break; - } - - // Add the effect of updated amount - Logger.addLog(`New type: ${trx.new_type}`); - switch (trx.new_type) { - case MYFIN.TRX_TYPES.INCOME: - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - trx.new_account_to_id, - Math.min(trx.new_date_timestamp, Number(oldTimestamp)) - 1, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance( - userId, - trx.new_account_to_id, - newBalance, - prismaTx - ); - break; - case MYFIN.TRX_TYPES.EXPENSE: - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - trx.new_account_from_id, - Math.min(trx.new_date_timestamp, Number(oldTimestamp)) - 1, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance( - userId, - trx.new_account_from_id, - newBalance, - prismaTx - ); - break; - case MYFIN.TRX_TYPES.TRANSFER: - default: - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - trx.new_account_to_id, - Math.min(trx.new_date_timestamp, Number(oldTimestamp)) - 1, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance( - userId, - trx.new_account_to_id, - newBalance, - prismaTx - ); - newBalance = await AccountService.recalculateBalanceForAccountIncrementally( - trx.new_account_from_id, - Math.min(trx.new_date_timestamp, Number(oldTimestamp)) - 1, - DateTimeUtils.getCurrentUnixTimestamp() + 1, - prismaTx - ); - await AccountService.setNewAccountBalance( - userId, - trx.new_account_from_id, - newBalance, - prismaTx - ); - break; - } - }, dbClient); - - // SPLIT HANDLING - if (trx.is_split === true) { - await createTransaction( - userId, - { - date_timestamp: trx.new_date_timestamp, - amount: trx.split_amount, - type: trx.split_type, - description: trx.split_description, - entity_id: trx.split_entity, - category_id: trx.split_category, - account_from_id: trx.split_account_from, - account_to_id: trx.split_account_to, - is_essential: trx.split_is_essential, - }, + // Add the effect of updated amount + Logger.addLog(`New type: ${trx.new_type}`); + switch (trx.new_type) { + case MYFIN.TRX_TYPES.INCOME: + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + trx.new_account_to_id, + Math.min(trx.new_date_timestamp, Number(oldTimestamp)) - 1, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance( + userId, + trx.new_account_to_id, + newBalance, + prismaTx ); + break; + case MYFIN.TRX_TYPES.EXPENSE: + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + trx.new_account_from_id, + Math.min(trx.new_date_timestamp, Number(oldTimestamp)) - 1, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance( + userId, + trx.new_account_from_id, + newBalance, + prismaTx + ); + break; + case MYFIN.TRX_TYPES.TRANSFER: + default: + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + trx.new_account_to_id, + Math.min(trx.new_date_timestamp, Number(oldTimestamp)) - 1, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance( + userId, + trx.new_account_to_id, + newBalance, + prismaTx + ); + newBalance = await AccountService.recalculateBalanceForAccountIncrementally( + trx.new_account_from_id, + Math.min(trx.new_date_timestamp, Number(oldTimestamp)) - 1, + DateTimeUtils.getCurrentUnixTimestamp() + 1, + prismaTx + ); + await AccountService.setNewAccountBalance( + userId, + trx.new_account_from_id, + newBalance, + prismaTx + ); + break; } + }, dbClient); + + // Remove all tags + await TagService.deleteAllTagsFromTransaction(userId, updatedTrx.transaction_id, dbClient); + + // Add new tags + if (Array.isArray(updatedTrx.tags)) { + const promises = []; + updatedTrx.tags?.forEach((tagName) => { + promises.push( + TagService.addTagToTransactionByName( + userId, + updatedTrx.transaction_id, + tagName, + true, + dbClient + ) + ); + }); + + await Promise.all(promises); + } + + // SPLIT HANDLING + if (trx.is_split === true) { + await createTransaction(userId, { + date_timestamp: trx.new_date_timestamp, + amount: trx.split_amount, + type: trx.split_type, + description: trx.split_description, + entity_id: trx.split_entity, + category_id: trx.split_category, + account_from_id: trx.split_account_from, + account_to_id: trx.split_account_to, + is_essential: trx.split_is_essential, + tags: trx.split_tags, + }); + } }; const getAllTransactionsForUserInCategoryAndInMonth = async ( - userId: bigint, - month: number, - year: number, - catId: bigint, - type: string, - dbClient = prisma + userId: bigint, + month: number, + year: number, + catId: bigint, + type: string, + dbClient = prisma ) => { - const nextMonth = month < 12 ? month + 1 : 1; - const nextMonthsYear = month < 12 ? year : year + 1; - const maxDate = new Date(nextMonthsYear, nextMonth - 1, 1); - const minDate = new Date(year, month - 1, 1); - Logger.addLog(`min date: ${DateTimeUtils.getUnixTimestampFromDate(minDate)}`); - Logger.addLog(`max date: ${DateTimeUtils.getUnixTimestampFromDate(maxDate)}`); - return dbClient.$queryRaw`SELECT transaction_id, + const nextMonth = month < 12 ? month + 1 : 1; + const nextMonthsYear = month < 12 ? year : year + 1; + const maxDate = new Date(nextMonthsYear, nextMonth - 1, 1); + const minDate = new Date(year, month - 1, 1); + Logger.addLog(`min date: ${DateTimeUtils.getUnixTimestampFromDate(minDate)}`); + Logger.addLog(`max date: ${DateTimeUtils.getUnixTimestampFromDate(maxDate)}`); + return dbClient.$queryRaw`SELECT transaction_id, transactions.date_timestamp, transactions.is_essential, (transactions.amount / 100) as amount, @@ -634,108 +754,108 @@ const getAllTransactionsForUserInCategoryAndInMonth = async ( }; const getCountOfUserTransactions = async (userId: bigint, dbClient = prisma) => { - const rawData = await dbClient.$queryRaw`SELECT count(DISTINCT (transaction_id)) as 'count' + const rawData = await dbClient.$queryRaw`SELECT count(DISTINCT (transaction_id)) as 'count' FROM transactions LEFT JOIN accounts ON transactions.accounts_account_from_id = accounts.account_id or transactions.accounts_account_to_id = accounts.account_id WHERE accounts.users_user_id = ${userId}`; - return rawData[0].count; + return rawData[0].count; }; interface RuleInstructions { - matching_rule?: bigint; - date?: number; - description?: string; - amount?: number; - type?: string; - selectedCategoryID?: bigint; - selectedEntityID?: bigint; - selectedAccountFromID?: bigint; - selectedAccountToID?: bigint; - isEssential?: boolean; + matching_rule?: bigint; + date?: number; + description?: string; + amount?: number; + type?: string; + selectedCategoryID?: bigint; + selectedEntityID?: bigint; + selectedAccountFromID?: bigint; + selectedAccountToID?: bigint; + isEssential?: boolean; } const autoCategorizeTransaction = async ( - userId: bigint, - description: string, - amount: number, - type: string, - accountsFromId?: bigint, - accountsToId?: bigint, - date?: number, - dbClient = undefined + userId: bigint, + description: string, + amount: number, + type: string, + accountsFromId?: bigint, + accountsToId?: bigint, + date?: number, + dbClient = undefined ): Promise => - performDatabaseRequest(async (prismaTx) => { - if (!dbClient) dbClient = prismaTx; - const matchedRule = await RuleService.getRuleForTransaction( - userId, - description, - amount, - type, - accountsFromId, - accountsToId, - MYFIN.RULES.MATCHING.IGNORE, - MYFIN.RULES.MATCHING.IGNORE, - dbClient - ); - Logger.addLog('Rule found:'); - Logger.addStringifiedLog(matchedRule); - - return { - matching_rule: matchedRule?.rule_id, - date: date, - description: description, - amount: amount, - type: type, - selectedCategoryID: matchedRule?.assign_category_id, - selectedEntityID: matchedRule?.assign_entity_id, - selectedAccountFromID: matchedRule?.assign_account_from_id ?? accountsFromId, //(type == MYFIN.TRX_TYPES.INCOME) ? matchedRule?.assign_account_from_id : accountsFromId, - selectedAccountToID: matchedRule?.assign_account_to_id ?? accountsToId, //(type == MYFIN.TRX_TYPES.INCOME) ? accountsToId : matchedRule?.assign_account_to_id,//matchedRule?.assign_account_to_id || accountsToId, - isEssential: matchedRule?.assign_is_essential, - }; - }, dbClient) as Promise; + performDatabaseRequest(async (prismaTx) => { + if (!dbClient) dbClient = prismaTx; + const matchedRule = await RuleService.getRuleForTransaction( + userId, + description, + amount, + type, + accountsFromId, + accountsToId, + MYFIN.RULES.MATCHING.IGNORE, + MYFIN.RULES.MATCHING.IGNORE, + dbClient + ); + Logger.addLog('Rule found:'); + Logger.addStringifiedLog(matchedRule); + + return { + matching_rule: matchedRule?.rule_id, + date: date, + description: description, + amount: amount, + type: type, + selectedCategoryID: matchedRule?.assign_category_id, + selectedEntityID: matchedRule?.assign_entity_id, + selectedAccountFromID: matchedRule?.assign_account_from_id ?? accountsFromId, //(type == MYFIN.TRX_TYPES.INCOME) ? matchedRule?.assign_account_from_id : accountsFromId, + selectedAccountToID: matchedRule?.assign_account_to_id ?? accountsToId, //(type == MYFIN.TRX_TYPES.INCOME) ? accountsToId : matchedRule?.assign_account_to_id,//matchedRule?.assign_account_to_id || accountsToId, + isEssential: matchedRule?.assign_is_essential, + }; + }, dbClient) as Promise; interface TransactionPreRuleInstructions { - date?: number; - description?: string; - amount?: number; - type?: string; - accounts_account_from_id?: bigint; - accounts_account_to_id?: bigint; + date?: number; + description?: string; + amount?: number; + type?: string; + accounts_account_from_id?: bigint; + accounts_account_to_id?: bigint; } const autoCategorizeTransactionList = async ( - userId: bigint, - accountId: bigint, - trxList: Array, - dbClient = undefined + userId: bigint, + accountId: bigint, + trxList: Array, + dbClient = undefined ) => { - const promises = []; - for (const trx of trxList) { - promises.push( - autoCategorizeTransaction( - userId, - trx.description, - trx.amount, - trx.type, - trx.type == MYFIN.TRX_TYPES.INCOME ? null : accountId, - trx.type != MYFIN.TRX_TYPES.INCOME ? null : accountId, - trx.date, - dbClient - ) - ); - } - - return Promise.all(promises); + const promises = []; + for (const trx of trxList) { + promises.push( + autoCategorizeTransaction( + userId, + trx.description, + trx.amount, + trx.type, + trx.type == MYFIN.TRX_TYPES.INCOME ? null : accountId, + trx.type != MYFIN.TRX_TYPES.INCOME ? null : accountId, + trx.date, + dbClient + ) + ); + } + + return Promise.all(promises); }; const getYearOfFirstTransactionForUser = async ( - userId: bigint, - dbClient = prisma + userId: bigint, + dbClient = prisma ): Promise => { - const result = await dbClient.$queryRaw`SELECT YEAR(FROM_UNIXTIME(date_timestamp)) as 'year' + const result = await dbClient.$queryRaw`SELECT YEAR(FROM_UNIXTIME(date_timestamp)) as 'year' FROM transactions LEFT JOIN accounts account_from ON account_from.account_id = transactions.accounts_account_from_id @@ -746,14 +866,14 @@ const getYearOfFirstTransactionForUser = async ( ORDER BY date_timestamp ASC LIMIT 1`; - return result[0].year; + return result[0].year; }; const getDateTimestampOfFirstTransactionForUser = async ( - userId: bigint, - dbClient = prisma + userId: bigint, + dbClient = prisma ): Promise => { - const result = await dbClient.$queryRaw`SELECT date_timestamp + const result = await dbClient.$queryRaw`SELECT date_timestamp FROM transactions LEFT JOIN accounts account_from ON account_from.account_id = transactions.accounts_account_from_id @@ -763,29 +883,30 @@ const getDateTimestampOfFirstTransactionForUser = async ( OR account_to.users_user_id = ${userId} ORDER BY date_timestamp ASC LIMIT 1`; - return result[0].date_timestamp; + return result[0].date_timestamp; }; const deleteAllTransactionsFromUser = async (userId: bigint, dbClient = prisma) => { - return dbClient.$queryRaw`DELETE transactions FROM transactions + return dbClient.$queryRaw`DELETE transactions FROM transactions LEFT JOIN accounts acc_to ON acc_to.account_id = transactions.accounts_account_to_id LEFT JOIN accounts acc_from ON acc_from.account_id = transactions.accounts_account_from_id WHERE acc_to.users_user_id = ${userId} OR acc_from.users_user_id = ${userId} `; }; export default { - getTransactionsForUser, - getFilteredTransactionsByForUser, - createTransactionStep0, - createTransaction, - deleteTransaction, - updateTransaction, - getAllTransactionsForUserInCategoryAndInMonth, - getCountOfUserTransactions, - autoCategorizeTransaction, - autoCategorizeTransactionList, - createTransactionsInBulk, - getYearOfFirstTransactionForUser, - getDateTimestampOfFirstTransactionForUser, - deleteAllTransactionsFromUser, + getTransactionsForUser, + getFilteredTransactionsByForUser, + createTransactionStep0, + createTransaction, + deleteTransaction, + updateTransaction, + getAllTransactionsForUserInCategoryAndInMonth, + getCountOfUserTransactions, + autoCategorizeTransaction, + autoCategorizeTransactionList, + createTransactionsInBulk, + getYearOfFirstTransactionForUser, + getDateTimestampOfFirstTransactionForUser, + deleteAllTransactionsFromUser, + doesTransactionBelongToUser, }; diff --git a/src/services/userService.ts b/src/services/userService.ts index b17765f..b1877a6 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -1,136 +1,139 @@ -import {prisma} from '../config/prisma.js'; +import { prisma } from '../config/prisma.js'; import APIError from '../errorHandling/apiError.js'; import * as cryptoUtils from '../utils/CryptoUtils.js'; import SessionManager from '../utils/sessionManager.js'; -import {Prisma} from '@prisma/client'; +import { Prisma } from '@prisma/client'; import CategoryService from './categoryService.js'; import EntityService from './entityService.js'; import DemoDataManager from '../utils/demoDataManager.js'; -import AccountService from "./accountService.js"; -import Logger from "../utils/Logger.js"; -import ConvertUtils from "../utils/convertUtils.js"; +import AccountService from './accountService.js'; +import Logger from '../utils/Logger.js'; +import ConvertUtils from '../utils/convertUtils.js'; +import TagService from './tagService.js'; const User = prisma.users; -interface CategoriesEntitiesOutput { - categories?: Array<{ category_id: bigint; name: string; type: string }>; - entities?: Array<{ entity_id: bigint; name: string }>; +interface CategoriesEntitiesTagsOutput { + categories?: Array<{ category_id: bigint; name: string; type: string }>; + entities?: Array<{ entity_id: bigint; name: string }>; + tags?: Array<{ tag_id: bigint; name: string; description?: string }>; } const userService = { - createUser: async (user: Prisma.usersCreateInput) => { - // eslint-disable-next-line no-param-reassign - user.password = cryptoUtils.hashPassword(user.password); - return User.create({data: user}); - }, - attemptLogin: async (username: string, password: string, mobile: boolean, dbClient = prisma) => { - const whereCondition = {username}; - const data = await User.findUniqueOrThrow({ - where: whereCondition, - }).catch(() => { - throw APIError.notAuthorized('User Not Found'); - }); - let userAccounts = []; - if (data) { - const isValid = cryptoUtils.verifyPassword(password, data.password); - if (isValid) { - const newSessionData = await SessionManager.generateNewSessionKeyForUser(username, mobile); - if (mobile) { - // eslint-disable-next-line no-param-reassign - data.sessionkey_mobile = newSessionData.sessionkey; - // eslint-disable-next-line no-param-reassign - data.trustlimit_mobile = newSessionData.trustlimit; - } else { - // eslint-disable-next-line no-param-reassign - data.sessionkey = newSessionData.sessionkey; - // eslint-disable-next-line no-param-reassign - data.trustlimit = newSessionData.trustlimit; - } + createUser: async (user: Prisma.usersCreateInput) => { + // eslint-disable-next-line no-param-reassign + user.password = cryptoUtils.hashPassword(user.password); + return User.create({ data: user }); + }, + attemptLogin: async (username: string, password: string, mobile: boolean, dbClient = prisma) => { + const whereCondition = { username }; + const data = await User.findUniqueOrThrow({ + where: whereCondition, + }).catch(() => { + throw APIError.notAuthorized('User Not Found'); + }); + let userAccounts = []; + if (data) { + const isValid = cryptoUtils.verifyPassword(password, data.password); + if (isValid) { + const newSessionData = await SessionManager.generateNewSessionKeyForUser(username, mobile); + if (mobile) { + // eslint-disable-next-line no-param-reassign + data.sessionkey_mobile = newSessionData.sessionkey; + // eslint-disable-next-line no-param-reassign + data.trustlimit_mobile = newSessionData.trustlimit; + } else { + // eslint-disable-next-line no-param-reassign + data.sessionkey = newSessionData.sessionkey; + // eslint-disable-next-line no-param-reassign + data.trustlimit = newSessionData.trustlimit; + } - userAccounts = await AccountService.getAccountsForUser(data.user_id, undefined, dbClient) - Logger.addStringifiedLog(userAccounts) - } else { - throw APIError.notAuthorized('Wrong Credentials'); - } - } else { - throw APIError.notAuthorized('User Not Found'); - } + userAccounts = await AccountService.getAccountsForUser(data.user_id, undefined, dbClient); + Logger.addStringifiedLog(userAccounts); + } else { + throw APIError.notAuthorized('Wrong Credentials'); + } + } else { + throw APIError.notAuthorized('User Not Found'); + } - return { - user_id: data.user_id, - username: data.username, - email: data.email, - sessionkey: data.sessionkey, - sessionkey_mobile: data.sessionkey_mobile, - last_update_timestamp: data.last_update_timestamp, - accounts: userAccounts.map((account) => { - return {...account, balance: ConvertUtils.convertBigIntegerToFloat(account.current_balance ?? 0)} - }), - }; - }, - getUserIdFromUsername: async (username: string): Promise => { - const whereCondition = {username}; - const selectCondition: Prisma.usersSelect = { - user_id: true, - } satisfies Prisma.usersSelect; + return { + user_id: data.user_id, + username: data.username, + email: data.email, + sessionkey: data.sessionkey, + sessionkey_mobile: data.sessionkey_mobile, + last_update_timestamp: data.last_update_timestamp, + accounts: userAccounts.map((account) => { + return { + ...account, + balance: ConvertUtils.convertBigIntegerToFloat(account.current_balance ?? 0), + }; + }), + }; + }, + getUserIdFromUsername: async (username: string): Promise => { + const whereCondition = { username }; + const selectCondition: Prisma.usersSelect = { + user_id: true, + } satisfies Prisma.usersSelect; - /* type UserPayload = Prisma.usersGetPayload<{select: typeof selectCondition}> */ + /* type UserPayload = Prisma.usersGetPayload<{select: typeof selectCondition}> */ - const user = await User.findUnique({ - where: whereCondition, - select: selectCondition, - }); - return (user as { user_id: bigint }).user_id; - }, - setupLastUpdateTimestamp: - async (userId, timestamp, prismaClient = prisma) => - prismaClient.users.update({ - where: {user_id: userId}, - data: {last_update_timestamp: timestamp}, - }), - changeUserPassword: - async ( - userId: bigint, - currentPassword: string, - newPassword: string, - mobile: boolean, - dbClient = prisma - ) => { - /* Check if current password is valid */ - const whereCondition = {user_id: userId}; - const data: Prisma.usersUpdateInput = await User.findUniqueOrThrow({ - where: whereCondition, - }).catch(() => { - throw APIError.notAuthorized('User Not Found'); - }); + const user = await User.findUnique({ + where: whereCondition, + select: selectCondition, + }); + return (user as { user_id: bigint }).user_id; + }, + setupLastUpdateTimestamp: async (userId, timestamp, prismaClient = prisma) => { + return prismaClient.users.update({ + where: { user_id: userId }, + data: { last_update_timestamp: timestamp }, + }); + }, + changeUserPassword: async ( + userId: bigint, + currentPassword: string, + newPassword: string, + mobile: boolean, + dbClient = prisma + ) => { + /* Check if current password is valid */ + const whereCondition = { user_id: userId }; + const data: Prisma.usersUpdateInput = await User.findUniqueOrThrow({ + where: whereCondition, + }).catch(() => { + throw APIError.notAuthorized('User Not Found'); + }); - const isValid = cryptoUtils.verifyPassword(currentPassword, data.password); - if (!isValid) { - throw APIError.notAuthorized('Wrong credentials'); - } + const isValid = cryptoUtils.verifyPassword(currentPassword, data.password); + if (!isValid) { + throw APIError.notAuthorized('Wrong credentials'); + } - /* Change the password */ - const hashedPassword = cryptoUtils.hashPassword(newPassword); - await dbClient.users.update({ - where: {user_id: userId}, - data: {password: hashedPassword}, - }); + /* Change the password */ + const hashedPassword = cryptoUtils.hashPassword(newPassword); + await dbClient.users.update({ + where: { user_id: userId }, + data: { password: hashedPassword }, + }); - await SessionManager.generateNewSessionKeyForUser(data.username as string, mobile); - }, - getFirstUserTransactionDate: - async ( - userId: bigint, - dbClient = prisma - ): Promise< - | { - date_timestamp: number; - month: number; - year: number; - } - | undefined - > => { - const data = await dbClient.$queryRaw`SELECT date_timestamp, + await SessionManager.generateNewSessionKeyForUser(data.username as string, mobile); + }, + getFirstUserTransactionDate: async ( + userId: bigint, + dbClient = prisma + ): Promise< + | { + date_timestamp: number; + month: number; + year: number; + } + | undefined + > => { + const data = await dbClient.$queryRaw`SELECT date_timestamp, MONTH(FROM_UNIXTIME(date_timestamp)) as 'month', YEAR(FROM_UNIXTIME(date_timestamp)) as 'year', entities.users_user_id @@ -139,50 +142,64 @@ const userService = { WHERE users_user_id = ${userId} ORDER BY date_timestamp ASC LIMIT 1`; - if (Array.isArray(data)) { - return data[0]; - } else return undefined; - }, - getUserCategoriesAndEntities: - async ( - userId: bigint, - dbClient = prisma - ): Promise => { - const categories = await CategoryService.getAllCategoriesForUser( - userId, - { - category_id: true, - name: true, - type: true, - }, - dbClient - ); + if (Array.isArray(data)) { + return data[0]; + } else return undefined; + }, + getUserCategoriesEntitiesTags: async ( + userId: bigint, + dbClient = prisma + ): Promise => { + const categories = await CategoryService.getAllCategoriesForUser( + userId, + { + category_id: true, + name: true, + type: true, + }, + dbClient + ); - const entities = await EntityService.getAllEntitiesForUser( - userId, - { - entity_id: true, - name: true, - }, - dbClient - ); + const entities = await EntityService.getAllEntitiesForUser( + userId, + { + entity_id: true, + name: true, + }, + dbClient + ); - return { - categories: categories.map((cat) => { - return { - category_id: cat.category_id as bigint, - name: cat.name as string, - type: cat.type as string, - }; - }), - entities: entities.map((ent) => { - return {entity_id: ent.entity_id as bigint, name: ent.name as string}; - }), - }; - }, - autoPopulateDemoData: - async (userId: bigint, dbClient = undefined) => DemoDataManager.createMockData(userId, dbClient), - } -; + const tags = await TagService.getAllTagsForUser( + userId, + { + tag_id: true, + name: true, + description: true, + }, + dbClient + ); + return { + categories: categories.map((cat) => { + return { + category_id: cat.category_id as bigint, + name: cat.name as string, + type: cat.type as string, + }; + }), + entities: entities.map((ent) => { + return { entity_id: ent.entity_id as bigint, name: ent.name as string }; + }), + tags: tags.map((tag) => { + return { + tag_id: tag.tag_id as bigint, + name: tag.name as string, + description: tag.description as string, + }; + }), + }; + }, + autoPopulateDemoData: async (userId: bigint, dbClient = undefined) => + DemoDataManager.createMockData(userId, dbClient), +}; export default userService;