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;