Compare commits
806 Commits
avoid-dele
...
v0.18.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69ddf43b3e | ||
|
|
249eadaeaa | ||
|
|
59168bc691 | ||
|
|
81b26c6f13 | ||
|
|
da435d85d9 | ||
|
|
533006b90e | ||
|
|
d096e49d45 | ||
|
|
73acdb6240 | ||
|
|
38d4122d11 | ||
|
|
24a77c81b3 | ||
|
|
7f41b4280e | ||
|
|
aa89653967 | ||
|
|
b80bc95fa5 | ||
|
|
9a5befbee7 | ||
|
|
b7487f19d3 | ||
|
|
cd9039fe16 | ||
|
|
87f60f7461 | ||
|
|
202179ec0b | ||
|
|
168883a933 | ||
|
|
f62ec83e29 | ||
|
|
eff8b41720 | ||
|
|
632cc3d72e | ||
|
|
aefdaac68d | ||
|
|
b8a0a5509d | ||
|
|
a5eb42edaf | ||
|
|
67b519db61 | ||
|
|
91730d204e | ||
|
|
8a09de9771 | ||
|
|
50861940a8 | ||
|
|
55caf037cd | ||
|
|
c95eec565d | ||
|
|
79616cf1eb | ||
|
|
c27458ebcc | ||
|
|
da0fab9a58 | ||
|
|
5bbcb7913d | ||
|
|
48ff93b6ab | ||
|
|
f816e7f25c | ||
|
|
3cd66ba4d6 | ||
|
|
5d5d4a1972 | ||
|
|
b01528c06b | ||
|
|
cb1f587637 | ||
|
|
978ce6c441 | ||
|
|
fab22c9820 | ||
|
|
7edf268e75 | ||
|
|
87bf29f28c | ||
|
|
d305c7ad32 | ||
|
|
7a9c7209bc | ||
|
|
d2d37820f5 | ||
|
|
1889969191 | ||
|
|
8c2888fcd8 | ||
|
|
47879d04b2 | ||
|
|
f1f52ce972 | ||
|
|
dad8aeaff1 | ||
|
|
5aae45c8a8 | ||
|
|
3e437a041c | ||
|
|
e783cfeafa | ||
|
|
5dde7f5584 | ||
|
|
8e0911ec85 | ||
|
|
66d2d6a612 | ||
|
|
8dc2b18707 | ||
|
|
589b29bbdd | ||
|
|
ca403872b3 | ||
|
|
738a84bb4b | ||
|
|
b37002bea6 | ||
|
|
b6deb842ff | ||
|
|
d3230767dd | ||
|
|
6c4b0cdac5 | ||
|
|
0b5cee070a | ||
|
|
906835c396 | ||
|
|
7b4afd3859 | ||
|
|
590715037b | ||
|
|
1e53a8e85e | ||
|
|
2ad77103ac | ||
|
|
c1fc70863b | ||
|
|
125dff8376 | ||
|
|
84da7b7df5 | ||
|
|
4c82f6f8ad | ||
|
|
0d7aad5448 | ||
|
|
74b74a2722 | ||
|
|
3a0a0db8a7 | ||
|
|
265ea9ca48 | ||
|
|
cfd37f8894 | ||
|
|
d1caa5c5ce | ||
|
|
d998d716b7 | ||
|
|
031ccc4a0b | ||
|
|
e4f61823b3 | ||
|
|
1cbc1c056f | ||
|
|
4d4ef54c56 | ||
|
|
f7fcfefc78 | ||
|
|
858f347fd4 | ||
|
|
4d73b59cf3 | ||
|
|
bc67f0cca8 | ||
|
|
ef2d1ff141 | ||
|
|
dc4cdb2a8f | ||
|
|
8862810706 | ||
|
|
3dadbeac4d | ||
|
|
494d2c1fe0 | ||
|
|
d27562bd43 | ||
|
|
8b99e0938d | ||
|
|
94192bfc29 | ||
|
|
708a4dda9e | ||
|
|
10fcf94c92 | ||
|
|
fc9995c4da | ||
|
|
7dc769004d | ||
|
|
5dbfd36415 | ||
|
|
044f11ff74 | ||
|
|
6afe1a09c6 | ||
|
|
909a70e2c5 | ||
|
|
84dd0fa86b | ||
|
|
a4719fe15b | ||
|
|
fd915b503f | ||
|
|
bbba54c08e | ||
|
|
f241e2bede | ||
|
|
175bc243f3 | ||
|
|
7c06c8bb8a | ||
|
|
8fd930caac | ||
|
|
e175307da4 | ||
|
|
b1bf932e88 | ||
|
|
aa897212ab | ||
|
|
890903e08b | ||
|
|
16b2a33cf6 | ||
|
|
382d4ab028 | ||
|
|
85f26e1079 | ||
|
|
1b237323f6 | ||
|
|
f15fecde54 | ||
|
|
79be4266bb | ||
|
|
08ad117331 | ||
|
|
958f78e7a4 | ||
|
|
ba77351e44 | ||
|
|
09a15966f0 | ||
|
|
7ff36e8c4f | ||
|
|
8ed24748ec | ||
|
|
6a6dcadaf9 | ||
|
|
308a4f62ae | ||
|
|
6e50de1d28 | ||
|
|
ceb133e29a | ||
|
|
e7871e34a9 | ||
|
|
cfdbcea9c0 | ||
|
|
fcd61c6159 | ||
|
|
2244cc6116 | ||
|
|
15dbc4137c | ||
|
|
8f904fae3a | ||
|
|
c8f31f33be | ||
|
|
dc5bdf0b66 | ||
|
|
fe41f7976d | ||
|
|
2c7da86a00 | ||
|
|
8c2b9fba29 | ||
|
|
7208b8fab5 | ||
|
|
0836fe14e0 | ||
|
|
1227111fae | ||
|
|
2ada57a2b4 | ||
|
|
e380c598d3 | ||
|
|
370a8a4b91 | ||
|
|
c9ba9500cc | ||
|
|
23d27cafc1 | ||
|
|
92e3d31360 | ||
|
|
8aefa7709c | ||
|
|
3020295841 | ||
|
|
7f31a48755 | ||
|
|
8c0ef61038 | ||
|
|
76bb82f2b4 | ||
|
|
4c0dc276dd | ||
|
|
a69c4b4067 | ||
|
|
d81e544e82 | ||
|
|
6eeda23559 | ||
|
|
cd046cbe27 | ||
|
|
9b7bc1e5b9 | ||
|
|
987341ed29 | ||
|
|
d7cad17f1b | ||
|
|
dd02ae471e | ||
|
|
a5bfb0b02b | ||
|
|
f4440c9a03 | ||
|
|
cb88c234d1 | ||
|
|
f6a0476fb4 | ||
|
|
63cdc45295 | ||
|
|
83a5010dc5 | ||
|
|
55aab76c9b | ||
|
|
495941f43a | ||
|
|
00a1e070c6 | ||
|
|
fab71d2b65 | ||
|
|
9504bb5ccd | ||
|
|
7e89966f20 | ||
|
|
8a96c41258 | ||
|
|
4a713980bf | ||
|
|
2a1cbf6ced | ||
|
|
9103b60653 | ||
|
|
b9fc0cdd9e | ||
|
|
4368c18479 | ||
|
|
7e5c6b6487 | ||
|
|
b7214044bb | ||
|
|
93cb3615c3 | ||
|
|
7abfa6a162 | ||
|
|
1372a1f0a8 | ||
|
|
484024ec28 | ||
|
|
46639c7b86 | ||
|
|
d10d1654c1 | ||
|
|
2f06070ecb | ||
|
|
deefdb9bfd | ||
|
|
3cc62d80de | ||
|
|
4962c5d4d3 | ||
|
|
571a332658 | ||
|
|
b44c318a5d | ||
|
|
bd9717f4dc | ||
|
|
f48aea8e5a | ||
|
|
0ac3a5dea9 | ||
|
|
56b40ad4cb | ||
|
|
9b6f934990 | ||
|
|
80e3522f8a | ||
|
|
7975643765 | ||
|
|
2ac7f86bdb | ||
|
|
956b9b6812 | ||
|
|
60248ec3f6 | ||
|
|
9d3f1541eb | ||
|
|
9b5f1a36ab | ||
|
|
8ee691e1ed | ||
|
|
f9cb14da9e | ||
|
|
5e87581f4e | ||
|
|
8ca9cf39da | ||
|
|
9001fea524 | ||
|
|
dea0d71732 | ||
|
|
c191c4bd26 | ||
|
|
47d82ce591 | ||
|
|
9321db2a3a | ||
|
|
e486333c96 | ||
|
|
a9748b23c0 | ||
|
|
693ae61141 | ||
|
|
9807ac04b0 | ||
|
|
bddfde4138 | ||
|
|
a39dcd00d5 | ||
|
|
4d616e9287 | ||
|
|
dc52fb1de5 | ||
|
|
21a1777424 | ||
|
|
16b721db91 | ||
|
|
079491823d | ||
|
|
f7a87a6e9c | ||
|
|
e0cdf42980 | ||
|
|
ee56653f4b | ||
|
|
2310b09778 | ||
|
|
0684e50ebd | ||
|
|
aaa8f39e50 | ||
|
|
af981ce630 | ||
|
|
a1f8417b5d | ||
|
|
086b060351 | ||
|
|
bbafdcd8bd | ||
|
|
dd9098bdc1 | ||
|
|
3851d34ba4 | ||
|
|
b9651f30d5 | ||
|
|
45b5fb4088 | ||
|
|
aa64bcf69b | ||
|
|
cbd867b334 | ||
|
|
1a8ca83786 | ||
|
|
80c14ba1a0 | ||
|
|
785045dbad | ||
|
|
291301c1e3 | ||
|
|
824e4e13d1 | ||
|
|
74da28b464 | ||
|
|
22a016b56e | ||
|
|
040f016273 | ||
|
|
8ab809fc71 | ||
|
|
f9e5028e0d | ||
|
|
7a3e121942 | ||
|
|
fc1d123c6b | ||
|
|
ad4e51d81d | ||
|
|
973d1832bd | ||
|
|
858e3541cb | ||
|
|
a5ab535d3b | ||
|
|
1d8cec5069 | ||
|
|
2baa667c5d | ||
|
|
aba06991d4 | ||
|
|
ff5730d8a7 | ||
|
|
a27c877321 | ||
|
|
c5063fc5b5 | ||
|
|
ab4c0ab7a7 | ||
|
|
084d9d3d10 | ||
|
|
daa1e3a6bd | ||
|
|
4270d66928 | ||
|
|
90b4f3ef6d | ||
|
|
1fc6445123 | ||
|
|
b1d5390bfc | ||
|
|
1ba26a3b85 | ||
|
|
2c98376162 | ||
|
|
b71c79fef5 | ||
|
|
2baf407814 | ||
|
|
83fbb7225d | ||
|
|
b9a00418fa | ||
|
|
62d3e386dd | ||
|
|
d87d674aba | ||
|
|
a17b4f6a8a | ||
|
|
db839137d0 | ||
|
|
b602f28696 | ||
|
|
68f2f4ee84 | ||
|
|
f23e8d98f6 | ||
|
|
9db03350e0 | ||
|
|
0273714a07 | ||
|
|
ea497bfdea | ||
|
|
685a6150e6 | ||
|
|
daf87a8ec7 | ||
|
|
5b4ddadcf6 | ||
|
|
ea8c5458ff | ||
|
|
0833baabda | ||
|
|
ab7eb40ea9 | ||
|
|
00e6c141ee | ||
|
|
0aea339c1c | ||
|
|
2cf75e0136 | ||
|
|
5a8c394ad7 | ||
|
|
71742c3480 | ||
|
|
b340776278 | ||
|
|
6ea870edb2 | ||
|
|
dd072f8585 | ||
|
|
ecfdba9634 | ||
|
|
5be3ca75f4 | ||
|
|
7239b0ec7e | ||
|
|
2d3544fe37 | ||
|
|
3faeca20a9 | ||
|
|
b713b6922b | ||
|
|
4e40009ba0 | ||
|
|
e09f8e4334 | ||
|
|
1752c32eec | ||
|
|
d16ec7cda9 | ||
|
|
f6f6bc31b6 | ||
|
|
c06a8d9ca3 | ||
|
|
3509026ad8 | ||
|
|
465bb66d6b | ||
|
|
06e78db49d | ||
|
|
d2e6cc0036 | ||
|
|
7690789031 | ||
|
|
27fed5f18a | ||
|
|
1fd8a53ed1 | ||
|
|
d67189587e | ||
|
|
d229378957 | ||
|
|
eb4491f44a | ||
|
|
09ad725a67 | ||
|
|
b11c531cf5 | ||
|
|
0868eeaa0e | ||
|
|
9395ef094a | ||
|
|
a8671a8d99 | ||
|
|
cd8f64dfdc | ||
|
|
17dbe9713b | ||
|
|
706a324121 | ||
|
|
ecaf8c99bb | ||
|
|
90bde20674 | ||
|
|
0c61f85707 | ||
|
|
0f678e61c5 | ||
|
|
374f1acf8a | ||
|
|
ada1428193 | ||
|
|
528d447443 | ||
|
|
d6d67b9a51 | ||
|
|
12740223a8 | ||
|
|
f7f77b12c9 | ||
|
|
a6db4fb6df | ||
|
|
b38020d397 | ||
|
|
d090d5a026 | ||
|
|
7b5287ee80 | ||
|
|
00d9bc537c | ||
|
|
c688190acc | ||
|
|
2e0b3d0d5e | ||
|
|
6d888060d3 | ||
|
|
299a943153 | ||
|
|
e0ddcb022a | ||
|
|
b940c6dd17 | ||
|
|
b43cd26ecc | ||
|
|
b9886cfac3 | ||
|
|
ba3ea93a2d | ||
|
|
6b77bda77f | ||
|
|
ba387e81f7 | ||
|
|
0414c090ea | ||
|
|
a52f3a933f | ||
|
|
d01eacf8d9 | ||
|
|
0da151faaa | ||
|
|
f93c8b46dc | ||
|
|
59c5c8979d | ||
|
|
74a07847a4 | ||
|
|
1740226294 | ||
|
|
1b20d1b073 | ||
|
|
ac7175d83b | ||
|
|
d72d8e60d6 | ||
|
|
de5920f910 | ||
|
|
475c4e9967 | ||
|
|
63708ae839 | ||
|
|
760dbc6cfc | ||
|
|
9d4e7cec9e | ||
|
|
21eb88ef53 | ||
|
|
c8e7a2c7d9 | ||
|
|
429159acf9 | ||
|
|
7eb84474a5 | ||
|
|
e42adcae63 | ||
|
|
abffdd1029 | ||
|
|
d052c23560 | ||
|
|
f02afd3c9f | ||
|
|
3df17390e2 | ||
|
|
df38f8893d | ||
|
|
8882bc677e | ||
|
|
f03d01113c | ||
|
|
8f431597d9 | ||
|
|
03bc78a068 | ||
|
|
ecf5d60db0 | ||
|
|
3a5fd2782a | ||
|
|
1d8416ebfe | ||
|
|
3672abe7a5 | ||
|
|
2753908b83 | ||
|
|
8495990ec2 | ||
|
|
471ce1b7af | ||
|
|
4a920176f4 | ||
|
|
74fd76ce77 | ||
|
|
c9f57d9a75 | ||
|
|
6c9810ea4c | ||
|
|
1e4b29f83c | ||
|
|
2b03ac7f16 | ||
|
|
2cb8c2932f | ||
|
|
2814d23f75 | ||
|
|
35fa2389e1 | ||
|
|
c18b450129 | ||
|
|
8db6e9f049 | ||
|
|
b63a2006d3 | ||
|
|
8c1ca503bd | ||
|
|
0ac79e661c | ||
|
|
66ba4d35aa | ||
|
|
46d25dff4c | ||
|
|
5ef99f2cb3 | ||
|
|
7aee76e461 | ||
|
|
79f3f1b63d | ||
|
|
26e104b9f1 | ||
|
|
c71836ec27 | ||
|
|
60b1bc9ed7 | ||
|
|
e6a3daa2c3 | ||
|
|
276ef1c907 | ||
|
|
83e48cce42 | ||
|
|
99ca683d13 | ||
|
|
5062d891e1 | ||
|
|
92d1fc670b | ||
|
|
ab7abfea35 | ||
|
|
0d15c16d40 | ||
|
|
2a85fe2f3c | ||
|
|
dc762567b5 | ||
|
|
a676e09537 | ||
|
|
c46948049c | ||
|
|
de1b7f132c | ||
|
|
90b552d5c9 | ||
|
|
39115b3000 | ||
|
|
3c8c3d8253 | ||
|
|
657400c671 | ||
|
|
6356cb5e63 | ||
|
|
13c6e7a62d | ||
|
|
b6d99b1d4b | ||
|
|
f0e15d43d3 | ||
|
|
50d5ddba8e | ||
|
|
d2c63878ed | ||
|
|
ef52eaf91a | ||
|
|
cd71900bdd | ||
|
|
34e97fc56b | ||
|
|
e6e119682b | ||
|
|
e096db2ab8 | ||
|
|
cdff641cdb | ||
|
|
d0126ffc2c | ||
|
|
fe043eb594 | ||
|
|
eb1ff7c151 | ||
|
|
a862ee9ccf | ||
|
|
6953f7c4a3 | ||
|
|
ad53ddb9dd | ||
|
|
cfd4540a65 | ||
|
|
217321380a | ||
|
|
33e5d1a979 | ||
|
|
e5bcb1c19a | ||
|
|
bf2dc844d2 | ||
|
|
c9d3a3cd7c | ||
|
|
33afa3af64 | ||
|
|
fb70c94465 | ||
|
|
0f39cfb3af | ||
|
|
7fc1320834 | ||
|
|
6091bc73cf | ||
|
|
30b5f8120a | ||
|
|
5a958cc9fa | ||
|
|
d15c5890ed | ||
|
|
151aff4c8e | ||
|
|
b167284c8e | ||
|
|
b75d44a3dd | ||
|
|
f0f0045353 | ||
|
|
50164873ce | ||
|
|
a2cbab0bc3 | ||
|
|
5027f9fe5d | ||
|
|
eeb67d4005 | ||
|
|
a3d9e8ef2b | ||
|
|
121d992b68 | ||
|
|
6634144d82 | ||
|
|
078a7ea51c | ||
|
|
e070ac72dd | ||
|
|
08ac5f4b01 | ||
|
|
e4a7f09dbc | ||
|
|
2c5537efad | ||
|
|
017908600e | ||
|
|
6307ca8935 | ||
|
|
9531730d7a | ||
|
|
1ed1c9ea1d | ||
|
|
d40de4d22b | ||
|
|
ee62e3e1c2 | ||
|
|
5df454dd30 | ||
|
|
07628ddc37 | ||
|
|
69afa07e3b | ||
|
|
b1a043f699 | ||
|
|
9eaff0785b | ||
|
|
b3a97ed5d5 | ||
|
|
1aaa65edd2 | ||
|
|
efebf424d1 | ||
|
|
597973d5d1 | ||
|
|
ee6bc822c9 | ||
|
|
07e78ebd6a | ||
|
|
8c04a6205b | ||
|
|
d54ea68d46 | ||
|
|
f42c8031c2 | ||
|
|
5261d332b7 | ||
|
|
f1e7d0fc44 | ||
|
|
1d1049043e | ||
|
|
1148fef9ad | ||
|
|
92ac0dbd25 | ||
|
|
ce41845bd7 | ||
|
|
eaf72d1608 | ||
|
|
ac336f9878 | ||
|
|
746c80c564 | ||
|
|
601434b107 | ||
|
|
5aaa33e585 | ||
|
|
d48d864a5f | ||
|
|
453df2ac4e | ||
|
|
73ceeaee46 | ||
|
|
1b4b656419 | ||
|
|
df823c0bfe | ||
|
|
e91d7b0cff | ||
|
|
aa52e7d02c | ||
|
|
4e53d08497 | ||
|
|
2356921f27 | ||
|
|
a0a5d00be3 | ||
|
|
fbd74c559b | ||
|
|
8a64198433 | ||
|
|
b98b73ad98 | ||
|
|
6abae43c6f | ||
|
|
7657337c4f | ||
|
|
983ceb5cc6 | ||
|
|
ac072d29fc | ||
|
|
17e055db5e | ||
|
|
b49a021506 | ||
|
|
b49b45fb43 | ||
|
|
bb7cf41e3e | ||
|
|
801ea5dfdb | ||
|
|
eb03a38553 | ||
|
|
0852feecbf | ||
|
|
54dcde657f | ||
|
|
5bb95eeb1a | ||
|
|
6baec8dd96 | ||
|
|
6535424d0f | ||
|
|
09d73db20f | ||
|
|
7f746b96c8 | ||
|
|
ed2bca6b74 | ||
|
|
f9d5a3c69a | ||
|
|
84445d4bac | ||
|
|
75d8864aae | ||
|
|
cb6dab08d8 | ||
|
|
5112ef9b64 | ||
|
|
a630e8a612 | ||
|
|
4df63561cf | ||
|
|
c7a3bac44c | ||
|
|
251c54be60 | ||
|
|
5dec4a7df0 | ||
|
|
0d57ca88bf | ||
|
|
b9be83dc2b | ||
|
|
321de4d327 | ||
|
|
4e66d1ac98 | ||
|
|
b5fe5a8bcb | ||
|
|
508054b594 | ||
|
|
34efd58f34 | ||
|
|
f898acdb8b | ||
|
|
f7b53692f5 | ||
|
|
b665d05526 | ||
|
|
de5300b186 | ||
|
|
abc5631ac2 | ||
|
|
9e7b906c86 | ||
|
|
d5decbbd0b | ||
|
|
fbeb489128 | ||
|
|
5bf8a9e0ff | ||
|
|
68fa5cf5c5 | ||
|
|
b1662c3175 | ||
|
|
0fcee0eaa7 | ||
|
|
5b2be2ac19 | ||
|
|
5bb80fde34 | ||
|
|
58f90a0bcd | ||
|
|
e1a3510f0b | ||
|
|
172eea0ad1 | ||
|
|
74c4418549 | ||
|
|
6b6e19f53b | ||
|
|
01f7effc71 | ||
|
|
d1121f0b81 | ||
|
|
a7644e6481 | ||
|
|
d6f56568a3 | ||
|
|
04d134806b | ||
|
|
26c6ca9e36 | ||
|
|
ffef627dc3 | ||
|
|
f105980f08 | ||
|
|
efad38fcdc | ||
|
|
d84568e43a | ||
|
|
ed6517c0e1 | ||
|
|
0fd256c801 | ||
|
|
f0285560aa | ||
|
|
7a33f79268 | ||
|
|
ef5ef647d4 | ||
|
|
339559d830 | ||
|
|
22e4d757e4 | ||
|
|
ce62a0524c | ||
|
|
3b98e8de80 | ||
|
|
278c8a01c5 | ||
|
|
f22dc9a18b | ||
|
|
b224a2c313 | ||
|
|
d4a933ef18 | ||
|
|
8b0feb9022 | ||
|
|
92f929152f | ||
|
|
da514403a1 | ||
|
|
d57f9e5171 | ||
|
|
0a1299b8a6 | ||
|
|
326a2038e7 | ||
|
|
94bb153120 | ||
|
|
223756b7ae | ||
|
|
a773ee9966 | ||
|
|
47790fba84 | ||
|
|
b122fdc43a | ||
|
|
cb93f313ec | ||
|
|
74c31c5f20 | ||
|
|
d411ae3e68 | ||
|
|
aecef878ba | ||
|
|
ec90056f7b | ||
|
|
30f2f1fb4c | ||
|
|
c72f5374f3 | ||
|
|
66de03b143 | ||
|
|
2b33583a03 | ||
|
|
e5611b4446 | ||
|
|
d12157a8d4 | ||
|
|
b24badfa52 | ||
|
|
c992562760 | ||
|
|
485138344c | ||
|
|
8d990ae85d | ||
|
|
7fbe51ddf2 | ||
|
|
215eb97762 | ||
|
|
3d4fd0b904 | ||
|
|
e552ff6449 | ||
|
|
268942af42 | ||
|
|
02489a907a | ||
|
|
4e0037d1c0 | ||
|
|
39786e5b1f | ||
|
|
6373862044 | ||
|
|
1411f64cf6 | ||
|
|
2c9739ac91 | ||
|
|
e214b86a62 | ||
|
|
b01b7010c0 | ||
|
|
e7cd035206 | ||
|
|
f82b78c4eb | ||
|
|
847b4380be | ||
|
|
271011cb3c | ||
|
|
b6d8766173 | ||
|
|
efffdc021b | ||
|
|
01dd0ffb8c | ||
|
|
d910985b37 | ||
|
|
d5799bf720 | ||
|
|
56cc1da034 | ||
|
|
ef9b4ebad6 | ||
|
|
29af788dcd | ||
|
|
b2510145dc | ||
|
|
5f3a309a8f | ||
|
|
e2fdc13b3e | ||
|
|
34cd21cced | ||
|
|
0e589ace82 | ||
|
|
f46f595e96 | ||
|
|
53ef940b05 | ||
|
|
65495775d4 | ||
|
|
1cf11e020f | ||
|
|
b46154ba59 | ||
|
|
59e3a4016b | ||
|
|
b6a1c9ab4b | ||
|
|
7171fb2a69 | ||
|
|
c64a14aef3 | ||
|
|
ac539aed34 | ||
|
|
c7b4846cb0 | ||
|
|
d54aac9b32 | ||
|
|
fe87713df0 | ||
|
|
4770fdf709 | ||
|
|
1b3c525ba5 | ||
|
|
b35d22d3b3 | ||
|
|
44fce6f33e | ||
|
|
e58a1d6ad1 | ||
|
|
5f191cf335 | ||
|
|
46bf1cc39a | ||
|
|
c98fe00f88 | ||
|
|
4b95c19d3e | ||
|
|
eadaac30d6 | ||
|
|
ca4d543482 | ||
|
|
1f3adf4879 | ||
|
|
532ad61500 | ||
|
|
a44d779670 | ||
|
|
ab5f9f50d0 | ||
|
|
f3f10db6db | ||
|
|
de5694681b | ||
|
|
b1a997c287 | ||
|
|
3e36146bce | ||
|
|
db833888c8 | ||
|
|
c415e3d693 | ||
|
|
e145eabf02 | ||
|
|
caab21647d | ||
|
|
98528e9e5b | ||
|
|
b993fad37f | ||
|
|
94ea44b58e | ||
|
|
877a57043a | ||
|
|
70415d1d63 | ||
|
|
ef2d1977d6 | ||
|
|
46ea26891d | ||
|
|
f750cede89 | ||
|
|
e5d0f16096 | ||
|
|
706694c768 | ||
|
|
d1a09e3b15 | ||
|
|
01c27b56ef | ||
|
|
0d8fb8cf25 | ||
|
|
6562e3ab8c | ||
|
|
c93650ffd3 | ||
|
|
e6a2825065 | ||
|
|
0c2a0b0010 | ||
|
|
a332c51249 | ||
|
|
0b6d0bc016 | ||
|
|
e6336b1451 | ||
|
|
46290c4d37 | ||
|
|
ff2b7563c8 | ||
|
|
b9572420ed | ||
|
|
35ebb9c2aa | ||
|
|
3ebeb29dc0 | ||
|
|
8e98068538 | ||
|
|
6a72594faf | ||
|
|
728729094a | ||
|
|
93d540fbd2 | ||
|
|
eb9b6ce717 | ||
|
|
f716d42d26 | ||
|
|
1c4c364f06 | ||
|
|
162ad91547 | ||
|
|
2950e5ede4 | ||
|
|
73b041d8d2 | ||
|
|
7bf008a9cb | ||
|
|
4d9e3ccfb4 | ||
|
|
1bfe19f26c | ||
|
|
a371cedb67 | ||
|
|
4ed9c36ebd | ||
|
|
e24b23ce7e | ||
|
|
19fe6e2423 | ||
|
|
aec09f178b | ||
|
|
ffe51bae07 | ||
|
|
68231d5edb | ||
|
|
e1ea5c402c | ||
|
|
34b2c2c8b4 | ||
|
|
5d96fe6aa0 | ||
|
|
d2b5084b42 | ||
|
|
81fb0734d5 | ||
|
|
3639ce44e5 | ||
|
|
a7c00d60d5 | ||
|
|
932750b62d | ||
|
|
c90ffed67f | ||
|
|
e92c4486aa | ||
|
|
aaceea5338 | ||
|
|
4d54d180bc | ||
|
|
8fdd98e34d | ||
|
|
d53c5ee5e6 | ||
|
|
4082e4e2b8 | ||
|
|
0c689459cb | ||
|
|
40ef02f215 | ||
|
|
d369f0bb17 | ||
|
|
425d0293cc | ||
|
|
b621650975 | ||
|
|
40948160fe | ||
|
|
aa6b9dd295 | ||
|
|
05c2232b97 | ||
|
|
8f6325d529 | ||
|
|
0aa681043d | ||
|
|
40bddfdfeb | ||
|
|
d6e2f01d70 | ||
|
|
2344d3d34d | ||
|
|
883c5dcb41 | ||
|
|
be10b8934d | ||
|
|
ce38c71fa7 | ||
|
|
1162fbc7c3 | ||
|
|
18b9e25f2b | ||
|
|
dd26bdc482 | ||
|
|
ad3c9ebfe9 | ||
|
|
36611652da | ||
|
|
06c7ee71b4 | ||
|
|
54d3188666 | ||
|
|
3ceb9adda2 | ||
|
|
1249415054 | ||
|
|
4d44ce4c7f | ||
|
|
6c96c371c5 | ||
|
|
6c61a69f10 | ||
|
|
981b65349d | ||
|
|
a7d29a31c8 | ||
|
|
c1d92b74f0 | ||
|
|
6f0f47f38a | ||
|
|
83510cfa70 | ||
|
|
903dc0522a | ||
|
|
eecbcacb90 | ||
|
|
cfbe4cfea0 | ||
|
|
8f039b77e7 | ||
|
|
672a1bbb82 | ||
|
|
b2f3585047 | ||
|
|
e6434ea2d1 | ||
|
|
a21d6a37e4 | ||
|
|
e9fdffa9d9 | ||
|
|
920c8ea95c | ||
|
|
8de3717587 |
143
.all-contributorsrc
Normal file
143
.all-contributorsrc
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"commitType": "docs",
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "abouolia",
|
||||
"name": "Ahmed Bouhuolia",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2197422?v=4",
|
||||
"profile": "https://github.com/abouolia",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ameir",
|
||||
"name": "Ameir Abdeldayem",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/374330?v=4",
|
||||
"profile": "http://ameir.net",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "elforjani13",
|
||||
"name": "ElforJani13",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/39470382?v=4",
|
||||
"profile": "https://github.com/elforjani13",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "scheibling",
|
||||
"name": "Lars Scheibling",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/24367830?v=4",
|
||||
"profile": "https://scheibling.se",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "suhaibaffan",
|
||||
"name": "Suhaib Affan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18115937?v=4",
|
||||
"profile": "https://github.com/suhaibaffan",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "KalliopiPliogka",
|
||||
"name": "Kalliopi Pliogka",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/81677549?v=4",
|
||||
"profile": "https://github.com/KalliopiPliogka",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kochie",
|
||||
"name": "Robert Koch",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10809884?v=4",
|
||||
"profile": "https://me.kochie.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cschuijt",
|
||||
"name": "Casper Schuijt",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5460015?v=4",
|
||||
"profile": "http://cschuijt.nl",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ANasouf",
|
||||
"name": "ANasouf",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/19536487?v=4",
|
||||
"profile": "https://github.com/ANasouf",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "xprnio",
|
||||
"name": "Ragnar Laud",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3042904?v=4",
|
||||
"profile": "https://ragnarlaud.dev",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "asenawritescode",
|
||||
"name": "Asena",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/67445192?v=4",
|
||||
"profile": "https://github.com/asenawritescode",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "benpsnyder",
|
||||
"name": "Ben Snyder",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/707567?v=4",
|
||||
"profile": "https://snyder.tech",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cloudsbird",
|
||||
"name": "Vederis Leunardus",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13505006?v=4",
|
||||
"profile": "http://vederis.id",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ccantrell72",
|
||||
"name": "Chris Cantrell",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/104120598?v=4",
|
||||
"profile": "http://www.pivoten.com",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true,
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"projectName": "bigcapital",
|
||||
"projectOwner": "bigcapitalhq"
|
||||
}
|
||||
83
.env.example
83
.env.example
@@ -8,27 +8,84 @@ MAIL_FROM_NAME=
|
||||
MAIL_FROM_ADDRESS=
|
||||
|
||||
# Database
|
||||
DB_USER=
|
||||
DB_HOST=
|
||||
DB_PASSWORD=
|
||||
DB_CHARSET=
|
||||
DB_HOST=localhost
|
||||
DB_USER=bigcapital
|
||||
DB_PASSWORD=bigcapital
|
||||
DB_ROOT_PASSWORD=root
|
||||
DB_CHARSET=utf8
|
||||
|
||||
# System database
|
||||
SYSTEM_DB_NAME=bigcapital_system
|
||||
# SYSTEM_DB_USER=
|
||||
# SYSTEM_DB_PASSWORD=
|
||||
# SYSTEM_DB_NAME=
|
||||
# SYSTEM_DB_CHARSET=
|
||||
|
||||
# Tenants databases
|
||||
# Tenant databases
|
||||
TENANT_DB_NAME_PERFIX=bigcapital_tenant_
|
||||
|
||||
# MongoDB
|
||||
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
|
||||
|
||||
# Authentication
|
||||
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
|
||||
# TENANT_DB_HOST=
|
||||
# TENANT_DB_USER=
|
||||
# TENANT_DB_PASSWORD=
|
||||
# TENANT_DB_CHARSET=
|
||||
|
||||
# Application
|
||||
BASE_URL=https://bigcapital.ly
|
||||
CONTACT_US_MAIL=support@bigcapital.ly
|
||||
BASE_URL=http://example.com
|
||||
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
|
||||
|
||||
# Jobs MongoDB
|
||||
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
|
||||
|
||||
# App proxy
|
||||
PUBLIC_PROXY_PORT=80
|
||||
PUBLIC_PROXY_SSL_PORT=443
|
||||
|
||||
# Agendash
|
||||
AGENDASH_AUTH_USER=agendash
|
||||
AGENDASH_AUTH_PASSWORD=123123
|
||||
|
||||
# Sign-up restrictions
|
||||
SIGNUP_DISABLED=false
|
||||
SIGNUP_ALLOWED_DOMAINS=
|
||||
SIGNUP_ALLOWED_EMAILS=
|
||||
|
||||
# Sign-up Email Confirmation
|
||||
SIGNUP_EMAIL_CONFIRMATION=false
|
||||
|
||||
# API rate limit (points,duration,block duration).
|
||||
API_RATE_LIMIT=120,60,600
|
||||
|
||||
# Gotenberg API for PDF printing - (production).
|
||||
GOTENBERG_URL=http://gotenberg:3000
|
||||
GOTENBERG_DOCS_URL=http://server:3000/public/
|
||||
|
||||
# Gotenberg API - (development)
|
||||
# GOTENBERG_URL=http://localhost:9000
|
||||
# GOTENBERG_DOCS_URL=http://host.docker.internal:3000/public/
|
||||
|
||||
# Exchange Rate Service
|
||||
EXCHANGE_RATE_SERVICE=open-exchange-rate
|
||||
|
||||
# Open Exchange Rate
|
||||
OPEN_EXCHANGE_RATE_APP_ID=
|
||||
|
||||
# The Plaid environment to use ('sandbox' or 'development').
|
||||
# https://plaid.com/docs/#api-host
|
||||
PLAID_ENV=sandbox
|
||||
|
||||
# Your Plaid keys, which can be found in the Plaid Dashboard.
|
||||
# https://dashboard.plaid.com/account/keys
|
||||
PLAID_CLIENT_ID=
|
||||
PLAID_SECRET=
|
||||
PLAID_LINK_WEBHOOK=
|
||||
|
||||
# https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key
|
||||
LEMONSQUEEZY_API_KEY=
|
||||
LEMONSQUEEZY_STORE_ID=
|
||||
LEMONSQUEEZY_WEBHOOK_SECRET=
|
||||
|
||||
# S3 documents and attachments
|
||||
S3_REGION=US
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_ENDPOINT=
|
||||
S3_BUCKET=
|
||||
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
docker/nginx/scripts/build-nginx.sh text eol=lf
|
||||
docker/mariadb/docker-entrypoint.sh text eol=lf
|
||||
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: Bigcapital # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
94
.github/workflows/build-deploy-container.yml
vendored
94
.github/workflows/build-deploy-container.yml
vendored
@@ -6,43 +6,66 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
WEBAPP_IMAGE_NAME: bigcapital/bigcapital-webapp
|
||||
SERVER_IMAGE_NAME: bigcapital/bigcapital-server
|
||||
WEBAPP_IMAGE_NAME: bigcapitalhq/webapp
|
||||
SERVER_IMAGE_NAME: bigcapitalhq/server
|
||||
|
||||
jobs:
|
||||
build-publish-webapp:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
name: Build and deploy webapp container
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Login to Container registry.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.WEBAPP_IMAGE_NAME }}
|
||||
images: ${{ env.WEBAPP_IMAGE_NAME }}
|
||||
|
||||
# Builds and push the Docker image.
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/webapp/Dockerfile
|
||||
push: true
|
||||
tags: ghcr.io/bigcapitalhq/webapp:latest
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
context: ./
|
||||
file: ./packages/webapp/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: bigcapitalhq/webapp:latest, bigcapitalhq/webapp:${{github.ref_name}}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-webapp
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
# Send notification to Slack channel.
|
||||
- name: Slack Notification built and published webapp container successfully.
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
@@ -53,29 +76,52 @@ jobs:
|
||||
name: Build and deploy server container
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Login to Container registry.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# Builds and push the Docker image.
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: ./
|
||||
file: ./packages/server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/bigcapitalhq/server:latest
|
||||
tags: bigcapitalhq/server:latest, bigcapitalhq/server:${{github.ref_name}}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-server
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Send notification to Slack channel.
|
||||
- name: Slack Notification built and published server container successfully.
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
127
.github/workflows/build-deploy-develop-container.yaml
vendored
Normal file
127
.github/workflows/build-deploy-develop-container.yaml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
# This workflow will build a docker container, publish it to Github Registry.
|
||||
name: Build and Deploy Develop Docker Container
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
env:
|
||||
WEBAPP_IMAGE_NAME: bigcapitalhq/webapp
|
||||
SERVER_IMAGE_NAME: bigcapitalhq/server
|
||||
|
||||
jobs:
|
||||
build-publish-webapp:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
name: Build and deploy webapp container
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Login to Container registry.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.WEBAPP_IMAGE_NAME }}
|
||||
|
||||
# Builds and push the Docker image.
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: ./
|
||||
file: ./packages/webapp/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: bigcapitalhq/webapp:develop
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-webapp
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
# Send notification to Slack channel.
|
||||
- name: Slack Notification built and published webapp container successfully.
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
build-publish-server:
|
||||
name: Build and deploy server container
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Login to Container registry.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# Builds and push the Docker image.
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: ./
|
||||
file: ./packages/server/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: bigcapitalhq/server:develop
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-server
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Send notification to Slack channel.
|
||||
- name: Slack Notification built and published server container successfully.
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
4
.github/workflows/e2e.yml
vendored
4
.github/workflows/e2e.yml
vendored
@@ -8,14 +8,14 @@ on:
|
||||
- '**.ts'
|
||||
- '**.tsx'
|
||||
- '**/tsconfig.json'
|
||||
- 'yarn.lock'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.github/workflows/e2e.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.ts'
|
||||
- '**.tsx'
|
||||
- '**/tsconfig.json'
|
||||
- 'yarn.lock'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.github/workflows/e2e.yml'
|
||||
|
||||
defaults:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,4 +1,9 @@
|
||||
node_modules/
|
||||
data
|
||||
|
||||
# Docker volumes data directory
|
||||
/data
|
||||
|
||||
# Production env file
|
||||
.env
|
||||
|
||||
test-results/
|
||||
22
.gitpod.yml
Normal file
22
.gitpod.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
tasks:
|
||||
- name: Init
|
||||
init: |
|
||||
pnpm install &&
|
||||
cp .env.example .env &&
|
||||
docker-compose up -d &&
|
||||
pnpm run build:server &&
|
||||
node packages/server/build/commands.js system:migrate:latest
|
||||
command: |
|
||||
docker-compose up -d &&
|
||||
pnpm run dev
|
||||
|
||||
ports:
|
||||
- port: 4000
|
||||
visibility: public
|
||||
onOpen: open-preview
|
||||
- port: 3000
|
||||
visibility: public
|
||||
onOpen: ignore
|
||||
- port: 3306
|
||||
visibility: public
|
||||
onOpen: ignore
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn commitlint --edit
|
||||
pnpx commitlint --edit
|
||||
|
||||
312
CHANGELOG.md
312
CHANGELOG.md
@@ -2,6 +2,318 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
## [v0.17.5] - 17-06-2024
|
||||
|
||||
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501
|
||||
* fix: add space between buttons on floating actions bar by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/508
|
||||
* feat: Migrating to Envoy proxy instead of Nginx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/509
|
||||
* fix: Disable email confirmation does not work with invited users by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/497
|
||||
* feat: Setting up the date format in the whole system dates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/506
|
||||
|
||||
## [0.17.0] - 04-06-2024
|
||||
|
||||
### New
|
||||
|
||||
* feat: Upload and attach documents by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/461
|
||||
* feat: Export resource tables to pdf by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/460
|
||||
* feat: Build and deploy develop Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/476
|
||||
* feat: Internal docker virtual network by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/478
|
||||
|
||||
### Fixes
|
||||
|
||||
* fix: Skip send confirmation email if disabled by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/459
|
||||
* fix: Lemon Squeezy redirect to base url by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/479
|
||||
* fix: Organize Plaid env variables for development and sandbox envs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/480
|
||||
* fix: Plaid syncs deposit imports as withdrawals by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/481
|
||||
* fix: Validate the s3 configures exist by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/482
|
||||
* fix: Run migrations only for initialized tenants by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/484
|
||||
|
||||
## [0.16.16] -
|
||||
|
||||
* feat: handle http exceptions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/456
|
||||
* feat: add the missing Newrelic env vars to docker-compose.prod file by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/457
|
||||
* fix: add the signup email confirmation env var by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/458
|
||||
|
||||
## [0.16.14] -
|
||||
|
||||
* fix: Typo in setup wizard by @ccantrell72 in https://github.com/bigcapitalhq/bigcapital/pull/440
|
||||
* fix: Showing the real mail address on email confirmation view by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/445
|
||||
* fix: Auto-increment setting parsing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/453
|
||||
|
||||
## [0.16.12] -
|
||||
|
||||
* feat: Create a manifest list for `webapp` Docker image and push it to DockerHub. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/436
|
||||
* feat: Combine arm64 and amd64 in one Github action runner by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/437
|
||||
|
||||
## [0.16.11] - 06-05-2024
|
||||
|
||||
### improvements
|
||||
|
||||
* feat: Export resource data to csv, xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/430
|
||||
* feat: User email verification after signing-up. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/426
|
||||
|
||||
### Fixes
|
||||
* feat(repo): upgrade to latest lerna v8 and pnpm v9 by @benpsnyder in https://github.com/bigcapitalhq/bigcapital/pull/414
|
||||
* feat: Update Docker Build-Push Action and Add ARM64 Support by @cloudsbird in https://github.com/bigcapitalhq/bigcapital/pull/412
|
||||
* feat: Pushing docker containers by version tag by @cloudsbird in https://github.com/bigcapitalhq/bigcapital/pull/421
|
||||
|
||||
## [0.16.10]
|
||||
|
||||
* fix: Running migration Docker container on Windows by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/432
|
||||
|
||||
## [0.16.9]
|
||||
|
||||
* feat: New Relic for tracking by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/429
|
||||
|
||||
## [0.16.8]
|
||||
|
||||
* feat: Ability to enable/disable the bank connect feature by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/423
|
||||
|
||||
## [0.16.6]
|
||||
|
||||
* hotfix: fix the subscription plan when subscribe on cloud by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/422
|
||||
|
||||
## [0.16.5]
|
||||
|
||||
IMPORTANT: If you upgraded to the v0.16 recently you should upgrade to v0.16.4 as soon as possible, because there're some breaking changes affected the sign-in and some users reported couldn't sign-in.
|
||||
|
||||
* feat: Seed free subscription to tenants that have no subscription. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/410
|
||||
|
||||
## [0.16.3]
|
||||
|
||||
* feat: Integrate Lemon Squeezy payment by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/402
|
||||
* feat: optimize the onboarding subscription experience. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/404
|
||||
* feat: subscription page content by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/405
|
||||
* feat: auto subscribe to free plan once signup on community version. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/406
|
||||
* chore: add default value to env variable by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/407
|
||||
* fix: absolute storage imports path. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/408
|
||||
|
||||
## [0.16.0]
|
||||
|
||||
* feat: add convert to invoice button on estimate drawer toolbar by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/361
|
||||
* feat(webapp): add mark as delivered to action bar of invoice details … by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/360
|
||||
* feat(webapp): Dialog to choose the bank service provider by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/378
|
||||
* feat: Categorize the bank synced transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/377
|
||||
* feat: uncategorize the cashflow transaction by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/381
|
||||
* Import resources from csv/xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/382
|
||||
* feat(webapp): import resource UI by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/386
|
||||
* fix: import resources improvements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/388
|
||||
* feat: add sample sheet to accounts and bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/389
|
||||
* fix: show the unique row value in the import preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/392
|
||||
* feat: advanced parser for numeric and boolean import values by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/394
|
||||
* feat: validate the given imported sheet whether is empty by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/395
|
||||
* feat: linking relation with id in importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/393
|
||||
* feat: Aggregate rows import by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/396
|
||||
* feat: clean up the imported temp files by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/400
|
||||
* feat: add hints to import fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/401
|
||||
|
||||
## [0.15.0]
|
||||
|
||||
* feat: Printing financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/363
|
||||
* feat: Convert invoice status after sending mail notification by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/332
|
||||
* feat: Bigcapital <> Plaid Integration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/346
|
||||
* fix: Broken transactions by vendor report by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/369
|
||||
* fix: Optimize the print style some financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/370
|
||||
|
||||
## [0.14.0] - 30-01-2024
|
||||
|
||||
* feat: purchases by items exporting by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/327
|
||||
* fix: expense amounts should not be rounded by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/339
|
||||
* feat: get latest exchange rate from third party services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/340
|
||||
* fix(webapp): inconsistency in currency of universal search items by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/335
|
||||
* hotfix: editing sales and expense transactions don't reflect GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/342
|
||||
|
||||
## [0.13.3] - 22-01-2024
|
||||
|
||||
* hotfix(server): Unhandled thrown errors of services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/329
|
||||
|
||||
## [0.13.2] - 21-01-2024
|
||||
|
||||
* feat: show customer / vendor balance. by @asenawritescode in https://github.com/bigcapitalhq/bigcapital/pull/311
|
||||
* feat: inventory valuation csv and xlsx export by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/308
|
||||
* feat: sales by items export csv & xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/310
|
||||
* fix(server): the invoice and payment receipt printing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/315
|
||||
* fix: get cashflow transaction broken cause transaction type by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/318
|
||||
* fix: `AccountActivateAlert` import by @xprnio in https://github.com/bigcapitalhq/bigcapital/pull/322
|
||||
|
||||
## [0.13.1] - 15-01-2024
|
||||
|
||||
* feat(webapp): add approve/reject to action bar of estimate details dr… by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/304
|
||||
* docs: add ANasouf as a contributor for code by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/305
|
||||
* feat: Export general ledger & Journal to CSV and XLSX by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/303
|
||||
* feat: Auto re-calculate the items rate once changing the invoice exchange rate. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/270
|
||||
|
||||
## [0.13.0] - 31-12-2023
|
||||
|
||||
* feat: Send an invoice mail the customer email by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/292
|
||||
* fix: Allow non-numeric postal codes by @cschuijt in https://github.com/bigcapitalhq/bigcapital/pull/294
|
||||
* docs: add cschuijt as a contributor for bug by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/295
|
||||
|
||||
## [0.12.1] - 17-11-2023
|
||||
|
||||
* feat: Add default customer message and terms conditions to the transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/291
|
||||
* fix: The currency code of transaction tax rate entry by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/293
|
||||
|
||||
## [0.12.0] - 04-11-2023
|
||||
|
||||
* feat: Export reports via CSV and XLSX by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/286
|
||||
* fix: Axios upgrade by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/288
|
||||
* fix(server): Allow decimal amount in sale/purchase transactions. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/289
|
||||
* feat: Optimize invoice documents printing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/280
|
||||
* chore(deps): bump axios from 0.20.0 to 1.6.0 in /packages/server by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/284
|
||||
* chore(deps): bump axios from 0.20.0 to 1.6.0 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/283
|
||||
|
||||
## [0.11.0] - 28-10-2023
|
||||
|
||||
* feat: Migrate to pnpm by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/253
|
||||
* feat: Integrate tax rates to bills by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/260
|
||||
* feat: Assign default sell/purchase tax rates to items by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/261
|
||||
* chore(deps-dev): bump @babel/traverse from 7.23.0 to 7.23.2 in /packages/server by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/272
|
||||
* feat: Improve financial statements rows color by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/276
|
||||
* fix: Trial balance sheet adjusted balance by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/273
|
||||
* feat: Adds tax numbers to organization and customers by @kochie in https://github.com/bigcapitalhq/bigcapital/pull/269
|
||||
* docs: Add kochie as a contributor for code by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/277
|
||||
* feat: Computed Net Income under Equity in Balance Sheet report. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/271
|
||||
* fix: Change Dockerfile files with new pnpm by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/278
|
||||
|
||||
## [0.10.2] - 02-10-2023
|
||||
|
||||
fix(webapp): Disable tax rates from item entries editor table services do not support tax rates (https://github.com/bigcapitalhq/bigcapital/commit/69afa07e3ba45495a4cab3490c15f2b0c40c4790) by @abouolia
|
||||
fix(server): Add missing method in ItemEntry model (https://github.com/bigcapitalhq/bigcapital/commit/07628ddc37f46c98959ced0323f28752e0a98944) by @abouolia
|
||||
|
||||
## [0.10.1] - 25-09-2023
|
||||
|
||||
* Fix: Running tenants migration on Docker migration container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/242
|
||||
|
||||
## [0.10.0] - 24-09-2023
|
||||
|
||||
* Added: Tax rates service by @abouolia @elforjani13 in https://github.com/bigcapitalhq/bigcapital/pull/204
|
||||
* Added: Sales Tax Liability Summary report by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/204
|
||||
* Added: Tax rates tracking with sale invoices by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/204
|
||||
* fix(webapp): Table headers sticky for all reports. by @elforjani13 in https://github.com/bigcapitalhq/bigcapital/pull/240
|
||||
* chore(deps): bump word-wrap from 1.2.3 to 1.2.4 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/200
|
||||
* chore(deps): bump word-wrap from 1.2.3 to 1.2.4 in /packages/webapp by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/199
|
||||
* chore(deps): bump mongoose from 5.13.15 to 5.13.20 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/197
|
||||
|
||||
## [0.9.12] - 29-08-2023
|
||||
|
||||
* Refactor: split the services to multiple service classes. (by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/202)
|
||||
* Fix: create quick customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/206
|
||||
* Fix: typo in bill success message without bill number by @KalliopiPliogka in https://github.com/bigcapitalhq/bigcapital/pull/219
|
||||
* Fix: AP/AR aging summary issue by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/229
|
||||
* Fix: shouldn't write GL entries when save transaction as draft. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/221
|
||||
* Fix: Transaction type of credit note and vendor credit are not defined on account transactions by @abouolia in
|
||||
* Fix: date format of filtering transactions by date range by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/231
|
||||
* Fix: change the default from/date date value of reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/230
|
||||
* Fix: typos in words start with `A` letter by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/227
|
||||
* Fix: filter by customers, vendors and items in reports do not work by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/224
|
||||
https://github.com/bigcapitalhq/bigcapital/pull/225
|
||||
|
||||
## [0.9.11] - 23-07-2023
|
||||
|
||||
* added: Restart policy to docker compose files. by @suhaibaffan in https://github.com/bigcapitalhq/bigcapital/pull/198
|
||||
* fix: Expose and expand the rate limit to the env variables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/195
|
||||
|
||||
## [0.9.10] - 18-07-2023
|
||||
|
||||
* feat(e2e): E2E onboarding process by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/176
|
||||
* fix(webapp): Show loading message of cost computing job on financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/196
|
||||
* fix(webapp): Change the currency code of sales and purchases transactions with foreign contacts.
|
||||
|
||||
## [0.9.9] - 28-06-2023
|
||||
|
||||
* refactor: Customer and vendor select component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/171
|
||||
* chore: Move auto-increment components in separate files by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/170
|
||||
* fix: Style of quick item drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/173
|
||||
* fix: Should not show the form before loading account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/172
|
||||
* fix: Payment made form does not handle not unique number an e… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/177
|
||||
* fix: Internal note of invoice/bill payment does not saving by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/181
|
||||
* fix: Storing cash flow transaction description by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/180
|
||||
* fix: No currency in amount field on money in/out dialogs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/179
|
||||
* fix: No default branch for customer/vendor opening balance branch by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/182
|
||||
|
||||
## [0.9.8] - 19-06-2023
|
||||
|
||||
`bigcapitalhq/webapp`
|
||||
|
||||
* add: Inventory Adjustment option to the item drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/158
|
||||
* fix: use all drawers names from common enum object by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/157
|
||||
* fix: adjustment type options do not show up by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/159
|
||||
* fix: change the remove line text to be red to intent as a danger action by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/162
|
||||
* fix: rename sidebar localization keys names to be keyword path by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/161
|
||||
* fix: manual journal placeholder text by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/160
|
||||
* fix: warehouses select component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/168
|
||||
|
||||
`bigcapitalhq/server`
|
||||
|
||||
* fix: sending emails on reset password and registration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/167
|
||||
|
||||
## [0.9.7] - 14-06-2023
|
||||
|
||||
`@bigcapital/webapp`
|
||||
* fix: change the footer links of onboarding pages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/139
|
||||
|
||||
`@bigcapital/server`
|
||||
* fix: expense transaction journal entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/155
|
||||
|
||||
## [0.9.6] - 12-06-2023
|
||||
|
||||
`@bigcapital/webapp`
|
||||
|
||||
* fix: remove duplicated form submitting by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/138
|
||||
* feat: add monorepo version on the application sidebar by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/136
|
||||
|
||||
## [0.9.5] - 11-06-2023
|
||||
|
||||
`@bigcapital/server`
|
||||
|
||||
* fix: filter ledger entries that effect contact balance to AR/AP accounts only by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/132
|
||||
|
||||
`@bigcapital/webapp`
|
||||
|
||||
* fix: catch journal error when create a journal with accounts have different currency by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/135
|
||||
* fix: add duplicate icon to context menu of customers and vendors table by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/133
|
||||
* fix: customer/vendor opening balance with exchange rate by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/134
|
||||
|
||||
## [0.9.4] - 08-06-2023
|
||||
|
||||
`@bigcapital/monorepo`
|
||||
- fixed: docker-compose line-ending issue on Windows by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/130
|
||||
|
||||
`@bigcapital/server`
|
||||
- fixed: Disable Webpack minification for JS class name reading.
|
||||
|
||||
## [0.9.3] -04-06-2023
|
||||
|
||||
`@bigcapital/monorepo`
|
||||
* Added: Add env variable to customize the proxy public ports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/125
|
||||
* Added: Migrate the server database to MariaDB by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/128
|
||||
|
||||
## [0.9.2] - 31-05-2023
|
||||
|
||||
`@bigcapital/webapp`
|
||||
|
||||
- fixed: move `packaeg-lock.json` inside docker container.
|
||||
- fixed: remove Sentry from the web client.
|
||||
|
||||
## [0.9.1] - 28-05-2023
|
||||
|
||||
`@bigcapital/server`
|
||||
- fix: deleting ledger entries of manual journal.
|
||||
- fix: base currency should be enabled.
|
||||
- fix: delete invoice transaction issue.
|
||||
|
||||
`@bigcapital/webapp`
|
||||
- fix: general, accountant and items preferences.
|
||||
- fix: auto-increment sale invoices, estiamtes, credit notes, payments and manual journals.
|
||||
- refactor: the setup organization form to use binded Formik components.
|
||||
|
||||
## [0.9.0] - 06-05-2023
|
||||
|
||||
`@bigcapital/server`
|
||||
|
||||
- [Sign-up restrictions](https://docs.bigcapital.ly/docs/deployment/signup_restriction) for self-hosting instances to disable signup or control the allowed email addresses and domains that can sign-up.
|
||||
|
||||
## [0.8.3] - 06-04-2023
|
||||
|
||||
`@bigcaptial/monorepo`
|
||||
|
||||
141
CONTRIBUTING.md
Normal file
141
CONTRIBUTING.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
Thank you for considering contributing to our project! We appreciate your interest and welcome any contributions you may have.
|
||||
|
||||
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution.
|
||||
|
||||
## Sections
|
||||
|
||||
- [General Instructions](#general-instructions)
|
||||
- [Local Setup Prerequisites](#local-setup-prerequisites)
|
||||
- [Contribute to Backend](#contribute-to-backend)
|
||||
- [Contribute to Frontend](#contribute-to-frontend)
|
||||
- [Other Ways to Contribute](#other-ways-to-contribute)
|
||||
|
||||
## General Instructions
|
||||
|
||||
## For Pull Request(s)
|
||||
|
||||
Contributions via pull requests are much appreciated. Once the approach is agreed upon ✅, make your changes and open a Pull Request(s). Before sending us a pull request, please ensure that,
|
||||
|
||||
- Fork the repo on GitHub, clone it on your machine.
|
||||
- Create a branch with your changes.
|
||||
- You are working against the latest source on the `develop` branch.
|
||||
- Modify the source; please focus only on the specific change.
|
||||
- Ensure local tests pass.
|
||||
- Commit to your fork using clear commit messages.
|
||||
- Send us a pull request.
|
||||
- Pay attention to any automated CI failures reported in the pull request.
|
||||
- Stay involved in the conversation
|
||||
|
||||
⚠️ Please note: If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻
|
||||
|
||||
---
|
||||
|
||||
## Local Setup Prerequisites
|
||||
- The application currently supports **Node.js v18.x**.
|
||||
- `pnpm` packages manager, (from pnpm [guide](https://pnpm.io/installation) pick any installation method).
|
||||
|
||||
## Contribute to Backend
|
||||
|
||||
- Clone the `bigcapital` repository and `cd` into `bigcapital` directory.
|
||||
- Create `.env` file by copying `.env.example` file to `.env`. (The ``.env.example`` file has all the necessary values of variables to start development directly).
|
||||
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
- Install all npm dependencies of the monorepo, you don't have to change directory to the `backend` package. just hit the command on root directory and it will install dependencies of all packages.
|
||||
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- Run all required docker containers in the development, we already configured all containers under `docker-compose.yml`.
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Wait some seconds, and hit `docker-compose ps` and you should see the same result below.
|
||||
|
||||
```
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
d974edfab9df bigcapital-mysql "docker-entrypoint.s…" 7 seconds ago Up 1 second 0.0.0.0:3306->3306/tcp, 33060/tcp bigcapital-mysql-1
|
||||
cefa73fe2881 bigcapital-redis "docker-entrypoint.s…" 7 seconds ago Up 1 second 6379/tcp bigcapital-redis-1
|
||||
1ea059198cb4 bigcapital-mongo "docker-entrypoint.s…" 7 seconds ago Up 1 second 0.0.0.0:27017->27017/tcp bigcapital-mongo-1
|
||||
```
|
||||
|
||||
- There're some CLI commands we should run before running the server like databaase migration, so we need to build the `server` app first.
|
||||
|
||||
```
|
||||
pnpm run build:server
|
||||
```
|
||||
|
||||
- Run the database migration for system database.
|
||||
|
||||
```
|
||||
node packages/server/build/commands.js system:migrate:latest
|
||||
```
|
||||
|
||||
And you should get something like that.
|
||||
|
||||
```
|
||||
Batch 1 run: 6 migrations
|
||||
```
|
||||
|
||||
- Next, start the webapp application.
|
||||
|
||||
```
|
||||
pnpm run dev:server
|
||||
```
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
----
|
||||
|
||||
## Contribute to Frontend
|
||||
|
||||
- Clone the `bigcapital` repository and cd into `bigcapital` directory.
|
||||
|
||||
```
|
||||
git clone https://github.com/bigcapital/bigcapital.git && cd bigcaptial
|
||||
```
|
||||
|
||||
- Install all npm dependencies of the monorepo, you don't have to change directory to the `frontend` package. just hit that command and will install all packages across all application.
|
||||
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- Next, start the webapp application.
|
||||
|
||||
```
|
||||
pnpm run dev:webapp
|
||||
```
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
---
|
||||
|
||||
## Code Review
|
||||
|
||||
We welcome constructive criticism and feedback on code submitted by contributors. All feedback should be constructive and respectful, and should focus on the code rather than the contributor. Code review may include suggestions for improvement or changes to the code.
|
||||
|
||||
---
|
||||
|
||||
## Other Ways to Contribute
|
||||
|
||||
There are many other ways to get involved with the community and to participate in this project:
|
||||
|
||||
- Use the product, submitting GitHub issues when a problem is found.
|
||||
- Help code review pull requests and participate in issue threads.
|
||||
- Submit a new feature request as an issue.
|
||||
- Help answer questions on forums such as Bigcapital Community Discord Channel.
|
||||
- Tell others about the project on Twitter, your blog, etc.
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
Again, Feel free to ping us on [`#contributing`](https://discord.com/invite/c8nPBJafeb) on our Discord community if you need any help on this :)
|
||||
|
||||
Thank You!
|
||||
114
README.md
114
README.md
@@ -1,12 +1,34 @@
|
||||
<p align="center">
|
||||
<p align="center">
|
||||
<a href="https://bigcapital.ly" target="_blank">
|
||||
<a href="https://bigcapital.app" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/bigcapital.svg" alt="Bigcapital" width="280" height="75">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
Simple, smart online accounting software for small and medium businesses.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/bigcapitalhq/bigcapital/commits/develop">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/bigcapitalhq/bigcapital/develop" />
|
||||
</a>
|
||||
<a href="https://discord.com/invite/c8nPBJafeb">
|
||||
<img src="https://img.shields.io/discord/1066514716752625725?label=Discord" alt="" />
|
||||
</a>
|
||||
<a href="https://github.com/bigcapitalhq/bigcapital/graphs/contributors">
|
||||
<img src="https://img.shields.io/github/contributors/bigcapitalhq/bigcapital" alt="" />
|
||||
</a>
|
||||
<a href="https://github.com/bigcapitalhq/bigcapital/blob/develop/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/bigcapitalhq/bigcapital" alt="" />
|
||||
</a>
|
||||
<a href="https://twitter.com/bigcapitalhq">
|
||||
<img src="https://img.shields.io/twitter/follow/bigcapitalhq?style=social" alt="twitter" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://my.bigcapital.app">Bigcapital Cloud</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
# What's Bigcapital?
|
||||
@@ -19,13 +41,97 @@ Bigcapital is a smart and open-source accounting and inventory software, Bigcapi
|
||||
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/screenshot-3.png" width="270">
|
||||
</p>
|
||||
|
||||
# Getting Started
|
||||
|
||||
We've got serveral options on dev and prod depending on your need to get started quickly with Bigcapital.
|
||||
|
||||
## Self-hosted
|
||||
|
||||
Bigcapital is available open-source under AGPL license. You can host it on your own servers using Docker.
|
||||
|
||||
### Docker
|
||||
|
||||
To get started with self-hosted with Docker and Docker Compose, take a look at the [Docker guide](https://docs.bigcapital.app/deployment/docker).
|
||||
|
||||
## Development
|
||||
|
||||
### Local Setup
|
||||
|
||||
To get started locally, we have a [guide to help you](https://github.com/bigcapitalhq/bigcapital/blob/develop/CONTRIBUTING.md).
|
||||
|
||||
### Gitpod
|
||||
|
||||
- Click the Gitpod button below to open this project in development mode.
|
||||
- This will open and configure the workspace in your browser with all the necessary dependencies.
|
||||
|
||||
[](https://gitpod.io/new/#https://github.com/bigcapitalhq/bigcapital)
|
||||
|
||||
## Headless Accounting
|
||||
|
||||
You can integrate Bigcapital API with your system to organize your transactions in double-entry system to get the best financial reports.
|
||||
|
||||
[](https://www.postman.com/bigcapital/workspace/bigcapital-api)
|
||||
|
||||
# Resources
|
||||
|
||||
- [Documentation](https://docs.bigcapital.ly/) - Learn how to use.
|
||||
- [Documentation](https://docs.bigcapital.app/) - Learn how to use.
|
||||
- [Contribution](https://github.com/bigcapitalhq/bigcapital/blob/develop/CONTRIBUTING.md) - Welcome to any contributions.
|
||||
- [Discord](https://discord.com/invite/c8nPBJafeb) - Ask for help.
|
||||
- [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs.
|
||||
- [Source Code](https://github.com/bigcapitalhq/bigcapital) - Github repo.
|
||||
|
||||
# Changlog
|
||||
# Changelog
|
||||
|
||||
Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently.
|
||||
|
||||
# Contact us
|
||||
|
||||
Meet our sales team for any commercial inquiries.
|
||||
|
||||
<a target="_blank" href="https://cal.com/ahmed-bouhuolia-ekk3ph/30min"><img src="https://cal.com/book-with-cal-dark.svg" alt="Book us with Cal.com"></a>
|
||||
|
||||
# Recognition
|
||||
|
||||
<a href="https://news.ycombinator.com/item?id=36118990">
|
||||
<img
|
||||
style="width: 250px; height: 54px;" width="250" height="54"
|
||||
alt="Featured on Hacker News"
|
||||
src="https://hackernews-badge.vercel.app/api?id=36118990"
|
||||
/>
|
||||
</a>
|
||||
|
||||
# Contributors
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abouolia"><img src="https://avatars.githubusercontent.com/u/2197422?v=4?s=100" width="100px;" alt="Ahmed Bouhuolia"/><br /><sub><b>Ahmed Bouhuolia</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=abouolia" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://ameir.net"><img src="https://avatars.githubusercontent.com/u/374330?v=4?s=100" width="100px;" alt="Ameir Abdeldayem"/><br /><sub><b>Ameir Abdeldayem</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aameir" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/elforjani13"><img src="https://avatars.githubusercontent.com/u/39470382?v=4?s=100" width="100px;" alt="ElforJani13"/><br /><sub><b>ElforJani13</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=elforjani13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://scheibling.se"><img src="https://avatars.githubusercontent.com/u/24367830?v=4?s=100" width="100px;" alt="Lars Scheibling"/><br /><sub><b>Lars Scheibling</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Ascheibling" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suhaibaffan"><img src="https://avatars.githubusercontent.com/u/18115937?v=4?s=100" width="100px;" alt="Suhaib Affan"/><br /><sub><b>Suhaib Affan</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=suhaibaffan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://me.kochie.io"><img src="https://avatars.githubusercontent.com/u/10809884?v=4?s=100" width="100px;" alt="Robert Koch"/><br /><sub><b>Robert Koch</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=kochie" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://cschuijt.nl"><img src="https://avatars.githubusercontent.com/u/5460015?v=4?s=100" width="100px;" alt="Casper Schuijt"/><br /><sub><b>Casper Schuijt</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Acschuijt" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ANasouf"><img src="https://avatars.githubusercontent.com/u/19536487?v=4?s=100" width="100px;" alt="ANasouf"/><br /><sub><b>ANasouf</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=ANasouf" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://ragnarlaud.dev"><img src="https://avatars.githubusercontent.com/u/3042904?v=4?s=100" width="100px;" alt="Ragnar Laud"/><br /><sub><b>Ragnar Laud</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Axprnio" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asenawritescode"><img src="https://avatars.githubusercontent.com/u/67445192?v=4?s=100" width="100px;" alt="Asena"/><br /><sub><b>Asena</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aasenawritescode" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://snyder.tech"><img src="https://avatars.githubusercontent.com/u/707567?v=4?s=100" width="100px;" alt="Ben Snyder"/><br /><sub><b>Ben Snyder</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=benpsnyder" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://vederis.id"><img src="https://avatars.githubusercontent.com/u/13505006?v=4?s=100" width="100px;" alt="Vederis Leunardus"/><br /><sub><b>Vederis Leunardus</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=cloudsbird" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.pivoten.com"><img src="https://avatars.githubusercontent.com/u/104120598?v=4?s=100" width="100px;" alt="Chris Cantrell"/><br /><sub><b>Chris Cantrell</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Accantrell72" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
@@ -3,32 +3,33 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
nginx:
|
||||
container_name: bigcapital-nginx-gateway
|
||||
build:
|
||||
context: ./docker/nginx
|
||||
args:
|
||||
- SERVER_PROXY_PORT=3000
|
||||
- WEB_SSL=false
|
||||
- SELF_SIGNED=false
|
||||
volumes:
|
||||
- ./data/logs/nginx/:/var/log/nginx
|
||||
- ./docker/certbot/certs/:/var/certs
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
tty: true
|
||||
proxy:
|
||||
image: envoyproxy/envoy:v1.30-latest
|
||||
depends_on:
|
||||
- server
|
||||
- webapp
|
||||
ports:
|
||||
- '${PUBLIC_PROXY_PORT:-80}:80'
|
||||
- '${PUBLIC_PROXY_SSL_PORT:-443}:443'
|
||||
tty: true
|
||||
volumes:
|
||||
- ./docker/envoy/envoy.yaml:/etc/envoy/envoy.yaml
|
||||
restart: on-failure
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
webapp:
|
||||
webapp:
|
||||
container_name: bigcapital-webapp
|
||||
image: ghcr.io/bigcapitalhq/webapp:latest
|
||||
image: bigcapitalhq/webapp:latest
|
||||
restart: on-failure
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
server:
|
||||
container_name: bigcapital-server
|
||||
image: ghcr.io/bigcapitalhq/server:latest
|
||||
image: bigcapitalhq/server:latest
|
||||
expose:
|
||||
- '3000'
|
||||
links:
|
||||
- mysql
|
||||
- mongo
|
||||
@@ -37,6 +38,9 @@ services:
|
||||
- mysql
|
||||
- mongo
|
||||
- redis
|
||||
restart: on-failure
|
||||
networks:
|
||||
- bigcapital_network
|
||||
environment:
|
||||
# Mail
|
||||
- MAIL_HOST=${MAIL_HOST}
|
||||
@@ -62,7 +66,7 @@ services:
|
||||
# Authentication
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
|
||||
# MongoDB
|
||||
# MongoDB
|
||||
- MONGODB_DATABASE_URL=mongodb://mongo/bigcapital
|
||||
|
||||
# Application
|
||||
@@ -72,50 +76,118 @@ services:
|
||||
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
|
||||
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
|
||||
|
||||
# Sign-up restrictions
|
||||
- SIGNUP_DISABLED=${SIGNUP_DISABLED}
|
||||
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
|
||||
- SIGNUP_ALLOWED_EMAILS=${SIGNUP_ALLOWED_EMAILS}
|
||||
|
||||
# Sign-up email confirmation
|
||||
- SIGNUP_EMAIL_CONFIRMATION=${SIGNUP_EMAIL_CONFIRMATION}
|
||||
|
||||
# Gotenberg (Pdf generator)
|
||||
- GOTENBERG_URL=${GOTENBERG_URL}
|
||||
- GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL}
|
||||
|
||||
# Exchange Rate
|
||||
- EXCHANGE_RATE_SERVICE=${EXCHANGE_RATE_SERVICE}
|
||||
- OPEN_EXCHANGE_RATE_APP_ID-${OPEN_EXCHANGE_RATE_APP_ID}
|
||||
|
||||
# Bank Sync
|
||||
- BANKING_CONNECT=${BANKING_CONNECT}
|
||||
|
||||
# Plaid
|
||||
- PLAID_ENV=${PLAID_ENV}
|
||||
- PLAID_CLIENT_ID=${PLAID_CLIENT_ID}
|
||||
- PLAID_SECRET=${PLAID_SECRET}
|
||||
- PLAID_LINK_WEBHOOK=${PLAID_LINK_WEBHOOK}
|
||||
|
||||
# Lemon Squeez
|
||||
- LEMONSQUEEZY_API_KEY=${LEMONSQUEEZY_API_KEY}
|
||||
- LEMONSQUEEZY_STORE_ID=${LEMONSQUEEZY_STORE_ID}
|
||||
- LEMONSQUEEZY_WEBHOOK_SECRET=${LEMONSQUEEZY_WEBHOOK_SECRET}
|
||||
- HOSTED_ON_BIGCAPITAL_CLOUD=${HOSTED_ON_BIGCAPITAL_CLOUD}
|
||||
|
||||
# New Relic matrics tracking.
|
||||
- NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=${NEW_RELIC_DISTRIBUTED_TRACING_ENABLED}
|
||||
- NEW_RELIC_LOG=${NEW_RELIC_LOG}
|
||||
- NEW_RELIC_AI_MONITORING_ENABLED=${NEW_RELIC_AI_MONITORING_ENABLED}
|
||||
- NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED}
|
||||
- NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED}
|
||||
- NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY}
|
||||
- NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME}
|
||||
|
||||
# S3
|
||||
- S3_REGION=${S3_REGION}
|
||||
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
|
||||
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
|
||||
- S3_ENDPOINT=${S3_ENDPOINT}
|
||||
- S3_BUCKET=${S3_BUCKET}
|
||||
|
||||
database_migration:
|
||||
container_name: bigcapital-database-migration
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: docker/migration/Dockerfile
|
||||
environment:
|
||||
# Database
|
||||
- DB_HOST=mysql
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_CHARSET=${DB_CHARSET}
|
||||
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
|
||||
# Tenants databases
|
||||
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}
|
||||
depends_on:
|
||||
- mysql
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
mysql:
|
||||
container_name: bigcapital-mysql
|
||||
restart: on-failure
|
||||
build:
|
||||
context: ./docker/mysql
|
||||
context: ./docker/mariadb
|
||||
environment:
|
||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- mysql:/var/lib/mysql
|
||||
expose:
|
||||
- '3306'
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
mongo:
|
||||
container_name: bigcapital-mongo
|
||||
container_name: bigcapital-mongo
|
||||
restart: on-failure
|
||||
build: ./docker/mongo
|
||||
expose:
|
||||
- '27017'
|
||||
volumes:
|
||||
- mongo:/var/lib/mongodb
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
redis:
|
||||
container_name: bigcapital-redis
|
||||
restart: on-failure
|
||||
build:
|
||||
context: ./docker/redis
|
||||
expose:
|
||||
- "6379"
|
||||
- '6379'
|
||||
volumes:
|
||||
- redis:/data
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7
|
||||
expose:
|
||||
- '9000'
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
# Volumes
|
||||
volumes:
|
||||
@@ -130,3 +202,8 @@ volumes:
|
||||
redis:
|
||||
name: bigcapital_prod_redis
|
||||
driver: local
|
||||
|
||||
# Networks
|
||||
networks:
|
||||
bigcapital_network:
|
||||
driver: bridge
|
||||
|
||||
@@ -6,20 +6,23 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
mariadb:
|
||||
build:
|
||||
context: ./docker/mysql
|
||||
context: ./docker/mariadb
|
||||
environment:
|
||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- mysql:/var/lib/mysql
|
||||
expose:
|
||||
- '3306'
|
||||
ports:
|
||||
- '3306:3306'
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: unless-stopped
|
||||
|
||||
mongo:
|
||||
build: ./docker/mongo
|
||||
@@ -29,6 +32,9 @@ services:
|
||||
- mongo:/var/lib/mongodb
|
||||
ports:
|
||||
- '27017:27017'
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: unless-stopped
|
||||
|
||||
redis:
|
||||
build:
|
||||
@@ -37,6 +43,14 @@ services:
|
||||
- "6379"
|
||||
volumes:
|
||||
- redis:/data
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: unless-stopped
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7
|
||||
ports:
|
||||
- "9000:3000"
|
||||
|
||||
# Volumes
|
||||
volumes:
|
||||
|
||||
62
docker/envoy/envoy.yaml
Normal file
62
docker/envoy/envoy.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 80
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
stat_prefix: ingress_http
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: backend
|
||||
domains: ['*']
|
||||
routes:
|
||||
- match:
|
||||
prefix: '/api'
|
||||
route:
|
||||
cluster: dynamic_server
|
||||
- match:
|
||||
prefix: '/'
|
||||
route:
|
||||
cluster: webapp
|
||||
http_filters:
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
|
||||
clusters:
|
||||
- name: dynamic_server
|
||||
connect_timeout: 0.25s
|
||||
type: STRICT_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: dynamic_server
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: server
|
||||
port_value: 3000
|
||||
|
||||
- name: webapp
|
||||
connect_timeout: 0.25s
|
||||
type: STRICT_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: webapp
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: webapp
|
||||
port_value: 80
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mysql:5.7
|
||||
FROM mariadb:10.2
|
||||
|
||||
USER root
|
||||
ADD my.cnf /etc/mysql/conf.d/my.cnf
|
||||
@@ -17,7 +17,7 @@ ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
|
||||
COPY ./init.sql /scripts/init.template.sql
|
||||
COPY ./docker-entrypoint.sh /docker-entrypoint-initdb.d/docker-initialize.sh
|
||||
|
||||
# The scripts in the docker-entrypoint-initdb.d/ directory are executed as
|
||||
# The scripts in the `docker-entrypoint-initdb.d/` directory are executed as
|
||||
# the mysql user inside the MySQL Docker container.
|
||||
RUN chown -R mysql:root /docker-entrypoint-initdb.d
|
||||
RUN chown -R mysql:root /scripts
|
||||
@@ -1,2 +1,3 @@
|
||||
GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION;
|
||||
|
||||
FLUSH PRIVILEGES;
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/bigcapitalhq/server:latest as build
|
||||
FROM bigcapitalhq/server:latest as build
|
||||
|
||||
ARG DB_HOST= \
|
||||
DB_USER= \
|
||||
@@ -35,4 +35,4 @@ WORKDIR /app/packages/server
|
||||
RUN git clone https://github.com/vishnubob/wait-for-it.git
|
||||
|
||||
# Once we listen the mysql port run the migration task.
|
||||
CMD ["./wait-for-it/wait-for-it.sh", "mysql:3306", "--", "node", "./build/commands.js", "system:migrate:latest"]
|
||||
CMD ./wait-for-it/wait-for-it.sh mysql:3306 -- sh -c "node ./build/commands.js system:migrate:latest && node ./build/commands.js tenants:migrate:latest"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
FROM nginx:1.11
|
||||
|
||||
RUN mkdir /etc/nginx/sites-available && rm /etc/nginx/conf.d/default.conf
|
||||
ADD nginx.conf /etc/nginx/
|
||||
|
||||
COPY scripts /root/scripts/
|
||||
COPY certs /etc/ssl/
|
||||
|
||||
COPY sites /etc/nginx/templates
|
||||
|
||||
ARG SERVER_PROXY_PORT=3000
|
||||
ARG WEB_SSL=false
|
||||
ARG SELF_SIGNED=false
|
||||
|
||||
ENV SERVER_PROXY_PORT=$SERVER_PROXY_PORT
|
||||
ENV WEB_SSL=$WEB_SSL
|
||||
ENV SELF_SIGNED=$SELF_SIGNED
|
||||
|
||||
RUN /bin/bash /root/scripts/build-nginx.sh
|
||||
|
||||
CMD nginx
|
||||
@@ -1,33 +0,0 @@
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
daemon off;
|
||||
|
||||
events {
|
||||
worker_connections 2048;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
server_tokens off;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 15;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 20M;
|
||||
open_file_cache max=100;
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-available/*;
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
for conf in /etc/nginx/templates/*.conf; do
|
||||
mv $conf "/etc/nginx/sites-available/"$(basename $conf) > /dev/null
|
||||
done
|
||||
|
||||
for template in /etc/nginx/templates/*.template; do
|
||||
envsubst < $template > "/etc/nginx/sites-available/"$(basename $template)".conf"
|
||||
done
|
||||
@@ -1,16 +0,0 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://server:${SERVER_PROXY_PORT};
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://webapp;
|
||||
}
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/letsencrypt/;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
13
e2e/_utils.ts
Normal file
13
e2e/_utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
export const clearLocalStorage = (page: Page) => {
|
||||
return page.evaluate(`window.localStorage.clear()`);
|
||||
};
|
||||
|
||||
export const defaultPageConfig = () => {
|
||||
return {
|
||||
extraHTTPHeaders: {
|
||||
'ngrok-skip-browser-warning': 'any-value',
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,14 +1,23 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { clearLocalStorage, defaultPageConfig } from './_utils';
|
||||
|
||||
let authPage: Page;
|
||||
|
||||
test.describe('authentication', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
authPage = await browser.newPage();
|
||||
authPage = await browser.newPage({ ...defaultPageConfig() });
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await authPage.close();
|
||||
});
|
||||
test.afterEach(async ({ context }) => {
|
||||
context.clearCookies();
|
||||
await clearLocalStorage(authPage);
|
||||
});
|
||||
|
||||
test.describe('login', () => {
|
||||
test.beforeAll(async () => {
|
||||
test.beforeEach(async () => {
|
||||
await authPage.goto('/auth/login');
|
||||
});
|
||||
test('should show the login page.', async () => {
|
||||
@@ -30,10 +39,23 @@ test.describe('authentication', () => {
|
||||
await authPage.getByRole('link', { name: 'Sign up' }).click();
|
||||
await expect(authPage.url()).toContain('/auth/register');
|
||||
});
|
||||
test('should the email or password is not correct.', async () => {
|
||||
await authPage.getByLabel('Email Address').click();
|
||||
await authPage.getByLabel('Email Address').fill(faker.internet.email());
|
||||
|
||||
await authPage.getByLabel('Password').click();
|
||||
await authPage.getByLabel('Password').fill(faker.internet.password());
|
||||
|
||||
await authPage.getByRole('button', { name: 'Log in' }).click();
|
||||
|
||||
await expect(authPage.locator('body')).toContainText(
|
||||
'The email and password you entered did not match our records.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('register', () => {
|
||||
test.beforeAll(async () => {
|
||||
test.beforeEach(async () => {
|
||||
await authPage.goto('/auth/register');
|
||||
});
|
||||
test('should first name, last name, email and password be required.', async () => {
|
||||
@@ -52,10 +74,36 @@ test.describe('authentication', () => {
|
||||
'Password is a required field'
|
||||
);
|
||||
});
|
||||
test('should signup successfully.', async () => {
|
||||
const form = authPage.locator('form');
|
||||
await form.getByLabel('First Name').click();
|
||||
await form.getByLabel('First Name').fill(faker.person.firstName());
|
||||
|
||||
await form.getByLabel('Email').click();
|
||||
await form.getByLabel('Email').fill(faker.internet.email());
|
||||
|
||||
await form.getByLabel('Last Name').click();
|
||||
await form.getByLabel('Last Name').fill(faker.person.lastName());
|
||||
|
||||
await form.getByLabel('Password').click();
|
||||
await form.getByLabel('Password').fill(faker.internet.password());
|
||||
|
||||
await authPage.getByRole('button', { name: 'Register' }).click();
|
||||
|
||||
await expect(authPage.locator('h1')).toContainText(
|
||||
'Register a New Organization now!'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('reset password', () => {
|
||||
test.beforeAll(async () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
authPage = await browser.newPage({ ...defaultPageConfig() });
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await authPage.close();
|
||||
});
|
||||
test.beforeEach(async () => {
|
||||
await authPage.goto('/auth/send_reset_password');
|
||||
});
|
||||
test('should email be required.', async () => {
|
||||
|
||||
7
e2e/items.spec.ts
Normal file
7
e2e/items.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
test.describe('item', () => {
|
||||
test('should validate all required fields.', () => {});
|
||||
test('should save the item successfully.', () => {});
|
||||
test('should item code be unqiue.', () => {});
|
||||
});
|
||||
86
e2e/onboarding.spec.ts
Normal file
86
e2e/onboarding.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { defaultPageConfig } from './_utils';
|
||||
|
||||
let authPage: Page;
|
||||
let businessLegalName: string = faker.company.name();
|
||||
|
||||
test.describe('onboarding', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
authPage = await browser.newPage({ ...defaultPageConfig() });
|
||||
await authPage.goto('/auth/register');
|
||||
|
||||
const form = authPage.locator('form');
|
||||
|
||||
await form.getByLabel('First Name').fill(faker.person.firstName());
|
||||
await form.getByLabel('Email').fill(faker.internet.email());
|
||||
await form.getByLabel('Last Name').fill(faker.person.lastName());
|
||||
await form.getByLabel('Password').fill(faker.internet.password());
|
||||
|
||||
await authPage.getByRole('button', { name: 'Register' }).click();
|
||||
});
|
||||
test('should validation catch all required fields', async () => {
|
||||
const form = authPage.locator('form');
|
||||
|
||||
await authPage.getByRole('button', { name: 'Save & Continue' }).click();
|
||||
|
||||
await expect(form).toContainText('Organization name is a required field');
|
||||
await expect(form).toContainText('Location is a required field');
|
||||
await expect(form).toContainText('Base currency is a required field');
|
||||
await expect(form).toContainText('Fiscal year is a required field');
|
||||
await expect(form).toContainText('Time zone is a required field');
|
||||
});
|
||||
test.describe('after onboarding', () => {
|
||||
test.beforeAll(async () => {
|
||||
await authPage.getByLabel('Legal Organization Name').click();
|
||||
await authPage
|
||||
.getByLabel('Legal Organization Name')
|
||||
.fill(businessLegalName);
|
||||
|
||||
// Fill Business Location.
|
||||
await authPage
|
||||
.getByRole('button', { name: 'Select Business Location...' })
|
||||
.click();
|
||||
await authPage.locator('a').filter({ hasText: 'Albania' }).click();
|
||||
|
||||
// Fill Base Currency.
|
||||
await authPage
|
||||
.getByRole('button', { name: 'Select Base Currency...' })
|
||||
.click();
|
||||
await authPage
|
||||
.locator('a')
|
||||
.filter({ hasText: 'AED - United Arab Emirates Dirham' })
|
||||
.click();
|
||||
|
||||
// Fill Fasical Year.
|
||||
await authPage
|
||||
.getByRole('button', { name: 'Select Fiscal Year...' })
|
||||
.click();
|
||||
await authPage.locator('a').filter({ hasText: 'June - May' }).click();
|
||||
|
||||
// Fill Timezone.
|
||||
await authPage
|
||||
.getByRole('button', { name: 'Select Time Zone...' })
|
||||
.click();
|
||||
await authPage.getByText('Pacific/Marquesas-09:30').click();
|
||||
|
||||
// Click on Submit button
|
||||
await authPage.getByRole('button', { name: 'Save & Continue' }).click();
|
||||
});
|
||||
test('should onboarding process success', async () => {
|
||||
await expect(authPage.locator('body')).toContainText(
|
||||
'Congrats! You are ready to go',
|
||||
{
|
||||
timeout: 30000,
|
||||
}
|
||||
);
|
||||
});
|
||||
test('should go to the dashboard after clicking on "Go to dashboard" button.', async () => {
|
||||
await authPage.getByRole('button', { name: 'Go to dashboard' }).click();
|
||||
|
||||
await expect(
|
||||
authPage.locator('[data-testId="dashboard-topbar"] h1')
|
||||
).toContainText(businessLegalName);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.0.0",
|
||||
"npmClient": "npm"
|
||||
"version": "independent",
|
||||
"npmClient": "pnpm",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
|
||||
5720
package-lock.json
generated
5720
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -2,7 +2,6 @@
|
||||
"name": "bigcapital-monorepo",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"bootstrap": "lerna exec npm install",
|
||||
"dev": "lerna run dev",
|
||||
"build": "lerna run build",
|
||||
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
|
||||
@@ -13,20 +12,18 @@
|
||||
"test:e2e": "playwright test",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"shared/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.4.2",
|
||||
"@commitlint/config-conventional": "^17.4.2",
|
||||
"@commitlint/config-lerna-scopes": "^17.4.2",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@playwright/test": "^1.32.3",
|
||||
"husky": "^8.0.3",
|
||||
"lerna": "^6.4.1",
|
||||
"@commitlint/cli": "^17.4.2",
|
||||
"@playwright/test": "^1.32.3"
|
||||
"lerna": "^8.1.2",
|
||||
"pnpm": "^9.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.x"
|
||||
"node": "16.x || 17.x || 18.x"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
7
packages/server/.gitignore
vendored
7
packages/server/.gitignore
vendored
@@ -1,7 +1,8 @@
|
||||
/node_modules/
|
||||
/.env
|
||||
/storage
|
||||
package-lock.json
|
||||
stdout.log
|
||||
/dist
|
||||
/build
|
||||
/build
|
||||
/public/imports
|
||||
|
||||
dist
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:14.20-alpine as build
|
||||
FROM node:18.16.0-alpine as build
|
||||
|
||||
USER root
|
||||
|
||||
@@ -34,7 +34,11 @@ ARG MAIL_HOST= \
|
||||
BASE_URL= \
|
||||
# Agendash
|
||||
AGENDASH_AUTH_USER=agendash \
|
||||
AGENDASH_AUTH_PASSWORD=123123
|
||||
AGENDASH_AUTH_PASSWORD=123123 \
|
||||
# Sign-up restriction
|
||||
SIGNUP_DISABLED= \
|
||||
SIGNUP_ALLOWED_DOMAINS= \
|
||||
SIGNUP_ALLOWED_EMAILS=
|
||||
|
||||
ENV MAIL_HOST=$MAIL_HOST \
|
||||
MAIL_USERNAME=$MAIL_USERNAME \
|
||||
@@ -68,22 +72,39 @@ ENV MAIL_HOST=$MAIL_HOST \
|
||||
# MongoDB
|
||||
MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \
|
||||
# Application
|
||||
BASE_URL=$BASE_URL
|
||||
BASE_URL=$BASE_URL \
|
||||
# Sign-up restriction
|
||||
SIGNUP_DISABLED=$SIGNUP_DISABLED \
|
||||
SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \
|
||||
SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS
|
||||
|
||||
# New Relic config file.
|
||||
ENV NEW_RELIC_NO_CONFIG_FILE=true
|
||||
|
||||
# Create app directory.
|
||||
WORKDIR /app
|
||||
|
||||
RUN chown node:node /
|
||||
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy application dependency manifests to the container image.
|
||||
COPY ./package*.json ./
|
||||
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
COPY ./lerna.json ./lerna.json
|
||||
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
|
||||
COPY ./packages/server/package*.json ./packages/server/
|
||||
|
||||
COPY ./lerna.json ./lerna.json
|
||||
# Install application dependencies
|
||||
RUN apk update
|
||||
RUN apk add python3 build-base chromium
|
||||
|
||||
# Install app dependencies for production.
|
||||
RUN npm install
|
||||
RUN npm run bootstrap
|
||||
# Set PYHTON env
|
||||
ENV PYTHON=/usr/bin/python3
|
||||
|
||||
# Install packages dependencies for production.
|
||||
RUN pnpm install
|
||||
|
||||
COPY --chown=node:node ./packages/server ./packages/server
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bigcapital/server",
|
||||
"version": "1.7.1",
|
||||
"version": "0.10.2",
|
||||
"description": "",
|
||||
"main": "src/server.ts",
|
||||
"scripts": {
|
||||
@@ -20,21 +20,28 @@
|
||||
"bigcapital": "./bin/bigcapital.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.576.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.583.0",
|
||||
"@casl/ability": "^5.4.3",
|
||||
"@hapi/boom": "^7.4.3",
|
||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||
"@supercharge/promise-pool": "^3.2.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/i18n": "^0.8.7",
|
||||
"@types/knex": "^0.16.1",
|
||||
"@types/mathjs": "^6.0.12",
|
||||
"@types/yup": "^0.29.13",
|
||||
"accepts": "^1.3.7",
|
||||
"accounting": "^0.4.1",
|
||||
"agenda": "^4.2.1",
|
||||
"agendash": "^3.1.0",
|
||||
"app-root-path": "^3.0.0",
|
||||
"async": "^3.2.0",
|
||||
"axios": "^0.20.0",
|
||||
"axios": "^1.6.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.20.2",
|
||||
"compression": "^1.7.4",
|
||||
"country-codes-list": "^1.6.8",
|
||||
"cpy": "^8.1.2",
|
||||
@@ -42,7 +49,7 @@
|
||||
"crypto-random-string": "^3.2.0",
|
||||
"csurf": "^1.10.0",
|
||||
"deep-map": "^2.0.0",
|
||||
"deepdash": "^5.3.7",
|
||||
"deepdash": "^5.3.9",
|
||||
"dotenv": "^8.1.0",
|
||||
"errorhandler": "^1.5.1",
|
||||
"es6-weak-map": "^2.0.3",
|
||||
@@ -52,9 +59,9 @@
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"express-boom": "^3.0.0",
|
||||
"express-fileupload": "^1.1.7-alpha.3",
|
||||
"express-oauth-server": "^2.0.0",
|
||||
"express-validator": "^6.12.2",
|
||||
"form-data": "^4.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-sass": "^5.0.0",
|
||||
"helmet": "^3.21.0",
|
||||
@@ -70,12 +77,18 @@
|
||||
"lru-cache": "^6.0.0",
|
||||
"mathjs": "^9.4.0",
|
||||
"memory-cache": "^0.2.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"moment": "^2.24.0",
|
||||
"moment-range": "^4.0.2",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mongodb": "^6.1.0",
|
||||
"mongoose": "^5.10.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"multer-s3": "^3.0.1",
|
||||
"mustache": "^3.0.3",
|
||||
"mysql": "^2.17.1",
|
||||
"mysql2": "^1.6.5",
|
||||
"newrelic": "^11.15.0",
|
||||
"node-cache": "^4.2.1",
|
||||
"nodemailer": "^6.3.0",
|
||||
"nodemon": "^1.19.1",
|
||||
@@ -84,6 +97,7 @@
|
||||
"objection-filter": "^4.0.1",
|
||||
"objection-soft-delete": "^1.0.7",
|
||||
"objection-unique": "^1.2.2",
|
||||
"plaid": "^10.3.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"pug": "^3.0.2",
|
||||
"puppeteer": "^10.2.0",
|
||||
@@ -92,14 +106,20 @@
|
||||
"rate-limiter-flexible": "^2.1.14",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rtl-detect": "^1.0.4",
|
||||
"socket.io": "^4.7.4",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"tmp-promise": "^3.0.3",
|
||||
"ts-transformer-keys": "^0.4.2",
|
||||
"tsyringe": "^4.3.0",
|
||||
"typedi": "^0.8.0",
|
||||
"uniqid": "^5.2.0",
|
||||
"winston": "^3.2.1"
|
||||
"winston": "^3.2.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"yup": "^0.28.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.158",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/ramda": "^0.27.64",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
@@ -131,7 +151,7 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"rtlcss": "^3.3.0",
|
||||
"run-script-webpack-plugin": "^0.1.1",
|
||||
"sass": "^1.37.5",
|
||||
"sass": "^1.58.0",
|
||||
"sinon": "^7.4.2",
|
||||
"start-server-webpack-plugin": "^2.2.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
|
||||
BIN
packages/server/public/.DS_Store
vendored
Normal file
BIN
packages/server/public/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
packages/server/public/imports/.DS_Store
vendored
Normal file
BIN
packages/server/public/imports/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -152,7 +152,7 @@
|
||||
"Opening Balance Liabilities": "رصيد الالتزامات الافتتاحي",
|
||||
"Loan": "اقراض",
|
||||
"Owner A Drawings": "مسحوبات المالك",
|
||||
"An account that holds valuation of products or goods that availiable for sale.": "حساب يحمل قيم مخزون البضاعة أو السلع المتاحة للبيع.",
|
||||
"An account that holds valuation of products or goods that available for sale.": "حساب يحمل قيم مخزون البضاعة أو السلع المتاحة للبيع.",
|
||||
"Tracks the gain and losses of the exchange differences.": "يسجل مكاسب وخسائر فروق الصرف.",
|
||||
"Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "يتم تسجيل أي رسوم مصرفية يتم فرضها في حساب الرسوم والمصروفات البنكية. ومن الأمثلة على ذلك رسوم صيانة الحساب المصرفي ورسوم المعاملات ورسوم الدفع المتأخر.",
|
||||
"The income activities are not associated to the core business.": "لا ترتبط انشطة الدخل إلى الأعمال الأساسية.",
|
||||
@@ -242,7 +242,7 @@
|
||||
"account.field.normal.credit": "دائن",
|
||||
"account.field.normal.debit": "مدين",
|
||||
"account.field.type": "نوع الحساب",
|
||||
"account.field.active": "Activity",
|
||||
"account.field.active": "Active",
|
||||
"account.field.balance": "الرصيد",
|
||||
"account.field.created_at": "أنشئت في",
|
||||
"item.field.type": "نوع الصنف",
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
"Opening Balance Liabilities": "Opening Balance Liabilities",
|
||||
"Loan": "Loan",
|
||||
"Owner A Drawings": "Owner A Drawings",
|
||||
"An account that holds valuation of products or goods that availiable for sale.": "An account that holds valuation of products or goods that availiable for sale.",
|
||||
"An account that holds valuation of products or goods that available for sale.": "An account that holds valuation of products or goods that available for sale.",
|
||||
"Tracks the gain and losses of the exchange differences.": "Tracks the gain and losses of the exchange differences.",
|
||||
"Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.",
|
||||
"The income activities are not associated to the core business.": "The income activities are not associated to the core business.",
|
||||
@@ -176,6 +176,7 @@
|
||||
"invoice.paper.conditions_title": "Conditions & terms",
|
||||
"invoice.paper.notes_title": "Notes",
|
||||
"invoice.paper.total": "Total",
|
||||
"invoice.paper.subtotal": "Subtotal",
|
||||
"invoice.paper.payment_amount": "Payment Amount",
|
||||
"invoice.paper.balance_due": "Balance Due",
|
||||
|
||||
@@ -240,28 +241,32 @@
|
||||
"account.field.normal.credit": "Credit",
|
||||
"account.field.normal.debit": "Debit",
|
||||
"account.field.type": "Type",
|
||||
"account.field.active": "Activity",
|
||||
"account.field.active": "Active",
|
||||
"account.field.currency": "Currency",
|
||||
"account.field.balance": "Balance",
|
||||
"account.field.bank_balance": "Bank Balance",
|
||||
"account.field.parent_account": "Parent Account",
|
||||
"account.field.created_at": "Created at",
|
||||
"item.field.type": "Item type",
|
||||
"item.field.type": "Item Type",
|
||||
"item.field.type.inventory": "Inventory",
|
||||
"item.field.type.service": "Service",
|
||||
"item.field.type.non-inventory": "Non inventory",
|
||||
"item.field.name": "Name",
|
||||
"item.field.code": "Code",
|
||||
"item.field.type.non-inventory": "Non Inventory",
|
||||
"item.field.name": "Item Name",
|
||||
"item.field.code": "Item Code",
|
||||
"item.field.sellable": "Sellable",
|
||||
"item.field.purchasable": "Purchasable",
|
||||
"item.field.cost_price": "Cost price",
|
||||
"item.field.cost_account": "Cost account",
|
||||
"item.field.sell_account": "Sell account",
|
||||
"item.field.sell_description": "Sell description",
|
||||
"item.field.inventory_account": "Inventory account",
|
||||
"item.field.purchase_description": "Purchase description",
|
||||
"item.field.quantity_on_hand": "Quantity on hand",
|
||||
"item.field.cost_price": "Cost Price",
|
||||
"item.field.sell_price": "Sell Price",
|
||||
"item.field.cost_account": "Cost Account",
|
||||
"item.field.sell_account": "Sell Account",
|
||||
"item.field.sell_description": "Sell Description",
|
||||
"item.field.inventory_account": "Inventory Account",
|
||||
"item.field.purchase_description": "Purchase Description",
|
||||
"item.field.quantity_on_hand": "Quantity on Hand",
|
||||
"item.field.note": "Note",
|
||||
"item.field.category": "Category",
|
||||
"item.field.active": "Active",
|
||||
"item.field.created_at": "Created at",
|
||||
"item.field.created_at": "Created At",
|
||||
"item_category.field.name": "Name",
|
||||
"item_category.field.description": "Description",
|
||||
"item_category.field.count": "Count",
|
||||
@@ -274,8 +279,14 @@
|
||||
"invoice.field.invoice_message": "Invoice message",
|
||||
"invoice.field.terms_conditions": "Terms & conditions",
|
||||
"invoice.field.amount": "Amount",
|
||||
"invoice.field.exchange_rate": "Exchange Rate",
|
||||
"invoice.field.payment_amount": "Payment amount",
|
||||
"invoice.field.due_amount": "Due amount",
|
||||
"invoice.field.delivered": "Delivered",
|
||||
"invoice.field.item_name": "Item Name",
|
||||
"invoice.field.rate": "Rate",
|
||||
"invoice.field.quantity": "Quantity",
|
||||
"invoice.field.description": "Description",
|
||||
"invoice.field.status": "Status",
|
||||
"invoice.field.status.paid": "Paid",
|
||||
"invoice.field.status.partially-paid": "Partially paid",
|
||||
@@ -284,6 +295,8 @@
|
||||
"invoice.field.status.delivered": "Delivered",
|
||||
"invoice.field.status.draft": "Draft",
|
||||
"invoice.field.created_at": "Created at",
|
||||
"invoice.field.currency": "Currency",
|
||||
"invoice.field.entries": "Entries",
|
||||
"estimate.field.amount": "Amount",
|
||||
"estimate.field.estimate_number": "Estimate number",
|
||||
"estimate.field.customer": "Customer",
|
||||
@@ -298,22 +311,31 @@
|
||||
"estimate.field.status.approved": "Approved",
|
||||
"estimate.field.status.draft": "Draft",
|
||||
"estimate.field.created_at": "Created at",
|
||||
"payment_receive.field.customer": "Customer",
|
||||
"payment_receive.field.payment_date": "Payment date",
|
||||
"payment_receive.field.amount": "Amount",
|
||||
"payment_receive.field.reference_no": "Reference No.",
|
||||
"payment_receive.field.deposit_account": "Deposit account",
|
||||
"payment_receive.field.payment_receive_no": "Payment receive No.",
|
||||
"payment_receive.field.statement": "Statement",
|
||||
"payment_receive.field.created_at": "Created at",
|
||||
"payment_receive.field.customer": "Customer",
|
||||
"payment_receive.field.exchange_rate": "Exchange Rate",
|
||||
"payment_receive.field.payment_date": "Payment Date",
|
||||
"payment_receive.field.reference_no": "Reference No.",
|
||||
"payment_receive.field.deposit_account": "Deposit Account",
|
||||
"payment_receive.field.entries": "Entries",
|
||||
"payment_receive.field.invoice": "Invoice",
|
||||
"payment_receive.field.entries.payment_amount": "Payment Amount",
|
||||
"bill_payment.field.vendor": "Vendor",
|
||||
"bill_payment.field.amount": "Amount",
|
||||
"bill_payment.field.due_amount": "Due amount",
|
||||
"bill_payment.field.payment_account": "Payment account",
|
||||
"bill_payment.field.payment_number": "Payment number",
|
||||
"bill_payment.field.payment_date": "Payment date",
|
||||
"bill_payment.field.due_amount": "Due Amount",
|
||||
"bill_payment.field.payment_account": "Payment Account",
|
||||
"bill_payment.field.payment_number": "Payment No.",
|
||||
"bill_payment.field.payment_date": "Payment Date",
|
||||
"bill_payment.field.reference_no": "Reference No.",
|
||||
"bill_payment.field.description": "Description",
|
||||
"bill_payment.field.exchange_rate": "Exchange Rate",
|
||||
"bill_payment.field.note": "Note",
|
||||
"bill_payment.field.entries.bill": "Bill No.",
|
||||
"bill_payment.field.entries.payment_amount": "Payment Amount",
|
||||
"bill_payment.field.reference": "Reference No.",
|
||||
"bill_payment.field.created_at": "Created at",
|
||||
"bill.field.vendor": "Vendor",
|
||||
"bill.field.bill_number": "Bill number",
|
||||
@@ -341,22 +363,30 @@
|
||||
"inventory_adjustment.field.description": "Description",
|
||||
"inventory_adjustment.field.published_at": "Published at",
|
||||
"inventory_adjustment.field.created_at": "Created at",
|
||||
"expense.field.payment_date": "Payment date",
|
||||
"expense.field.payment_account": "Payment account",
|
||||
"expense.field.payment_date": "Payment Date",
|
||||
"expense.field.payment_account": "Payment Account",
|
||||
"expense.field.amount": "Amount",
|
||||
"expense.field.currency_code": "Currency",
|
||||
"expense.field.exchange_rate": "Exchange Rate",
|
||||
"expense.field.reference_no": "Reference No.",
|
||||
"expense.field.description": "Description",
|
||||
"expense.field.line_description": "Line Description",
|
||||
"expense.field.published": "Published",
|
||||
"expense.field.categories": "Categories",
|
||||
"expense.field.expense_account": "Expense Account",
|
||||
"expense.field.publish": "Publish",
|
||||
"expense.field.status": "Status",
|
||||
"expense.field.status.draft": "Draft",
|
||||
"expense.field.status.published": "Published",
|
||||
"expense.field.created_at": "Created at",
|
||||
"manual_journal.field.date": "Date",
|
||||
"manual_journal.field.journal_number": "Journal number",
|
||||
"manual_journal.field.journal_number": "Journal No.",
|
||||
"manual_journal.field.reference": "Reference No.",
|
||||
"manual_journal.field.journal_type": "Journal type",
|
||||
"manual_journal.field.journal_type": "Journal Type",
|
||||
"manual_journal.field.amount": "Amount",
|
||||
"manual_journal.field.description": "Description",
|
||||
"manual_journal.field.currency": "Currency",
|
||||
"manual_journal.field.exchange_rate": "Exchange Rate",
|
||||
"manual_journal.field.status": "Status",
|
||||
"manual_journal.field.created_at": "Created at",
|
||||
"receipt.field.amount": "Amount",
|
||||
@@ -375,8 +405,8 @@
|
||||
"customer.field.last_name": "Last name",
|
||||
"customer.field.display_name": "Display name",
|
||||
"customer.field.email": "Email",
|
||||
"customer.field.work_phone": "Work phone",
|
||||
"customer.field.personal_phone": "Personal phone",
|
||||
"customer.field.work_phone": "Work Phone Number",
|
||||
"customer.field.personal_phone": "Personal Phone Number",
|
||||
"customer.field.company_name": "Company name",
|
||||
"customer.field.website": "Website",
|
||||
"customer.field.opening_balance_at": "Opening balance at",
|
||||
@@ -384,7 +414,7 @@
|
||||
"customer.field.created_at": "Created at",
|
||||
"customer.field.balance": "Balance",
|
||||
"customer.field.status": "Status",
|
||||
"customer.field.currency": "Curreny",
|
||||
"customer.field.currency": "Currency",
|
||||
"customer.field.status.active": "Active",
|
||||
"customer.field.status.inactive": "Inactive",
|
||||
"customer.field.status.overdue": "Overdue",
|
||||
@@ -393,8 +423,8 @@
|
||||
"vendor.field.last_name": "Last name",
|
||||
"vendor.field.display_name": "Display name",
|
||||
"vendor.field.email": "Email",
|
||||
"vendor.field.work_phone": "Work phone",
|
||||
"vendor.field.personal_phone": "Personal phone",
|
||||
"vendor.field.work_phone": "Work Phone Number",
|
||||
"vendor.field.personal_phone": "Personal Phone Number",
|
||||
"vendor.field.company_name": "Company name",
|
||||
"vendor.field.website": "Website",
|
||||
"vendor.field.opening_balance_at": "Opening balance at",
|
||||
@@ -402,13 +432,16 @@
|
||||
"vendor.field.created_at": "Created at",
|
||||
"vendor.field.balance": "Balance",
|
||||
"vendor.field.status": "Status",
|
||||
"vendor.field.currency": "Curreny",
|
||||
"vendor.field.note": "Note",
|
||||
"vendor.field.currency": "Currency",
|
||||
"vendor.field.status.active": "Active",
|
||||
"vendor.field.status.inactive": "Inactive",
|
||||
"vendor.field.status.overdue": "Overdue",
|
||||
"vendor.field.status.unpaid": "Unpaid",
|
||||
"Invoice write-off": "Invoice write-off",
|
||||
|
||||
|
||||
|
||||
"transaction_type.credit_note": "Credit note",
|
||||
"transaction_type.refund_credit_note": "Refund credit note",
|
||||
"transaction_type.vendor_credit": "Vendor credit",
|
||||
@@ -587,6 +620,7 @@
|
||||
"balance_sheet.long_term_liabilities": "Long-Term Liabilities",
|
||||
"balance_sheet.non_current_liabilities": "Non-Current Liabilities",
|
||||
"balance_sheet.equity": "Equity",
|
||||
"balance_sheet.net_income": "Net Income",
|
||||
|
||||
"balance_sheet.account_name": "Account name",
|
||||
"balance_sheet.total": "Total",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
@import "../base.scss";
|
||||
|
||||
body {
|
||||
font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
.sheet__title{
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.sheet__title h2{
|
||||
line-height: 1;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.sheet__table {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
.sheet__table {
|
||||
table-layout: auto;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.sheet__table thead tr th {
|
||||
border-top: 1px solid #000;
|
||||
border-bottom: 1px solid #000;
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.sheet__table tbody tr td {
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid #CCC;
|
||||
}
|
||||
57
packages/server/resources/scss/modules/financial-sheet.scss
Normal file
57
packages/server/resources/scss/modules/financial-sheet.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
@import "../base.scss";
|
||||
|
||||
html,
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
body{
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.28581;
|
||||
text-transform: none;
|
||||
color: #000;
|
||||
font-family: Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, Icons16, sans-serif;
|
||||
}
|
||||
.sheet{
|
||||
padding: 20px;
|
||||
}
|
||||
.sheet__company-name{
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.sheet__sheet-type {
|
||||
margin: 0
|
||||
}
|
||||
.sheet__sheet-date {
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.sheet__header {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sheet__table {
|
||||
border-top: 1px solid #000;
|
||||
table-layout: fixed;
|
||||
border-spacing: 0;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sheet__table thead th {
|
||||
color: #000;
|
||||
border-bottom: 1px solid #000000;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.sheet__table tbody td {
|
||||
border-bottom: 0;
|
||||
padding-top: 0.28rem;
|
||||
padding-bottom: 0.28rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
color: #252A31;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
@@ -137,10 +137,14 @@
|
||||
tbody tr.payment-amount td:last-child {
|
||||
color: red
|
||||
}
|
||||
tbody tr.blanace-due td {
|
||||
tbody tr.blanace-due td{
|
||||
border-top: 3px double #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
tbody tr.total td {
|
||||
border-top: 1px solid #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
block head
|
||||
style
|
||||
include ../../css/modules/export-resource-table.css
|
||||
|
||||
style.
|
||||
!{customCSS}
|
||||
|
||||
block content
|
||||
.sheet
|
||||
.sheet__title
|
||||
h2.sheetTitle= sheetTitle
|
||||
p.sheetDesc= sheetDescription
|
||||
|
||||
table.sheet__table
|
||||
thead
|
||||
tr
|
||||
each column in table.columns
|
||||
th(style=column.style class='column--' + column.key)= column.name
|
||||
tbody
|
||||
each row in table.rows
|
||||
tr(class=row.classNames)
|
||||
each cell in row.cells
|
||||
td(class='cell--' + cell.key)
|
||||
span!= cell.value
|
||||
25
packages/server/resources/views/modules/financial-sheet.pug
Normal file
25
packages/server/resources/views/modules/financial-sheet.pug
Normal file
@@ -0,0 +1,25 @@
|
||||
block head
|
||||
style
|
||||
include ../../css/modules/financial-sheet.css
|
||||
|
||||
style.
|
||||
!{customCSS}
|
||||
|
||||
block content
|
||||
.sheet
|
||||
.sheet__header
|
||||
.sheet__company-name=organizationName
|
||||
.sheet__sheet-type=sheetName
|
||||
.sheet__sheet-date=sheetDate
|
||||
|
||||
table.sheet__table
|
||||
thead
|
||||
tr
|
||||
each column in table.columns
|
||||
th(style=column.style class='column--' + column.key)= column.label
|
||||
tbody
|
||||
each row in table.rows
|
||||
tr(class=row.classNames)
|
||||
each cell in row.cells
|
||||
td(class='cell--' + cell.key)
|
||||
span!= cell.value
|
||||
@@ -22,12 +22,12 @@ block content
|
||||
|
||||
div.invoice__due-amount
|
||||
div.label #{__('invoice.paper.invoice_amount')}
|
||||
div.amount #{saleInvoice.formattedAmount}
|
||||
div.amount #{saleInvoice.totalFormatted}
|
||||
|
||||
div.invoice__meta
|
||||
div.invoice__meta-item.invoice__meta-item--amount
|
||||
span.label #{__('invoice.paper.due_amount')}
|
||||
span.value #{saleInvoice.formattedDueAmount}
|
||||
span.value #{saleInvoice.dueAmountFormatted}
|
||||
|
||||
div.invoice__meta-item.invoice__meta-item--billed-to
|
||||
span.label #{__("invoice.paper.billed_to")}
|
||||
@@ -35,11 +35,11 @@ block content
|
||||
|
||||
div.invoice__meta-item.invoice__meta-item--invoice-date
|
||||
span.label #{__("invoice.paper.invoice_date")}
|
||||
span.value #{saleInvoice.formattedInvoiceDate}
|
||||
span.value #{saleInvoice.invoiceDateFormatted}
|
||||
|
||||
div.invoice__meta-item.invoice__meta-item--due-date
|
||||
span.label #{__("invoice.paper.due_date")}
|
||||
span.value #{saleInvoice.formattedDueDate}
|
||||
span.value #{saleInvoice.dueDateFormatted}
|
||||
|
||||
div.invoice__table
|
||||
table
|
||||
@@ -63,15 +63,22 @@ block content
|
||||
div.invoice__table-total
|
||||
table
|
||||
tbody
|
||||
tr.subtotal
|
||||
td #{__('invoice.paper.subtotal')}
|
||||
td #{saleInvoice.subtotalFormatted}
|
||||
each tax in saleInvoice.taxes
|
||||
tr.tax_line
|
||||
td #{tax.name} [#{tax.taxRate}%]
|
||||
td #{tax.taxRateAmountFormatted}
|
||||
tr.total
|
||||
td #{__('invoice.paper.total')}
|
||||
td #{saleInvoice.formattedAmount}
|
||||
td #{saleInvoice.totalFormatted}
|
||||
tr.payment-amount
|
||||
td #{__('invoice.paper.payment_amount')}
|
||||
td #{saleInvoice.formattedPaymentAmount}
|
||||
td #{saleInvoice.paymentAmountFormatted}
|
||||
tr.blanace-due
|
||||
td #{__('invoice.paper.balance_due')}
|
||||
td #{saleInvoice.formattedDueAmount}
|
||||
td #{saleInvoice.dueAmountFormatted}
|
||||
|
||||
div.invoice__footer
|
||||
if saleInvoice.termsConditions
|
||||
|
||||
@@ -45,9 +45,9 @@ block content
|
||||
each entry in paymentReceive.entries
|
||||
tr
|
||||
td.item=entry.invoice.invoiceNo
|
||||
td.date=entry.invoice.formattedInvoiceDate
|
||||
td.invoiceAmount=entry.invoice.formattedAmount
|
||||
td.paymentAmount=entry.invoice.formattedPaymentAmount
|
||||
td.date=entry.invoice.invoiceDateFormatted
|
||||
td.invoiceAmount=entry.invoice.totalFormatted
|
||||
td.paymentAmount=entry.invoice.paymentAmountFormatted
|
||||
|
||||
div.payment__table-after
|
||||
div.payment__table-total
|
||||
|
||||
@@ -66,12 +66,14 @@ module.exports = {
|
||||
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
|
||||
// minify: true, // Allow to enable/disable minify the source.
|
||||
},
|
||||
// {
|
||||
// src: './assets/sass/editor-style.scss',
|
||||
// dest: './assets/css',
|
||||
// sourcemaps: true,
|
||||
// minify: true,
|
||||
// },
|
||||
{
|
||||
src: `${RESOURCES_PATH}/scss/modules/financial-sheet.scss`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/scss/modules/export-resource-table.scss`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
},
|
||||
],
|
||||
// RTL builds.
|
||||
rtl: [
|
||||
@@ -114,7 +116,7 @@ module.exports = {
|
||||
// SASS Configuration for all builds.
|
||||
sass: {
|
||||
errLogToConsole: true,
|
||||
// outputStyle: 'compact',
|
||||
// outputStyle: 'compact',
|
||||
},
|
||||
|
||||
// CSS MQ Packer configuration for all builds and style tasks.
|
||||
|
||||
@@ -65,6 +65,9 @@ exports.getCommonWebpackOptions = ({
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
};
|
||||
|
||||
if (isDev) {
|
||||
|
||||
@@ -3,7 +3,12 @@ import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
|
||||
import {
|
||||
AbilitySubject,
|
||||
AccountAction,
|
||||
IAccountDTO,
|
||||
IAccountsStructureType,
|
||||
} from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
@@ -22,7 +27,7 @@ export default class AccountsController extends BaseController {
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -93,7 +98,7 @@ export default class AccountsController extends BaseController {
|
||||
/**
|
||||
* Create account DTO Schema validation.
|
||||
*/
|
||||
get createAccountDTOSchema() {
|
||||
private get createAccountDTOSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
@@ -126,7 +131,7 @@ export default class AccountsController extends BaseController {
|
||||
/**
|
||||
* Account DTO Schema validation.
|
||||
*/
|
||||
get editAccountDTOSchema() {
|
||||
private get editAccountDTOSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
@@ -155,14 +160,14 @@ export default class AccountsController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
get accountParamSchema() {
|
||||
private get accountParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts list validation schema.
|
||||
*/
|
||||
get accountsListSchema() {
|
||||
private get accountsListSchema() {
|
||||
return [
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
@@ -172,6 +177,11 @@ export default class AccountsController extends BaseController {
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
|
||||
query('structure')
|
||||
.optional()
|
||||
.isString()
|
||||
.isIn([IAccountsStructureType.Tree, IAccountsStructureType.Flat]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -197,7 +207,6 @@ export default class AccountsController extends BaseController {
|
||||
tenantId,
|
||||
accountDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: account.id,
|
||||
message: 'The account has been created successfully.',
|
||||
@@ -341,6 +350,7 @@ export default class AccountsController extends BaseController {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
inactiveMode: false,
|
||||
structure: IAccountsStructureType.Tree,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import mime from 'mime-types';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Response, NextFunction, Request } from 'express';
|
||||
import { body, param } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AttachmentsApplication } from '@/services/Attachments/AttachmentsApplication';
|
||||
import { AttachmentUploadPipeline } from '@/services/Attachments/S3UploadPipeline';
|
||||
|
||||
@Service()
|
||||
export class AttachmentsController extends BaseController {
|
||||
@Inject()
|
||||
private attachmentsApplication: AttachmentsApplication;
|
||||
|
||||
@Inject()
|
||||
private uploadPipelineService: AttachmentUploadPipeline;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
this.uploadPipelineService.validateS3Configured,
|
||||
this.uploadPipelineService.uploadPipeline().single('file'),
|
||||
this.validateUploadedFileExistance,
|
||||
this.uploadAttachment.bind(this)
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[param('id').exists()],
|
||||
this.validationResult,
|
||||
this.deleteAttachment.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[param('id').exists()],
|
||||
this.validationResult,
|
||||
this.getAttachment.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/:id/link',
|
||||
[body('modelRef').exists(), body('modelId').exists()],
|
||||
this.validationResult
|
||||
);
|
||||
router.post(
|
||||
'/:id/link',
|
||||
[body('modelRef').exists(), body('modelId').exists()],
|
||||
this.validationResult,
|
||||
this.linkDocument.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/:id/unlink',
|
||||
[body('modelRef').exists(), body('modelId').exists()],
|
||||
this.validationResult,
|
||||
this.unlinkDocument.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/:id/presigned-url',
|
||||
[param('id').exists()],
|
||||
this.validationResult,
|
||||
this.getAttachmentPresignedUrl.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the upload file existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response|void}
|
||||
*/
|
||||
private validateUploadedFileExistance(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (!req.file) {
|
||||
return res.boom.badRequest(null, {
|
||||
errorType: 'FILE_UPLOAD_FAILED',
|
||||
message: 'Now file uploaded.',
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the attachments to S3 and store the file metadata to DB.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response|void}
|
||||
*/
|
||||
private async uploadAttachment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const file = req.file;
|
||||
|
||||
try {
|
||||
const data = await this.attachmentsApplication.upload(tenantId, file);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The document has uploaded successfully.',
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given attachment key.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|void>}
|
||||
*/
|
||||
private async getAttachment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.attachmentsApplication.get(tenantId, id);
|
||||
|
||||
const byte = await data.Body.transformToByteArray();
|
||||
const extension = mime.extension(data.ContentType);
|
||||
const buffer = Buffer.from(byte);
|
||||
|
||||
res.set(
|
||||
'Content-Disposition',
|
||||
`filename="${req.params.id}.${extension}"`
|
||||
);
|
||||
res.set('Content-Type', data.ContentType);
|
||||
res.send(buffer);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given document key.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|void>}
|
||||
*/
|
||||
private async deleteAttachment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const { id: documentId } = req.params;
|
||||
|
||||
try {
|
||||
await this.attachmentsApplication.delete(tenantId, documentId);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The document has been delete successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given document key.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|void>}
|
||||
*/
|
||||
private async linkDocument(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: Function
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const { id: documentId } = req.params;
|
||||
const { modelRef, modelId } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.attachmentsApplication.link(
|
||||
tenantId,
|
||||
documentId,
|
||||
modelRef,
|
||||
modelId
|
||||
);
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The document has been linked successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given document key.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|void>}
|
||||
*/
|
||||
private async unlinkDocument(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const { id: documentId } = req.params;
|
||||
const { modelRef, modelId } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.attachmentsApplication.link(
|
||||
tenantId,
|
||||
documentId,
|
||||
modelRef,
|
||||
modelId
|
||||
);
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The document has been linked successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreives the presigned url of the given attachment key.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|void>}
|
||||
*/
|
||||
private async getAttachmentPresignedUrl(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { id: documentKey } = req.params;
|
||||
|
||||
try {
|
||||
const presignedUrl = await this.attachmentsApplication.getPresignedUrl(
|
||||
documentKey
|
||||
);
|
||||
return res.status(200).send({ presignedUrl });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
||||
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
|
||||
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
@Service()
|
||||
export default class AuthenticationController extends BaseController {
|
||||
@Inject()
|
||||
@@ -28,6 +30,20 @@ export default class AuthenticationController extends BaseController {
|
||||
asyncMiddleware(this.login.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.use('/register/verify/resend', JWTAuth);
|
||||
router.use('/register/verify/resend', AttachCurrentTenantUser);
|
||||
router.post(
|
||||
'/register/verify/resend',
|
||||
asyncMiddleware(this.registerVerifyResendMail.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/register/verify',
|
||||
this.signupVerifySchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.registerVerify.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/register',
|
||||
this.registerSchema,
|
||||
@@ -49,6 +65,7 @@ export default class AuthenticationController extends BaseController {
|
||||
asyncMiddleware(this.resetPassword.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this)));
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -91,12 +108,24 @@ export default class AuthenticationController extends BaseController {
|
||||
check('password')
|
||||
.exists()
|
||||
.isString()
|
||||
.isLength({ min: 6 })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
];
|
||||
}
|
||||
|
||||
private get signupVerifySchema(): ValidationChain[] {
|
||||
return [
|
||||
check('email')
|
||||
.exists()
|
||||
.isString()
|
||||
.isEmail()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('token').exists().isString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password schema.
|
||||
* @returns {ValidationChain[]}
|
||||
@@ -105,7 +134,7 @@ export default class AuthenticationController extends BaseController {
|
||||
return [
|
||||
check('password')
|
||||
.exists()
|
||||
.isLength({ min: 5 })
|
||||
.isLength({ min: 6 })
|
||||
.custom((value, { req }) => {
|
||||
if (value !== req.body.confirm_password) {
|
||||
throw new Error("Passwords don't match");
|
||||
@@ -164,6 +193,58 @@ export default class AuthenticationController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provider user's email after signin-up.
|
||||
* @param {Request} req
|
||||
* @param {Response}| res
|
||||
* @param {Function} next
|
||||
* @returns {Response|void}
|
||||
*/
|
||||
private async registerVerify(req: Request, res: Response, next: Function) {
|
||||
const signUpVerifyDTO: { email: string; token: string } =
|
||||
this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const user = await this.authApplication.signUpConfirm(
|
||||
signUpVerifyDTO.email,
|
||||
signUpVerifyDTO.token
|
||||
);
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
message: 'The given user has verified successfully',
|
||||
user,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends the confirmation email to the user.
|
||||
* @param {Request} req
|
||||
* @param {Response}| res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private async registerVerifyResendMail(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: Function
|
||||
) {
|
||||
const { user } = req;
|
||||
|
||||
try {
|
||||
const data = await this.authApplication.signUpConfirmResend(user.id);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
message: 'The given user has verified successfully',
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password handler
|
||||
* @param {Request} req
|
||||
@@ -207,6 +288,23 @@ export default class AuthenticationController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the authentication meta for SPA.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
* @returns {Response|void}
|
||||
*/
|
||||
private async getAuthMeta(req: Request, res: Response, next: Function) {
|
||||
try {
|
||||
const meta = await this.authApplication.getAuthMeta();
|
||||
|
||||
return res.status(200).send({ meta });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the service errors.
|
||||
*/
|
||||
@@ -247,6 +345,30 @@ export default class AuthenticationController extends BaseController {
|
||||
errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SIGNUP_RESTRICTED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'SIGNUP_RESTRICTED',
|
||||
message:
|
||||
'Sign-up is restricted no one can sign-up to the system.',
|
||||
code: 700,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SIGNUP_RESTRICTED_NOT_ALLOWED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
|
||||
message:
|
||||
'Sign-up is restricted the given email address is not allowed to sign-up.',
|
||||
code: 710,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
|
||||
|
||||
@Service()
|
||||
export class BankAccountsController extends BaseController {
|
||||
@Inject()
|
||||
private getBankAccountSummaryService: GetBankAccountSummary;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bank account meta summary.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
async getBankAccountSummary(
|
||||
req: Request<{ bankAccountId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { bankAccountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.getBankAccountSummaryService.getBankAccountSummary(
|
||||
tenantId,
|
||||
bankAccountId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
|
||||
import { body, param } from 'express-validator';
|
||||
import {
|
||||
GetMatchedTransactionsFilter,
|
||||
IMatchTransactionsDTO,
|
||||
} from '@/services/Banking/Matching/types';
|
||||
|
||||
@Service()
|
||||
export class BankTransactionsMatchingController extends BaseController {
|
||||
@Inject()
|
||||
private bankTransactionsMatchingApp: MatchBankTransactionsApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:transactionId',
|
||||
[
|
||||
param('transactionId').exists(),
|
||||
body('matchedTransactions').isArray({ min: 1 }),
|
||||
body('matchedTransactions.*.reference_type').exists(),
|
||||
body('matchedTransactions.*.reference_id').isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.matchBankTransaction.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/unmatch/:transactionId',
|
||||
[param('transactionId').exists()],
|
||||
this.validationResult,
|
||||
this.unmatchMatchedBankTransaction.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the given bank transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
private async matchBankTransaction(
|
||||
req: Request<{ transactionId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
const matchTransactionDTO = this.matchedBodyData(
|
||||
req
|
||||
) as IMatchTransactionsDTO;
|
||||
|
||||
try {
|
||||
await this.bankTransactionsMatchingApp.matchTransaction(
|
||||
tenantId,
|
||||
transactionId,
|
||||
matchTransactionDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: transactionId,
|
||||
message: 'The bank transaction has been matched.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmatches the matched bank transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
private async unmatchMatchedBankTransaction(
|
||||
req: Request<{ transactionId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
|
||||
try {
|
||||
await this.bankTransactionsMatchingApp.unmatchMatchedTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: transactionId,
|
||||
message: 'The bank matched transaction has been unmatched.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import Container, { Inject, Service } from 'typedi';
|
||||
import { Router } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { PlaidBankingController } from './PlaidBankingController';
|
||||
import { BankingRulesController } from './BankingRulesController';
|
||||
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
|
||||
import { RecognizedTransactionsController } from './RecognizedTransactionsController';
|
||||
import { BankAccountsController } from './BankAccountsController';
|
||||
|
||||
@Service()
|
||||
export class BankingController extends BaseController {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/plaid', Container.get(PlaidBankingController).router());
|
||||
router.use('/rules', Container.get(BankingRulesController).router());
|
||||
router.use(
|
||||
'/matches',
|
||||
Container.get(BankTransactionsMatchingController).router()
|
||||
);
|
||||
router.use(
|
||||
'/recognized',
|
||||
Container.get(RecognizedTransactionsController).router()
|
||||
);
|
||||
router.use(
|
||||
'/bank_accounts',
|
||||
Container.get(BankAccountsController).router()
|
||||
);
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { BankRulesApplication } from '@/services/Banking/Rules/BankRulesApplication';
|
||||
import { body, param } from 'express-validator';
|
||||
import {
|
||||
ICreateBankRuleDTO,
|
||||
IEditBankRuleDTO,
|
||||
} from '@/services/Banking/Rules/types';
|
||||
|
||||
@Service()
|
||||
export class BankingRulesController extends BaseController {
|
||||
@Inject()
|
||||
private bankRulesApplication: BankRulesApplication;
|
||||
|
||||
/**
|
||||
* Bank rule DTO validation schema.
|
||||
*/
|
||||
private get bankRuleValidationSchema() {
|
||||
return [
|
||||
body('name').isString().exists(),
|
||||
body('order').isInt({ min: 0 }),
|
||||
|
||||
// Apply to if transaction is.
|
||||
body('apply_if_account_id')
|
||||
.isInt({ min: 0 })
|
||||
.optional({ nullable: true }),
|
||||
body('apply_if_transaction_type').isIn(['deposit', 'withdrawal']),
|
||||
|
||||
// Conditions
|
||||
body('conditions_type').isString().isIn(['and', 'or']).default('and'),
|
||||
body('conditions').isArray({ min: 1 }),
|
||||
body('conditions.*.field').exists().isIn(['description', 'amount']),
|
||||
body('conditions.*.comparator')
|
||||
.exists()
|
||||
.isIn(['equals', 'contains', 'not_contain'])
|
||||
.default('contain'),
|
||||
body('conditions.*.value').exists(),
|
||||
|
||||
// Assign
|
||||
body('assign_category').isString(),
|
||||
body('assign_account_id').isInt({ min: 0 }),
|
||||
body('assign_payee').isString().optional({ nullable: true }),
|
||||
body('assign_memo').isString().optional({ nullable: true }),
|
||||
|
||||
body('recognition').isBoolean().toBoolean().optional({ nullable: true }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
[...this.bankRuleValidationSchema],
|
||||
this.validationResult,
|
||||
this.createBankRule.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[param('id').toInt().exists(), ...this.bankRuleValidationSchema],
|
||||
this.validationResult,
|
||||
this.editBankRule.bind(this)
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[param('id').toInt().exists()],
|
||||
this.validationResult,
|
||||
this.deleteBankRule.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[param('id').toInt().exists()],
|
||||
this.validationResult,
|
||||
this.getBankRule.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
[param('id').toInt().exists()],
|
||||
this.validationResult,
|
||||
this.getBankRules.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new bank rule.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async createBankRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const createBankRuleDTO = this.matchedBodyData(req) as ICreateBankRuleDTO;
|
||||
|
||||
try {
|
||||
const bankRule = await this.bankRulesApplication.createBankRule(
|
||||
tenantId,
|
||||
createBankRuleDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: bankRule.id,
|
||||
message: 'The bank rule has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given bank rule.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async editBankRule(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: ruleId } = req.params;
|
||||
const editBankRuleDTO = this.matchedBodyData(req) as IEditBankRuleDTO;
|
||||
|
||||
try {
|
||||
await this.bankRulesApplication.editBankRule(
|
||||
tenantId,
|
||||
ruleId,
|
||||
editBankRuleDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: ruleId,
|
||||
message: 'The bank rule has been updated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given bank rule.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async deleteBankRule(
|
||||
req: Request<{ id: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { id: ruleId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.bankRulesApplication.deleteBankRule(tenantId, ruleId);
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.send({ message: 'The bank rule has been deleted.', id: ruleId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given bank rule.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async getBankRule(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: ruleId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const bankRule = await this.bankRulesApplication.getBankRule(
|
||||
tenantId,
|
||||
ruleId
|
||||
);
|
||||
|
||||
return res.status(200).send({ bankRule });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bank rules.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async getBankRules(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const bankRules = await this.bankRulesApplication.getBankRules(tenantId);
|
||||
return res.status(200).send({ bankRules });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { param } from 'express-validator';
|
||||
import { NextFunction, Request, Response, Router, query } from 'express';
|
||||
import BaseController from '../BaseController';
|
||||
import { ExcludeBankTransactionsApplication } from '@/services/Banking/Exclude/ExcludeBankTransactionsApplication';
|
||||
|
||||
@Service()
|
||||
export class ExcludeBankTransactionsController extends BaseController {
|
||||
@Inject()
|
||||
private excludeBankTransactionApp: ExcludeBankTransactionsApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.put(
|
||||
'/transactions/:transactionId/exclude',
|
||||
[param('transactionId').exists()],
|
||||
this.validationResult,
|
||||
this.excludeBankTransaction.bind(this)
|
||||
);
|
||||
router.put(
|
||||
'/transactions/:transactionId/unexclude',
|
||||
[param('transactionId').exists()],
|
||||
this.validationResult,
|
||||
this.unexcludeBankTransaction.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/excluded',
|
||||
[],
|
||||
this.validationResult,
|
||||
this.getExcludedBankTransactions.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a bank transaction as excluded.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
private async excludeBankTransaction(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
|
||||
try {
|
||||
await this.excludeBankTransactionApp.excludeBankTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'The bank transaction has been excluded.',
|
||||
id: transactionId,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a bank transaction as not excluded.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|void>}
|
||||
*/
|
||||
private async unexcludeBankTransaction(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
|
||||
try {
|
||||
await this.excludeBankTransactionApp.unexcludeBankTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'The bank transaction has been unexcluded.',
|
||||
id: transactionId,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the excluded uncategorized bank transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
private async getExcludedBankTransactions(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedBodyData(req);
|
||||
|
||||
console.log('123');
|
||||
try {
|
||||
const data =
|
||||
await this.excludeBankTransactionApp.getExcludedBankTransactions(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(data);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
|
||||
|
||||
@Service()
|
||||
export class PlaidBankingController extends BaseController {
|
||||
@Inject()
|
||||
private plaidApp: PlaidApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post('/link-token', this.linkToken.bind(this));
|
||||
router.post('/exchange-token', this.exchangeToken.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Plaid link token.
|
||||
* @param {Request} req
|
||||
* @param {response} res
|
||||
* @returns {Response}
|
||||
*/
|
||||
private async linkToken(req: Request, res: Response) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const linkToken = await this.plaidApp.getLinkToken(tenantId);
|
||||
|
||||
return res.status(200).send(linkToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the given public token.
|
||||
* @param {Request} req
|
||||
* @param {response} res
|
||||
* @returns {Response}
|
||||
*/
|
||||
public async exchangeToken(req: Request, res: Response) {
|
||||
const { tenantId } = req;
|
||||
const { public_token, institution_id } = req.body;
|
||||
|
||||
await this.plaidApp.exchangeToken(tenantId, {
|
||||
institutionId: institution_id,
|
||||
publicToken: public_token,
|
||||
});
|
||||
return res.status(200).send({});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
|
||||
@Service()
|
||||
export class RecognizedTransactionsController extends BaseController {
|
||||
@Inject()
|
||||
private cashflowApplication: CashflowApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/', this.getRecognizedTransactions.bind(this));
|
||||
router.get(
|
||||
'/transactions/:uncategorizedTransactionId',
|
||||
this.getRecognizedTransaction.bind(this)
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the recognized bank transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
async getRecognizedTransactions(
|
||||
req: Request<{ accountId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const filter = this.matchedQueryData(req);
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const data = await this.cashflowApplication.getRecognizedTransactions(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(data);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the recognized transaction of the ginen uncategorized transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
async getRecognizedTransaction(
|
||||
req: Request<{ uncategorizedTransactionId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { uncategorizedTransactionId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.cashflowApplication.getRecognizedTransaction(
|
||||
tenantId,
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import CommandCashflowTransaction from './NewCashflowTransaction';
|
||||
import DeleteCashflowTransaction from './DeleteCashflowTransaction';
|
||||
import GetCashflowTransaction from './GetCashflowTransaction';
|
||||
import GetCashflowAccounts from './GetCashflowAccounts';
|
||||
import { ExcludeBankTransactionsController } from '../Banking/ExcludeBankTransactionsController';
|
||||
|
||||
@Service()
|
||||
export default class CashflowController {
|
||||
@@ -13,9 +14,10 @@ export default class CashflowController {
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(Container.get(CommandCashflowTransaction).router());
|
||||
router.use(Container.get(ExcludeBankTransactionsController).router());
|
||||
router.use(Container.get(GetCashflowTransaction).router());
|
||||
router.use(Container.get(GetCashflowAccounts).router());
|
||||
router.use(Container.get(CommandCashflowTransaction).router());
|
||||
router.use(Container.get(DeleteCashflowTransaction).router());
|
||||
|
||||
return router;
|
||||
|
||||
@@ -3,14 +3,15 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DeleteCashflowTransactionService from '../../../services/Cashflow/DeleteCashflowTransactionService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
|
||||
@Service()
|
||||
export default class DeleteCashflowTransaction extends BaseController {
|
||||
export default class DeleteCashflowTransactionController extends BaseController {
|
||||
@Inject()
|
||||
deleteCashflowService: DeleteCashflowTransactionService;
|
||||
private cashflowApplication: CashflowApplication;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
@@ -44,7 +45,7 @@ export default class DeleteCashflowTransaction extends BaseController {
|
||||
|
||||
try {
|
||||
const { oldCashflowTransaction } =
|
||||
await this.deleteCashflowService.deleteCashflowTransaction(
|
||||
await this.cashflowApplication.deleteTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
@@ -92,6 +93,19 @@ export default class DeleteCashflowTransaction extends BaseController {
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType ===
|
||||
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED'
|
||||
) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
|
||||
code: 4100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param, query } from 'express-validator';
|
||||
import GetCashflowAccountsService from '@/services/Cashflow/GetCashflowAccountsService';
|
||||
import { query } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
|
||||
@Service()
|
||||
export default class GetCashflowAccounts extends BaseController {
|
||||
@Inject()
|
||||
getCashflowAccountsService: GetCashflowAccountsService;
|
||||
|
||||
@Inject()
|
||||
getCashflowTransactionsService: GetCashflowTransactionsService;
|
||||
private cashflowApplication: CashflowApplication;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
@@ -35,7 +31,6 @@ export default class GetCashflowAccounts extends BaseController {
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
],
|
||||
this.asyncMiddleware(this.getCashflowAccounts),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
@@ -62,10 +57,7 @@ export default class GetCashflowAccounts extends BaseController {
|
||||
|
||||
try {
|
||||
const cashflowAccounts =
|
||||
await this.getCashflowAccountsService.getCashflowAccounts(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
await this.cashflowApplication.getCashflowAccounts(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
cashflow_accounts: this.transfromToResponse(cashflowAccounts),
|
||||
@@ -74,22 +66,4 @@ export default class GetCashflowAccounts extends BaseController {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param {Error} error - Error.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,20 @@ import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
import { GetMatchedTransactionsFilter } from '@/services/Banking/Matching/types';
|
||||
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
|
||||
|
||||
@Service()
|
||||
export default class GetCashflowAccounts extends BaseController {
|
||||
@Inject()
|
||||
getCashflowTransactionsService: GetCashflowTransactionsService;
|
||||
private cashflowApplication: CashflowApplication;
|
||||
|
||||
@Inject()
|
||||
private bankTransactionsMatchingApp: MatchBankTransactionsApplication;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
@@ -18,6 +23,10 @@ export default class GetCashflowAccounts extends BaseController {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/transactions/:transactionId/matches',
|
||||
this.getMatchedTransactions.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/transactions/:transactionId',
|
||||
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
|
||||
@@ -43,12 +52,10 @@ export default class GetCashflowAccounts extends BaseController {
|
||||
const { transactionId } = req.params;
|
||||
|
||||
try {
|
||||
const cashflowTransaction =
|
||||
await this.getCashflowTransactionsService.getCashflowTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
|
||||
const cashflowTransaction = await this.cashflowApplication.getTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
return res.status(200).send({
|
||||
cashflow_transaction: this.transfromToResponse(cashflowTransaction),
|
||||
});
|
||||
@@ -57,6 +64,34 @@ export default class GetCashflowAccounts extends BaseController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async getMatchedTransactions(
|
||||
req: Request<{ transactionId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.bankTransactionsMatchingApp.getMatchedTransactions(
|
||||
tenantId,
|
||||
transactionId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(data);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param {Error} error - Error.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { check } from 'express-validator';
|
||||
import { ValidationChain, check, param, query } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import BaseController from '../BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
|
||||
@Service()
|
||||
export default class NewCashflowTransactionController extends BaseController {
|
||||
@Inject()
|
||||
private newCashflowTranscationService: NewCashflowTransactionService;
|
||||
private cashflowApplication: CashflowApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -18,6 +18,18 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/transactions/uncategorized/:id',
|
||||
this.asyncMiddleware(this.getUncategorizedCashflowTransaction),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/transactions/:id/uncategorized',
|
||||
this.getUncategorizedTransactionsValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getUncategorizedCashflowTransactions),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/transactions',
|
||||
CheckPolicies(CashflowAction.Create, AbilitySubject.Cashflow),
|
||||
@@ -26,13 +38,72 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
this.asyncMiddleware(this.newCashflowTransaction),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/transactions/:id/uncategorize',
|
||||
this.revertCategorizedCashflowTransaction,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/transactions/:id/categorize',
|
||||
this.categorizeCashflowTransactionValidationSchema,
|
||||
this.validationResult,
|
||||
this.categorizeCashflowTransaction,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/transaction/:id/categorize/expense',
|
||||
this.categorizeAsExpenseValidationSchema,
|
||||
this.validationResult,
|
||||
this.categorizesCashflowTransactionAsExpense,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting uncategorized transactions validation schema.
|
||||
* @returns {ValidationChain}
|
||||
*/
|
||||
public get getUncategorizedTransactionsValidationSchema() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize as expense validation schema.
|
||||
*/
|
||||
public get categorizeAsExpenseValidationSchema() {
|
||||
return [
|
||||
check('expense_account_id').exists(),
|
||||
check('date').isISO8601().exists(),
|
||||
check('reference_no').optional(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize cashflow tranasction validation schema.
|
||||
*/
|
||||
public get categorizeCashflowTransactionValidationSchema() {
|
||||
return [
|
||||
check('date').exists().isISO8601().toDate(),
|
||||
check('credit_account_id').exists().isInt().toInt(),
|
||||
check('transaction_number').optional(),
|
||||
check('transaction_type').exists(),
|
||||
check('reference_no').optional(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
check('description').optional(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* New cashflow transaction validation schema.
|
||||
*/
|
||||
get newTransactionValidationSchema() {
|
||||
public get newTransactionValidationSchema() {
|
||||
return [
|
||||
check('date').exists().isISO8601().toDate(),
|
||||
check('reference_no').optional({ nullable: true }).trim().escape(),
|
||||
@@ -48,9 +119,7 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
check('credit_account_id').exists().isInt().toInt(),
|
||||
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('publish').default(false).isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
@@ -70,13 +139,12 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
const ownerContributionDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { cashflowTransaction } =
|
||||
await this.newCashflowTranscationService.newCashflowTransaction(
|
||||
const cashflowTransaction =
|
||||
await this.cashflowApplication.createTransaction(
|
||||
tenantId,
|
||||
ownerContributionDTO,
|
||||
userId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: cashflowTransaction.id,
|
||||
message: 'New cashflow transaction has been created successfully.',
|
||||
@@ -86,11 +154,147 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Revert the categorized cashflow transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private revertCategorizedCashflowTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: cashflowTransactionId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.cashflowApplication.uncategorizeTransaction(
|
||||
tenantId,
|
||||
cashflowTransactionId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Categorize the cashflow transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private categorizeCashflowTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: cashflowTransactionId } = req.params;
|
||||
const cashflowTransaction = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.cashflowApplication.categorizeTransaction(
|
||||
tenantId,
|
||||
cashflowTransactionId,
|
||||
cashflowTransaction
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'The cashflow transaction has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Categorize the transaction as expense transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private categorizesCashflowTransactionAsExpense = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: cashflowTransactionId } = req.params;
|
||||
const cashflowTransaction = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.cashflowApplication.categorizeAsExpense(
|
||||
tenantId,
|
||||
cashflowTransactionId,
|
||||
cashflowTransaction
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'The cashflow transaction has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the uncategorized cashflow transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getUncategorizedCashflowTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: transactionId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.cashflowApplication.getUncategorizedTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the uncategorized cashflow transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getUncategorizedCashflowTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: accountId } = req.params;
|
||||
const query = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const data = await this.cashflowApplication.getUncategorizedTransactions(
|
||||
tenantId,
|
||||
accountId,
|
||||
query
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the service errors.
|
||||
* @param error
|
||||
* @param req
|
||||
* @param res
|
||||
* @param {Request} req
|
||||
* @param {res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
@@ -140,6 +344,16 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
|
||||
code: 4100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -26,27 +26,27 @@ export default class ContactsController extends BaseController {
|
||||
[...this.autocompleteQuerySchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.autocompleteContacts.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getContact.bind(this))
|
||||
this.asyncMiddleware(this.getContact.bind(this)),
|
||||
);
|
||||
router.post(
|
||||
'/:id/inactivate',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.inactivateContact.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.post(
|
||||
'/:id/activate',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.activateContact.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export default class ContactsController extends BaseController {
|
||||
try {
|
||||
const contact = await this.contactsService.getContact(
|
||||
tenantId,
|
||||
contactId
|
||||
contactId,
|
||||
);
|
||||
return res.status(200).send({
|
||||
customer: this.transfromToResponse(contact),
|
||||
@@ -105,7 +105,7 @@ export default class ContactsController extends BaseController {
|
||||
try {
|
||||
const contacts = await this.contactsService.autocompleteContacts(
|
||||
tenantId,
|
||||
filter
|
||||
filter,
|
||||
);
|
||||
return res.status(200).send({ contacts });
|
||||
} catch (error) {
|
||||
@@ -153,7 +153,6 @@ export default class ContactsController extends BaseController {
|
||||
check('email')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.normalizeEmail()
|
||||
.isEmail()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('website')
|
||||
@@ -380,7 +379,7 @@ export default class ContactsController extends BaseController {
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
next: NextFunction,
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
|
||||
@@ -160,10 +160,8 @@ export default class CustomersController extends ContactsController {
|
||||
try {
|
||||
const contact = await this.customersApplication.createCustomer(
|
||||
tenantId,
|
||||
contactDTO,
|
||||
user
|
||||
contactDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contact.id,
|
||||
message: 'The customer has been created successfully.',
|
||||
|
||||
@@ -144,10 +144,8 @@ export default class VendorsController extends ContactsController {
|
||||
try {
|
||||
const vendor = await this.vendorsApplication.createVendor(
|
||||
tenantId,
|
||||
contactDTO,
|
||||
user
|
||||
contactDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: vendor.id,
|
||||
message: 'The vendor has been created successfully.',
|
||||
|
||||
@@ -5,13 +5,13 @@ import DashboardService from '@/services/Dashboard/DashboardService';
|
||||
@Service()
|
||||
export default class DashboardMetaController {
|
||||
@Inject()
|
||||
dashboardService: DashboardService;
|
||||
private dashboardService: DashboardService;
|
||||
|
||||
/**
|
||||
*
|
||||
* Constructor router.
|
||||
* @returns
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/boot', this.getDashboardBoot);
|
||||
@@ -25,7 +25,7 @@ export default class DashboardMetaController {
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
getDashboardBoot = async (
|
||||
private getDashboardBoot = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { query, oneOf } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from './BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import ExchangeRatesService from '@/services/ExchangeRates/ExchangeRatesService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { EchangeRateErrors } from '@/lib/ExchangeRate/types';
|
||||
import { ExchangeRateApplication } from '@/services/ExchangeRates/ExchangeRateApplication';
|
||||
|
||||
@Service()
|
||||
export default class ExchangeRatesController extends BaseController {
|
||||
@Inject()
|
||||
exchangeRatesService: ExchangeRatesService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
private exchangeRatesApp: ExchangeRateApplication;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
@@ -22,164 +19,40 @@ export default class ExchangeRatesController extends BaseController {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
[...this.exchangeRatesListSchema],
|
||||
'/latest',
|
||||
[
|
||||
oneOf([
|
||||
query('to_currency').exists().isString().isISO4217(),
|
||||
query('from_currency').exists().isString().isISO4217(),
|
||||
]),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.exchangeRates.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.handleServiceError,
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
[...this.exchangeRateDTOSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.addExchangeRate.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[...this.exchangeRateEditDTOSchema, ...this.exchangeRateIdSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editExchangeRate.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[...this.exchangeRateIdSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteExchangeRate.bind(this)),
|
||||
asyncMiddleware(this.latestExchangeRate.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
get exchangeRatesListSchema() {
|
||||
return [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
get exchangeRateDTOSchema() {
|
||||
return [
|
||||
check('exchange_rate').exists().isNumeric().toFloat(),
|
||||
check('currency_code').exists().trim().escape(),
|
||||
check('date').exists().isISO8601(),
|
||||
];
|
||||
}
|
||||
|
||||
get exchangeRateEditDTOSchema() {
|
||||
return [check('exchange_rate').exists().isNumeric().toFloat()];
|
||||
}
|
||||
|
||||
get exchangeRateIdSchema() {
|
||||
return [param('id').isNumeric().toInt()];
|
||||
}
|
||||
|
||||
get exchangeRatesIdsSchema() {
|
||||
return [
|
||||
query('ids').isArray({ min: 2 }),
|
||||
query('ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve exchange rates.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async exchangeRates(req: Request, res: Response, next: NextFunction) {
|
||||
private async latestExchangeRate(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
filterRoles: [],
|
||||
columnSortBy: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
if (filter.stringifiedFilterRoles) {
|
||||
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
|
||||
}
|
||||
try {
|
||||
const exchangeRates = await this.exchangeRatesService.listExchangeRates(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({ exchange_rates: exchangeRates });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new exchange rate on the given date.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async addExchangeRate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const exchangeRateDTO = this.matchedBodyData(req);
|
||||
const exchangeRateQuery = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const exchangeRate = await this.exchangeRatesService.newExchangeRate(
|
||||
const exchangeRate = await this.exchangeRatesApp.latest(
|
||||
tenantId,
|
||||
exchangeRateDTO
|
||||
exchangeRateQuery
|
||||
);
|
||||
return res.status(200).send({ id: exchangeRate.id });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the given exchange rate.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editExchangeRate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: exchangeRateId } = req.params;
|
||||
const exchangeRateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const exchangeRate = await this.exchangeRatesService.editExchangeRate(
|
||||
tenantId,
|
||||
exchangeRateId,
|
||||
exchangeRateDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: exchangeRateId,
|
||||
message: 'The exchange rate has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given exchange rate from the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteExchangeRate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: exchangeRateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.exchangeRatesService.deleteExchangeRate(
|
||||
tenantId,
|
||||
exchangeRateId
|
||||
);
|
||||
return res.status(200).send({ id: exchangeRateId });
|
||||
return res.status(200).send(exchangeRate);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@@ -192,26 +65,56 @@ export default class ExchangeRatesController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceError(
|
||||
private handleServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') {
|
||||
if (EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY === error.errorType) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }],
|
||||
errors: [
|
||||
{
|
||||
type: EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY,
|
||||
code: 100,
|
||||
message: 'The given base currency is invalid.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') {
|
||||
} else if (
|
||||
EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED === error.errorType
|
||||
) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }],
|
||||
errors: [
|
||||
{
|
||||
type: EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED,
|
||||
code: 200,
|
||||
message: 'The service is not allowed',
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (
|
||||
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED === error.errorType
|
||||
) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
|
||||
code: 300,
|
||||
message: 'The API key is required',
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED === error.errorType) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED,
|
||||
code: 400,
|
||||
message: 'The API rate limit has been exceeded',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export class ExpensesController extends BaseController {
|
||||
/**
|
||||
* Expense DTO schema.
|
||||
*/
|
||||
get expenseDTOSchema() {
|
||||
private get expenseDTOSchema() {
|
||||
return [
|
||||
check('reference_no')
|
||||
.optional({ nullable: true })
|
||||
@@ -130,6 +130,9 @@ export class ExpensesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -183,6 +186,9 @@ export class ExpensesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -269,7 +275,7 @@ export class ExpensesController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteExpense(req: Request, res: Response, next: NextFunction) {
|
||||
private async deleteExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: expenseId } = req.params;
|
||||
|
||||
@@ -291,7 +297,11 @@ export class ExpensesController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async publishExpense(req: Request, res: Response, next: NextFunction) {
|
||||
private async publishExpense(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: expenseId } = req.params;
|
||||
|
||||
@@ -313,7 +323,11 @@ export class ExpensesController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getExpensesList(req: Request, res: Response, next: NextFunction) {
|
||||
private async getExpensesList(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
@@ -343,7 +357,7 @@ export class ExpensesController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getExpense(req: Request, res: Response, next: NextFunction) {
|
||||
private async getExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: expenseId } = req.params;
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ExportApplication } from '@/services/Export/ExportApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { convertAcceptFormatToFormat } from './_utils';
|
||||
|
||||
@Service()
|
||||
export class ExportController extends BaseController {
|
||||
@Inject()
|
||||
private exportResourceApp: ExportApplication;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
[
|
||||
query('resource').exists(),
|
||||
query('format').isIn(['csv', 'xlsx']).optional(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.export.bind(this),
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports xlsx/csv to the given resource type.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private async export(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const query = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const accept = this.accepts(req);
|
||||
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
const applicationFormat = convertAcceptFormatToFormat(acceptType);
|
||||
|
||||
const data = await this.exportResourceApp.export(
|
||||
tenantId,
|
||||
query.resource,
|
||||
applicationFormat
|
||||
);
|
||||
// Retrieves the csv format.
|
||||
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(data);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(data);
|
||||
//
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': data.length,
|
||||
});
|
||||
res.send(data);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
packages/server/src/api/controllers/Export/_utils.ts
Normal file
13
packages/server/src/api/controllers/Export/_utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { ExportFormat } from '@/services/Export/common';
|
||||
|
||||
export const convertAcceptFormatToFormat = (accept: string): ExportFormat => {
|
||||
switch (accept) {
|
||||
case ACCEPT_TYPE.APPLICATION_CSV:
|
||||
return ExportFormat.Csv;
|
||||
case ACCEPT_TYPE.APPLICATION_PDF:
|
||||
return ExportFormat.Pdf;
|
||||
case ACCEPT_TYPE.APPLICATION_XLSX:
|
||||
return ExportFormat.Xlsx;
|
||||
}
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import InventoryDetailsController from './FinancialStatements/InventoryDetails';
|
||||
import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference';
|
||||
import CashflowAccountTransactions from './FinancialStatements/CashflowAccountTransactions';
|
||||
import ProjectProfitabilityController from './FinancialStatements/ProjectProfitabilitySummary';
|
||||
import SalesTaxLiabilitySummary from './FinancialStatements/SalesTaxLiabilitySummary';
|
||||
|
||||
@Service()
|
||||
export default class FinancialStatementsService {
|
||||
@@ -68,40 +69,44 @@ export default class FinancialStatementsService {
|
||||
);
|
||||
router.use(
|
||||
'/customer-balance-summary',
|
||||
Container.get(CustomerBalanceSummaryController).router(),
|
||||
Container.get(CustomerBalanceSummaryController).router()
|
||||
);
|
||||
router.use(
|
||||
'/vendor-balance-summary',
|
||||
Container.get(VendorBalanceSummaryController).router(),
|
||||
Container.get(VendorBalanceSummaryController).router()
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-customers',
|
||||
Container.get(TransactionsByCustomers).router(),
|
||||
Container.get(TransactionsByCustomers).router()
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-vendors',
|
||||
Container.get(TransactionsByVendors).router(),
|
||||
Container.get(TransactionsByVendors).router()
|
||||
);
|
||||
router.use(
|
||||
'/cash-flow',
|
||||
Container.get(CashFlowStatementController).router(),
|
||||
Container.get(CashFlowStatementController).router()
|
||||
);
|
||||
router.use(
|
||||
'/inventory-item-details',
|
||||
Container.get(InventoryDetailsController).router(),
|
||||
Container.get(InventoryDetailsController).router()
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-reference',
|
||||
Container.get(TransactionsByReferenceController).router(),
|
||||
Container.get(TransactionsByReferenceController).router()
|
||||
);
|
||||
router.use(
|
||||
'/cashflow-account-transactions',
|
||||
Container.get(CashflowAccountTransactions).router(),
|
||||
Container.get(CashflowAccountTransactions).router()
|
||||
);
|
||||
router.use(
|
||||
'/project-profitability-summary',
|
||||
Container.get(ProjectProfitabilityController).router(),
|
||||
)
|
||||
Container.get(ProjectProfitabilityController).router()
|
||||
);
|
||||
router.use(
|
||||
'/sales-tax-liability-summary',
|
||||
Container.get(SalesTaxLiabilitySummary).router()
|
||||
);
|
||||
return router;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import APAgingSummaryReportService from '@/services/FinancialStatements/AgingSummary/APAgingSummaryService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { APAgingSummaryApplication } from '@/services/FinancialStatements/AgingSummary/APAgingSummaryApplication';
|
||||
|
||||
export default class APAgingSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
APAgingSummaryService: APAgingSummaryReportService;
|
||||
private APAgingSummaryApp: APAgingSummaryApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -28,15 +29,19 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema() {
|
||||
private get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
|
||||
query('aging_days_before').default(30).isInt({ max: 500 }).toInt(),
|
||||
query('aging_periods').default(3).isInt({ max: 12 }).toInt(),
|
||||
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').isInt({ min: 1 }).toInt(),
|
||||
|
||||
query('none_zero').default(true).isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
@@ -46,22 +51,69 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* Retrieves payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async payableAgingSummary(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
private async payableAgingSummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF
|
||||
]);
|
||||
// Retrieves the json table format.
|
||||
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.APAgingSummaryApp.table(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const csv = await this.APAgingSummaryApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(csv);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.APAgingSummaryApp.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the pdf format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.APAgingSummaryApp.pdf(tenantId, filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const sheet = await this.APAgingSummaryApp.sheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -5,16 +5,18 @@ import ARAgingSummaryService from '@/services/FinancialStatements/AgingSummary/A
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ARAgingSummaryApplication } from '@/services/FinancialStatements/AgingSummary/ARAgingSummaryApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
|
||||
@Service()
|
||||
export default class ARAgingSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
ARAgingSummaryService: ARAgingSummaryService;
|
||||
private ARAgingSummaryApp: ARAgingSummaryApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -30,14 +32,14 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
|
||||
/**
|
||||
* AR aging summary validation roles.
|
||||
*/
|
||||
get validationSchema() {
|
||||
private get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
query('aging_days_before').optional().isInt({ max: 500 }).toInt(),
|
||||
query('aging_periods').optional().isInt({ max: 12 }).toInt(),
|
||||
query('aging_days_before').default(30).isInt({ max: 500 }).toInt(),
|
||||
query('aging_periods').default(3).isInt({ max: 12 }).toInt(),
|
||||
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').isInt({ min: 1 }).toInt(),
|
||||
@@ -52,21 +54,64 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
|
||||
|
||||
/**
|
||||
* Retrieve AR aging summary report.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async receivableAgingSummary(req: Request, res: Response) {
|
||||
const { tenantId, settings } = req;
|
||||
private async receivableAgingSummary(req: Request, res: Response) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
|
||||
const accept = this.accepts(req);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF
|
||||
]);
|
||||
// Retrieves the xlsx format.
|
||||
if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.ARAgingSummaryApp.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the table format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.ARAgingSummaryApp.table(tenantId, filter);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const buffer = await this.ARAgingSummaryApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves the pdf format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.ARAgingSummaryApp.pdf(tenantId, filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const sheet = await this.ARAgingSummaryApp.sheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -3,25 +3,21 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { castArray } from 'lodash';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BalanceSheetStatementService from '@/services/FinancialStatements/BalanceSheet/BalanceSheetService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import BalanceSheetTable from '@/services/FinancialStatements/BalanceSheet/BalanceSheetTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { BalanceSheetApplication } from '@/services/FinancialStatements/BalanceSheet/BalanceSheetApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
|
||||
@Service()
|
||||
export default class BalanceSheetStatementController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
balanceSheetService: BalanceSheetStatementService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private balanceSheetApp: BalanceSheetApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -38,10 +34,10 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get balanceSheetValidationSchema(): ValidationChain[] {
|
||||
private get balanceSheetValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('accounting_method').optional().isIn(['cash', 'accural']),
|
||||
query('accounting_method').optional().isIn(['cash', 'accrual']),
|
||||
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
@@ -84,10 +80,12 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async balanceSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
private async balanceSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
@@ -95,29 +93,55 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.balanceSheetService.balanceSheet(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
const table = new BalanceSheetTable(data, query, i18n);
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
// Retrieves the json table format.
|
||||
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE == acceptType) {
|
||||
const table = await this.balanceSheetApp.table(tenantId, filter);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res.status(200).send({
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
columns: table.tableColumns(),
|
||||
},
|
||||
query,
|
||||
meta,
|
||||
});
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send({ data, columns, query, meta });
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const buffer = await this.balanceSheetApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.balanceSheetApp.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the pdf format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.balanceSheetApp.pdf(tenantId, filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
} else {
|
||||
const sheet = await this.balanceSheetApp.sheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -8,29 +8,20 @@ import {
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import CashFlowStatementService from '@/services/FinancialStatements/CashFlow/CashFlowService';
|
||||
import {
|
||||
ICashFlowStatementDOO,
|
||||
ICashFlowStatement,
|
||||
AbilitySubject,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import CashFlowTable from '@/services/FinancialStatements/CashFlow/CashFlowTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { CashflowSheetApplication } from '@/services/FinancialStatements/CashFlow/CashflowSheetApplication';
|
||||
|
||||
@Service()
|
||||
export default class CashFlowController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
cashFlowService: CashFlowStatementService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private cashflowSheetApp: CashflowSheetApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -47,7 +38,7 @@ export default class CashFlowController extends BaseFinancialReportController {
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get cashflowValidationSchema(): ValidationChain[] {
|
||||
private get cashflowValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('from_date').optional(),
|
||||
@@ -67,41 +58,6 @@ export default class CashFlowController extends BaseFinancialReportController {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(cashFlowDOO: ICashFlowStatementDOO) {
|
||||
const { data, query, meta } = cashFlowDOO;
|
||||
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
cashFlowDOO: ICashFlowStatementDOO,
|
||||
tenantId: number
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const cashFlowTable = new CashFlowTable(cashFlowDOO, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: cashFlowTable.tableRows(),
|
||||
columns: cashFlowTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(cashFlowDOO.query),
|
||||
meta: this.transfromToResponse(cashFlowDOO.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* @param {Request} req
|
||||
@@ -109,26 +65,62 @@ export default class CashFlowController extends BaseFinancialReportController {
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async cashFlow(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
public async cashFlow(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const cashFlow = await this.cashFlowService.cashFlow(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(cashFlow, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send(this.transformJsonResponse(cashFlow));
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF
|
||||
]);
|
||||
// Retrieves the json table format.
|
||||
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.cashflowSheetApp.table(tenantId, filter);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const buffer = await this.cashflowSheetApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.status(200).send(buffer);
|
||||
// Retrieves the pdf format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.cashflowSheetApp.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the pdf format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.cashflowSheetApp.pdf(tenantId, filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const cashflow = await this.cashflowSheetApp.sheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send(cashflow);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ICustomerBalanceSummaryStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import CustomerBalanceSummary from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import CustomerBalanceSummaryTableRows from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { CustomerBalanceSummaryApplication } from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryApplication';
|
||||
|
||||
export default class CustomerBalanceSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
customerBalanceSummaryService: CustomerBalanceSummary;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private customerBalanceSummaryApp: CustomerBalanceSummaryApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -42,7 +34,7 @@ export default class CustomerBalanceSummaryReportController extends BaseFinancia
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
private get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
@@ -62,75 +54,81 @@ export default class CustomerBalanceSummaryReportController extends BaseFinancia
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the balance summary statement to table rows.
|
||||
* @param {ICustomerBalanceSummaryStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
tenantId,
|
||||
{ data, query }: ICustomerBalanceSummaryStatement
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const tableRows = new CustomerBalanceSummaryTableRows(data, query, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
columns: tableRows.tableColumns(),
|
||||
data: tableRows.tableRows(),
|
||||
},
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the balance summary statement to raw json.
|
||||
* @param {ICustomerBalanceSummaryStatement} customerBalance -
|
||||
*/
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
query,
|
||||
}: ICustomerBalanceSummaryStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async customerBalanceSummary(
|
||||
private async customerBalanceSummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, settings } = req;
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const customerBalanceSummary =
|
||||
await this.customerBalanceSummaryService.customerBalanceSummary(
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
|
||||
// Retrieves the xlsx format.
|
||||
if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.customerBalanceSummaryApp.xlsx(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const buffer = await this.customerBalanceSummaryApp.csv(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(tenantId, customerBalanceSummary));
|
||||
case 'application/json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToJsonResponse(customerBalanceSummary));
|
||||
return res.send(buffer);
|
||||
// Retrieves the json table format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.customerBalanceSummaryApp.table(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the pdf format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const buffer = await this.customerBalanceSummaryApp.pdf(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': buffer.length,
|
||||
});
|
||||
return res.send(buffer);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const sheet = await this.customerBalanceSummaryApp.sheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -2,20 +2,21 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import GeneralLedgerService from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { GeneralLedgerApplication } from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication';
|
||||
|
||||
@Service()
|
||||
export default class GeneralLedgerReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
generalLedgetService: GeneralLedgerService;
|
||||
private generalLedgerApplication: GeneralLedgerApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -31,7 +32,7 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
private get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
@@ -60,20 +61,56 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async generalLedger(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
private async generalLedger(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
const accept = this.accepts(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.generalLedgetService.generalLedger(tenantId, filter);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
// Retrieves the table format.
|
||||
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.generalLedgerApplication.table(tenantId, filter);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const buffer = await this.generalLedgerApplication.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.generalLedgerApplication.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the pdf format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.generalLedgerApplication.pdf(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
return res.send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const sheet = await this.generalLedgerApplication.sheet(tenantId, filter);
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,24 +8,20 @@ import {
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import InventoryDetailsService from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsService';
|
||||
import InventoryDetailsTable from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import { InventortyDetailsApplication } from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsApplication';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
|
||||
@Service()
|
||||
export default class InventoryDetailsController extends BaseController {
|
||||
@Inject()
|
||||
inventoryDetailsService: InventoryDetailsService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private inventoryItemDetailsApp: InventortyDetailsApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -45,7 +41,7 @@ export default class InventoryDetailsController extends BaseController {
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
private get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('number_format.precision')
|
||||
.optional()
|
||||
@@ -77,69 +73,76 @@ export default class InventoryDetailsController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(inventoryDetails) {
|
||||
const { data, query, meta } = inventoryDetails;
|
||||
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
*/
|
||||
private transformToTableRows(inventoryDetails, tenantId: number) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const inventoryDetailsTable = new InventoryDetailsTable(
|
||||
inventoryDetails,
|
||||
i18n
|
||||
);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: inventoryDetailsTable.tableData(),
|
||||
columns: inventoryDetailsTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(inventoryDetails.query),
|
||||
meta: this.transfromToResponse(inventoryDetails.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* Retrieve the inventory item details sheet.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async inventoryDetails(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
private async inventoryDetails(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const inventoryDetails =
|
||||
await this.inventoryDetailsService.inventoryDetails(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
// Retrieves the csv format.
|
||||
if (acceptType === ACCEPT_TYPE.APPLICATION_CSV) {
|
||||
const buffer = await this.inventoryItemDetailsApp.csv(tenantId, filter);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(inventoryDetails, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformJsonResponse(inventoryDetails));
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_XLSX) {
|
||||
const buffer = await this.inventoryItemDetailsApp.xlsx(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the json table format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_JSON_TABLE) {
|
||||
const table = await this.inventoryItemDetailsApp.table(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the pdf format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
|
||||
const buffer = await this.inventoryItemDetailsApp.pdf(tenantId, filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': buffer.length,
|
||||
});
|
||||
return res.send(buffer);
|
||||
} else {
|
||||
const sheet = await this.inventoryItemDetailsApp.sheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -3,14 +3,15 @@ import { query, ValidationChain } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import InventoryValuationService from '@/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { InventoryValuationSheetApplication } from '@/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
|
||||
@Service()
|
||||
export default class InventoryValuationReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
inventoryValuationService: InventoryValuationService;
|
||||
private inventoryValuationApp: InventoryValuationSheetApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -71,19 +72,55 @@ export default class InventoryValuationReportController extends BaseFinancialRep
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.inventoryValuationService.inventoryValuationSheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
const accept = this.accepts(req);
|
||||
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
|
||||
// Retrieves the json table format.
|
||||
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.inventoryValuationApp.table(tenantId, filter);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV == acceptType) {
|
||||
const buffer = await this.inventoryValuationApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves the xslx buffer format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.inventoryValuationApp.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the pdf format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.inventoryValuationApp.pdf(tenantId, filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
return res.status(200).send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const { data, query, meta } = await this.inventoryValuationApp.sheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({ meta, data, query });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,15 @@ import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { castArray } from 'lodash';
|
||||
import { query, oneOf } from 'express-validator';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import JournalSheetService from '@/services/FinancialStatements/JournalSheet/JournalSheetService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { JournalSheetApplication } from '@/services/FinancialStatements/JournalSheet/JournalSheetApplication';
|
||||
|
||||
@Service()
|
||||
export default class JournalSheetController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
journalService: JournalSheetService;
|
||||
private journalSheetApp: JournalSheetApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -57,28 +58,58 @@ export default class JournalSheetController extends BaseFinancialReportControlle
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async journal(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
private async journal(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
|
||||
try {
|
||||
const { data, query, meta } = await this.journalService.journalSheet(
|
||||
tenantId,
|
||||
filter
|
||||
// Retrieves the json table format.
|
||||
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.journalSheetApp.table(tenantId, filter);
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const buffer = await this.journalSheetApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.journalSheetApp.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the json format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.journalSheetApp.pdf(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
res.send(pdfContent);
|
||||
} else {
|
||||
const sheet = await this.journalSheetApp.sheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import ProfitLossSheetService from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import { ProfitLossSheetTable } from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { ProfitLossSheetApplication } from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetApplication';
|
||||
@Service()
|
||||
export default class ProfitLossSheetController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
profitLossSheetService: ProfitLossSheetService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private profitLossSheetApp: ProfitLossSheetApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -34,7 +30,7 @@ export default class ProfitLossSheetController extends BaseFinancialReportContro
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
private get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('basis').optional(),
|
||||
@@ -85,37 +81,63 @@ export default class ProfitLossSheetController extends BaseFinancialReportContro
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async profitLossSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
private async profitLossSheet(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.profitLossSheetService.profitLossSheet(tenantId, filter);
|
||||
// Retrieves the csv format.
|
||||
if (acceptType === ACCEPT_TYPE.APPLICATION_CSV) {
|
||||
const sheet = await this.profitLossSheetApp.csv(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
const table = new ProfitLossSheetTable(data, query, i18n);
|
||||
return res.send(sheet);
|
||||
// Retrieves the json table format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_JSON_TABLE) {
|
||||
const table = await this.profitLossSheetApp.table(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
columns: table.tableColumns(),
|
||||
},
|
||||
query,
|
||||
meta,
|
||||
});
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send({
|
||||
data,
|
||||
query,
|
||||
meta,
|
||||
});
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_XLSX) {
|
||||
const sheet = await this.profitLossSheetApp.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(sheet);
|
||||
// Retrieves the json format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
|
||||
const pdfContent = await this.profitLossSheetApp.pdf(tenantId, filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
} else {
|
||||
const sheet = await this.profitLossSheetApp.sheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import PurchasesByItemsService from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
|
||||
import { PurchasesByItemsService } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { PurcahsesByItemsApplication } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsApplication';
|
||||
|
||||
@Service()
|
||||
export default class PurchasesByItemReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
purchasesByItemsService: PurchasesByItemsService;
|
||||
private purchasesByItemsApp: PurcahsesByItemsApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -63,20 +64,56 @@ export default class PurchasesByItemReportController extends BaseFinancialReport
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
|
||||
public async purchasesByItems(req: Request, res: Response) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.purchasesByItemsService.purchasesByItems(tenantId, filter);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
const accept = this.accepts(req);
|
||||
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
// JSON table response format.
|
||||
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.purchasesByItemsApp.table(tenantId, filter);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// CSV response format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const buffer = await this.purchasesByItemsApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Xlsx response format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.purchasesByItemsApp.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// PDF response format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.purchasesByItemsApp.pdf(tenantId, filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
return res.send(pdfContent);
|
||||
// Json response format.
|
||||
} else {
|
||||
const sheet = await this.purchasesByItemsApp.sheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { query, ValidationChain, ValidationSchema } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import SalesByItemsReportService from '@/services/FinancialStatements/SalesByItems/SalesByItemsService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { SalesByItemsApplication } from '@/services/FinancialStatements/SalesByItems/SalesByItemsApplication';
|
||||
|
||||
@Service()
|
||||
export default class SalesByItemsReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
salesByItemsService: SalesByItemsReportService;
|
||||
private salesByItemsApp: SalesByItemsApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_SALES_BY_ITEMS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
CheckPolicies(ReportsAction.READ_SALES_BY_ITEMS, AbilitySubject.Report),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.purchasesByItems.bind(this))
|
||||
asyncMiddleware(this.salesByItems.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
private get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
@@ -63,22 +61,53 @@ export default class SalesByItemsReportController extends BaseFinancialReportCon
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
|
||||
private async salesByItems(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
const accept = this.accepts(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } = await this.salesByItemsService.salesByItems(
|
||||
tenantId,
|
||||
filter
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
// Retrieves the csv format.
|
||||
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const buffer = await this.salesByItemsApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves the json table format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.salesByItemsApp.table(tenantId, filter);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = this.salesByItemsApp.xlsx(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
return res.send(buffer);
|
||||
// Retrieves the json format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.salesByItemsApp.pdf(tenantId, filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
return res.send(pdfContent);
|
||||
} else {
|
||||
const sheet = await this.salesByItemsApp.sheet(tenantId, filter);
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { SalesTaxLiabilitySummaryApplication } from '@/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
|
||||
export default class SalesTaxLiabilitySummary extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
private salesTaxLiabilitySummaryApp: SalesTaxLiabilitySummaryApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_SALES_TAX_LIABILITY_SUMMARY,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
asyncMiddleware(this.salesTaxLiabilitySummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
private get validationSchema() {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* Retrieves the sales tax liability summary.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private async salesTaxLiabilitySummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
|
||||
// Retrieves the json table format.
|
||||
if (acceptType === ACCEPT_TYPE.APPLICATION_JSON_TABLE) {
|
||||
const table = await this.salesTaxLiabilitySummaryApp.table(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_XLSX) {
|
||||
const buffer = await this.salesTaxLiabilitySummaryApp.xlsx(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the csv format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_CSV) {
|
||||
const buffer = await this.salesTaxLiabilitySummaryApp.csv(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves the json format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
|
||||
const pdfContent = await this.salesTaxLiabilitySummaryApp.pdf(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.status(200).send(pdfContent);
|
||||
} else {
|
||||
const sheet = await this.salesTaxLiabilitySummaryApp.sheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,22 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ITransactionsByCustomersStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import TransactionsByCustomersService from '@/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService';
|
||||
import TransactionsByCustomersTableRows from '@/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { TransactionsByCustomerApplication } from '@/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
|
||||
@Service()
|
||||
export default class TransactionsByCustomersReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
transactionsByCustomersService: TransactionsByCustomersService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private transactionsByCustomersApp: TransactionsByCustomerApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -58,45 +50,13 @@ export default class TransactionsByCustomersReportController extends BaseFinanci
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the statement to table rows response.
|
||||
* @param {ITransactionsByCustomersStatement} statement -
|
||||
*/
|
||||
private transformToTableResponse(customersTransactions, tenantId) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const table = new TransactionsByCustomersTableRows(
|
||||
customersTransactions,
|
||||
i18n
|
||||
);
|
||||
return {
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the statement to json response.
|
||||
* @param {ITransactionsByCustomersStatement} statement -
|
||||
*/
|
||||
private transfromToJsonResponse(
|
||||
data,
|
||||
columns
|
||||
): ITransactionsByCustomersStatement {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async transactionsByCustomers(
|
||||
private async transactionsByCustomers(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
@@ -104,25 +64,62 @@ export default class TransactionsByCustomersReportController extends BaseFinanci
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
try {
|
||||
const report =
|
||||
await this.transactionsByCustomersService.transactionsByCustomers(
|
||||
// Retrieves the json table format.
|
||||
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.transactionsByCustomersApp.table(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
return res.status(200).send(table);
|
||||
// Retrieve the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const csv = await this.transactionsByCustomersApp.csv(tenantId, filter);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'json':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transfromToJsonResponse(report.data, report.columns));
|
||||
case 'application/json+table':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableResponse(report.data, tenantId));
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(csv);
|
||||
// Retrieve the xlsx format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.transactionsByCustomersApp.xlsx(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieve the json format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.transactionsByCustomersApp.pdf(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
} else {
|
||||
const sheet = await this.transactionsByCustomersApp.sheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -3,27 +3,19 @@ import { query, ValidationChain } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import TransactionsByVendorsTableRows from '@/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows';
|
||||
import TransactionsByVendorsService from '@/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ITransactionsByVendorsStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { TransactionsByVendorApplication } from '@/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorApplication';
|
||||
|
||||
export default class TransactionsByVendorsReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
transactionsByVendorsService: TransactionsByVendorsService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private transactionsByVendorsApp: TransactionsByVendorApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -42,7 +34,7 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
private get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
@@ -58,64 +50,76 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(tenantId: number, transactions: any[]) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const table = new TransactionsByVendorsTableRows(transactions, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: table.tableRows(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to json response.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
query,
|
||||
}: ITransactionsByVendorsStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async transactionsByVendors(req: Request, res: Response, next: NextFunction) {
|
||||
private async transactionsByVendors(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const report =
|
||||
await this.transactionsByVendorsService.transactionsByVendors(
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
|
||||
// Retrieves the xlsx format.
|
||||
if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||
const buffer = await this.transactionsByVendorsApp.xlsx(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(tenantId, report.data));
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send(this.transformToJsonResponse(report));
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats');
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=report.xlsx'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves the csv format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
const buffer = await this.transactionsByVendorsApp.csv(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=report.csv');
|
||||
return res.send(buffer);
|
||||
// Retrieves the json table format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||
const table = await this.transactionsByVendorsApp.table(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the pdf format.
|
||||
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.transactionsByVendorsApp.pdf(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const sheet = await this.transactionsByVendorsApp.sheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -3,20 +3,21 @@ import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { castArray } from 'lodash';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import TrialBalanceSheetService from '@/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { TrialBalanceSheetApplication } from '@/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
|
||||
@Service()
|
||||
export default class TrialBalanceSheetController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
trialBalanceSheetService: TrialBalanceSheetService;
|
||||
private trialBalanceSheetApp: TrialBalanceSheetApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -36,7 +37,7 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
|
||||
* Validation schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get trialBalanceSheetValidationSchema(): ValidationChain[] {
|
||||
private get trialBalanceSheetValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('basis').optional(),
|
||||
@@ -59,28 +60,74 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
|
||||
/**
|
||||
* Retrieve the trial balance sheet.
|
||||
*/
|
||||
public async trialBalanceSheet(
|
||||
private async trialBalanceSheet(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, settings } = req;
|
||||
const { tenantId } = req;
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.trialBalanceSheetService.trialBalanceSheet(tenantId, filter);
|
||||
const accept = this.accepts(req);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
// Retrieves in json table format.
|
||||
if (acceptType === ACCEPT_TYPE.APPLICATION_JSON_TABLE) {
|
||||
const { table, meta, query } = await this.trialBalanceSheetApp.table(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({ table, meta, query });
|
||||
// Retrieves in xlsx format
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_XLSX) {
|
||||
const buffer = await this.trialBalanceSheetApp.xlsx(tenantId, filter);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
return res.send(buffer);
|
||||
// Retrieves in csv format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_CSV) {
|
||||
const buffer = await this.trialBalanceSheetApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves in pdf format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
|
||||
const pdfContent = await this.trialBalanceSheetApp.pdf(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
// Retrieves in json format.
|
||||
} else {
|
||||
const { data, query, meta } = await this.trialBalanceSheetApp.sheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({ data, query, meta });
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -3,27 +3,19 @@ import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import VendorBalanceSummaryTableRows from '@/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableRows';
|
||||
import VendorBalanceSummaryService from '@/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService';
|
||||
import {
|
||||
AbilitySubject,
|
||||
IVendorBalanceSummaryStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { VendorBalanceSummaryApplication } from '@/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryApplication';
|
||||
|
||||
export default class VendorBalanceSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
vendorBalanceSummaryService: VendorBalanceSummaryService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private vendorBalanceSummaryApp: VendorBalanceSummaryApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -41,7 +33,7 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
private get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('as_date').optional().isISO8601(),
|
||||
@@ -59,73 +51,74 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {IVendorBalanceSummaryStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
tenantId: number,
|
||||
{ data, query }: IVendorBalanceSummaryStatement
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const tableData = new VendorBalanceSummaryTableRows(
|
||||
data,
|
||||
query,
|
||||
i18n
|
||||
);
|
||||
return {
|
||||
table: {
|
||||
columns: tableData.tableColumns(),
|
||||
data: tableData.tableRows(),
|
||||
},
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to raw json.
|
||||
* @param {IVendorBalanceSummaryStatement} statement -
|
||||
*/
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
}: IVendorBalanceSummaryStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve vendors balance summary.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async vendorBalanceSummary(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
public async vendorBalanceSummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const vendorBalanceSummary =
|
||||
await this.vendorBalanceSummaryService.vendorBalanceSummary(
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types([
|
||||
ACCEPT_TYPE.APPLICATION_JSON,
|
||||
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
|
||||
// Retrieves the csv format.
|
||||
if (acceptType === ACCEPT_TYPE.APPLICATION_CSV) {
|
||||
const buffer = await this.vendorBalanceSummaryApp.csv(tenantId, filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_XLSX) {
|
||||
const buffer = await this.vendorBalanceSummaryApp.xlsx(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(tenantId, vendorBalanceSummary));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToJsonResponse(vendorBalanceSummary));
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename=output.xlsx'
|
||||
);
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats');
|
||||
return res.send(buffer);
|
||||
// Retrieves the json table format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_JSON_TABLE) {
|
||||
const table = await this.vendorBalanceSummaryApp.table(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the pdf format.
|
||||
} else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
|
||||
const pdfContent = await this.vendorBalanceSummaryApp.pdf(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const sheet = await this.vendorBalanceSummaryApp.sheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
246
packages/server/src/api/controllers/Import/ImportController.ts
Normal file
246
packages/server/src/api/controllers/Import/ImportController.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import { defaultTo } from 'lodash';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
|
||||
import { uploadImportFile } from './_utils';
|
||||
import { parseJsonSafe } from '@/utils/parse-json-safe';
|
||||
|
||||
@Service()
|
||||
export class ImportController extends BaseController {
|
||||
@Inject()
|
||||
private importResourceApp: ImportResourceApplication;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/file',
|
||||
uploadImportFile.single('file'),
|
||||
this.importValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.fileUpload.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:import_id/import',
|
||||
this.asyncMiddleware(this.import.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:import_id/mapping',
|
||||
[
|
||||
param('import_id').exists().isString(),
|
||||
body('mapping').exists().isArray({ min: 1 }),
|
||||
body('mapping.*.group').optional(),
|
||||
body('mapping.*.from').exists(),
|
||||
body('mapping.*.to').exists(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.mapping.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/sample',
|
||||
[query('resource').exists(), query('format').optional()],
|
||||
this.validationResult,
|
||||
this.downloadImportSample.bind(this),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:import_id',
|
||||
this.asyncMiddleware(this.getImportFileMeta.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:import_id/preview',
|
||||
this.asyncMiddleware(this.preview.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import validation schema.
|
||||
* @returns {ValidationSchema[]}
|
||||
*/
|
||||
private get importValidationSchema() {
|
||||
return [body('resource').exists(), body('params').optional()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports xlsx/csv to the given resource type.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private async fileUpload(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const body = this.matchedBodyData(req);
|
||||
const params = defaultTo(parseJsonSafe(body.params), {});
|
||||
|
||||
try {
|
||||
const data = await this.importResourceApp.import(
|
||||
tenantId,
|
||||
body.resource,
|
||||
req.file.filename,
|
||||
params
|
||||
);
|
||||
return res.status(200).send(data);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the columns of the imported file.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async mapping(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { import_id: importId } = req.params;
|
||||
const body = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const mapping = await this.importResourceApp.mapping(
|
||||
tenantId,
|
||||
importId,
|
||||
body?.mapping
|
||||
);
|
||||
return res.status(200).send(mapping);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the imported file before actual importing.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async preview(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { import_id: importId } = req.params;
|
||||
|
||||
try {
|
||||
const preview = await this.importResourceApp.preview(tenantId, importId);
|
||||
|
||||
return res.status(200).send(preview);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Importing the imported file to the application storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async import(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { import_id: importId } = req.params;
|
||||
|
||||
try {
|
||||
const result = await this.importResourceApp.process(tenantId, importId);
|
||||
|
||||
return res.status(200).send(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the csv/xlsx sample sheet of the given resource name.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async downloadImportSample(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { format, resource } = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const result = this.importResourceApp.sample(tenantId, resource, format);
|
||||
|
||||
return res.status(200).send(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the import file meta.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async getImportFileMeta(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { import_id: importId } = req.params;
|
||||
|
||||
try {
|
||||
const result = await this.importResourceApp.importMeta(
|
||||
tenantId,
|
||||
importId
|
||||
);
|
||||
return res.status(200).send(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms service errors to response.
|
||||
* @param {Error}
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'INVALID_MAP_ATTRS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVALID_MAP_ATTRS' }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'DUPLICATED_FROM_MAP_ATTR') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DUPLICATED_FROM_MAP_ATTR' }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'DUPLICATED_TO_MAP_ATTR') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DUPLICATED_TO_MAP_ATTR' }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'IMPORTED_FILE_EXTENSION_INVALID') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'IMPORTED_FILE_EXTENSION_INVALID' }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
34
packages/server/src/api/controllers/Import/_utils.ts
Normal file
34
packages/server/src/api/controllers/Import/_utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Multer from 'multer';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { getImportsStoragePath } from '@/services/Import/_utils';
|
||||
|
||||
export function allowSheetExtensions(req, file, cb) {
|
||||
if (
|
||||
file.mimetype !== 'text/csv' &&
|
||||
file.mimetype !== 'application/vnd.ms-excel' &&
|
||||
file.mimetype !==
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
) {
|
||||
cb(new ServiceError('IMPORTED_FILE_EXTENSION_INVALID'));
|
||||
return;
|
||||
}
|
||||
cb(null, true);
|
||||
}
|
||||
|
||||
const storage = Multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
const path = getImportsStoragePath();
|
||||
cb(null, path);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
// Add the creation timestamp to clean up temp files later.
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
cb(null, uniqueSuffix);
|
||||
},
|
||||
});
|
||||
|
||||
export const uploadImportFile = Multer({
|
||||
storage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
fileFilter: allowSheetExtensions,
|
||||
});
|
||||
@@ -92,6 +92,7 @@ export default class InviteUsersController extends BaseController {
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.sendInvite(tenantId, sendInviteDTO, user);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'INVITE.SENT.SUCCESSFULLY',
|
||||
|
||||
@@ -149,6 +149,11 @@ export default class ItemsController extends BaseController {
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('sell_tax_rate_id').optional({ nullable: true }).isInt().toInt(),
|
||||
check('purchase_tax_rate_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt()
|
||||
.toInt(),
|
||||
check('category_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
@@ -177,7 +182,7 @@ export default class ItemsController extends BaseController {
|
||||
/**
|
||||
* Validate list query schema.
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
private get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
@@ -193,32 +198,20 @@ export default class ItemsController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate autocomplete list query schema.
|
||||
*/
|
||||
get autocompleteQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('limit').optional().isNumeric().toInt(),
|
||||
|
||||
query('keyword').optional().isString().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given item details to the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async newItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async newItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemDTO: IItemDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const storedItem = await this.itemsApplication.createItem(tenantId, itemDTO);
|
||||
const storedItem = await this.itemsApplication.createItem(
|
||||
tenantId,
|
||||
itemDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: storedItem.id,
|
||||
@@ -234,7 +227,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async editItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
const item: IItemDTO = this.matchedBodyData(req);
|
||||
@@ -257,7 +250,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async activateItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async activateItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
|
||||
@@ -279,7 +272,11 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async inactivateItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async inactivateItem(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
|
||||
@@ -300,7 +297,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deleteItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async deleteItem(req: Request, res: Response, next: NextFunction) {
|
||||
const itemId: number = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
@@ -322,7 +319,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async getItem(req: Request, res: Response, next: NextFunction) {
|
||||
const itemId: number = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
@@ -342,7 +339,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getItemsList(req: Request, res: Response, next: NextFunction) {
|
||||
private async getItemsList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const filter = {
|
||||
@@ -516,6 +513,28 @@ export default class ItemsController extends BaseController {
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PURCHASE_TAX_RATE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'PURCHASE_TAX_RATE_NOT_FOUND',
|
||||
message: 'Purchase tax rate has not found.',
|
||||
code: 410,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_TAX_RATE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'SELL_TAX_RATE_NOT_FOUND',
|
||||
message: 'Sell tax rate is not found.',
|
||||
code: 420,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import ItemTransactionsController from './ItemsTransactions';
|
||||
|
||||
@Service()
|
||||
export default class ItemsBaseController {
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/', Container.get(ItemsController).router());
|
||||
|
||||
@@ -77,14 +77,14 @@ export default class ManualJournalsController extends BaseController {
|
||||
/**
|
||||
* Specific manual journal id param validation schema.
|
||||
*/
|
||||
get manualJournalParamSchema() {
|
||||
private get manualJournalParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual journal DTO schema.
|
||||
*/
|
||||
get manualJournalValidationSchema() {
|
||||
private get manualJournalValidationSchema() {
|
||||
return [
|
||||
check('date').exists().isISO8601(),
|
||||
check('currency_code').optional(),
|
||||
@@ -148,13 +148,16 @@ export default class ManualJournalsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual journals list validation schema.
|
||||
*/
|
||||
get manualJournalsListSchema() {
|
||||
private get manualJournalsListSchema() {
|
||||
return [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
@@ -320,7 +323,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
getManualJournalsList = async (
|
||||
private getManualJournalsList = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
@@ -387,7 +390,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
errors: [{ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'acccounts_ids_not_found') {
|
||||
if (error.errorType === 'accounts_ids_not_found') {
|
||||
return res.boom.badRequest(
|
||||
'Journal entries some of accounts ids not exists.',
|
||||
{ errors: [{ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 400 }] }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { check, ValidationChain } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import OrganizationService from '@/services/Organization/OrganizationService';
|
||||
import { MONTHS, ACCEPTED_LOCALES } from '@/services/Organization/constants';
|
||||
@@ -17,7 +18,7 @@ import BaseController from '@/api/controllers/BaseController';
|
||||
@Service()
|
||||
export default class OrganizationController extends BaseController {
|
||||
@Inject()
|
||||
private organizationService: OrganizationService;
|
||||
organizationService: OrganizationService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -25,20 +26,23 @@ export default class OrganizationController extends BaseController {
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
// Should before build tenant database the user be authorized and
|
||||
// most important than that, should be subscribed to any plan.
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.use('/build', SubscriptionMiddleware('main'));
|
||||
router.post(
|
||||
'/build',
|
||||
this.organizationValidationSchema,
|
||||
this.buildOrganizationValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.build.bind(this)),
|
||||
this.handleServiceErrors.bind(this)
|
||||
);
|
||||
router.put(
|
||||
'/',
|
||||
this.organizationValidationSchema,
|
||||
this.updateOrganizationValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.updateOrganization.bind(this)),
|
||||
this.handleServiceErrors.bind(this)
|
||||
@@ -55,10 +59,10 @@ export default class OrganizationController extends BaseController {
|
||||
* Organization setup schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
private get organizationValidationSchema(): ValidationChain[] {
|
||||
private get commonOrganizationValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('name').exists().trim(),
|
||||
check('industry').optional().isString(),
|
||||
check('industry').optional({ nullable: true }).isString().trim().escape(),
|
||||
check('location').exists().isString().isISO31661Alpha2(),
|
||||
check('base_currency').exists().isISO4217(),
|
||||
check('timezone').exists().isIn(moment.tz.names()),
|
||||
@@ -68,6 +72,29 @@ export default class OrganizationController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build organization validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
private get buildOrganizationValidationSchema(): ValidationChain[] {
|
||||
return [...this.commonOrganizationValidationSchema];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update organization validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
private get updateOrganizationValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.commonOrganizationValidationSchema,
|
||||
check('tax_number')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds tenant database and migrate database schema.
|
||||
* @param {Request} req - Express request.
|
||||
|
||||
@@ -33,17 +33,17 @@ export default class OrganizationDashboardController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
* Detarmines whether the current authed organization to able to change its currency/.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response|void}
|
||||
*/
|
||||
private async baseCurrencyMutateAbility(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: Function
|
||||
) {
|
||||
): Promise<Response|void> {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
|
||||
@@ -29,8 +29,7 @@ export class ProjectsController extends BaseController {
|
||||
check('cost_estimate').exists().isDecimal(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.createProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
asyncMiddleware(this.createProject.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
@@ -43,8 +42,7 @@ export class ProjectsController extends BaseController {
|
||||
check('cost_estimate').exists().isDecimal(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
asyncMiddleware(this.editProject.bind(this))
|
||||
);
|
||||
router.patch(
|
||||
'/:projectId/status',
|
||||
@@ -56,16 +54,14 @@ export class ProjectsController extends BaseController {
|
||||
.isIn([IProjectStatus.InProgress, IProjectStatus.Closed]),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
asyncMiddleware(this.editProject.bind(this))
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
asyncMiddleware(this.getProject.bind(this))
|
||||
);
|
||||
router.get(
|
||||
'/:projectId/billable/entries',
|
||||
@@ -76,24 +72,21 @@ export class ProjectsController extends BaseController {
|
||||
query('to_date').optional().isISO8601(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.projectBillableEntries.bind(this)),
|
||||
this.catchServiceErrors
|
||||
asyncMiddleware(this.projectBillableEntries.bind(this))
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project),
|
||||
[],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getProjects.bind(this)),
|
||||
this.catchServiceErrors
|
||||
asyncMiddleware(this.getProjects.bind(this))
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(ProjectAction.DELETE, AbilitySubject.Project),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteProject.bind(this)),
|
||||
this.catchServiceErrors
|
||||
asyncMiddleware(this.deleteProject.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
@@ -252,22 +245,4 @@ export class ProjectsController extends BaseController {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms service errors to response.
|
||||
* @param {Error}
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { AbilitySubject, BillAction, IBillDTO, IBillEditDTO } from '@/interfaces';
|
||||
import {
|
||||
AbilitySubject,
|
||||
BillAction,
|
||||
IBillDTO,
|
||||
IBillEditDTO,
|
||||
} from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BillsService from '@/services/Purchases/Bills';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import BillPaymentsService from '@/services/Purchases/BillPaymentsService';
|
||||
import { BillsApplication } from '@/services/Purchases/Bills/BillsApplication';
|
||||
|
||||
@Service()
|
||||
export default class BillsController extends BaseController {
|
||||
@Inject()
|
||||
private billsService: BillsService;
|
||||
private billsApplication: BillsApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
private billPayments: BillPaymentsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
@@ -97,7 +98,7 @@ export default class BillsController extends BaseController {
|
||||
/**
|
||||
* Common validation schema.
|
||||
*/
|
||||
get billValidationSchema() {
|
||||
private get billValidationSchema() {
|
||||
return [
|
||||
check('bill_number').exists().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
@@ -114,12 +115,13 @@ export default class BillsController extends BaseController {
|
||||
check('note').optional().trim().escape(),
|
||||
check('open').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
check('is_inclusive_tax').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toInt(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
@@ -136,13 +138,25 @@ export default class BillsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.tax_code')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isString(),
|
||||
check('entries.*.tax_rate_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Common validation schema.
|
||||
*/
|
||||
get billEditValidationSchema() {
|
||||
private get billEditValidationSchema() {
|
||||
return [
|
||||
check('bill_number').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
@@ -178,20 +192,23 @@ export default class BillsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill validation schema.
|
||||
*/
|
||||
get specificBillValidationSchema() {
|
||||
private get specificBillValidationSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bills list validation schema.
|
||||
*/
|
||||
get billsListingValidationSchema() {
|
||||
private get billsListingValidationSchema() {
|
||||
return [
|
||||
query('view_slug').optional().isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
@@ -203,7 +220,7 @@ export default class BillsController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
get dueBillsListingValidationSchema() {
|
||||
private get dueBillsListingValidationSchema() {
|
||||
return [
|
||||
query('vendor_id').optional().trim().escape(),
|
||||
query('payment_made_id').optional().trim().escape(),
|
||||
@@ -216,17 +233,16 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async newBill(req: Request, res: Response, next: NextFunction) {
|
||||
private async newBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const billDTO: IBillDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const storedBill = await this.billsService.createBill(
|
||||
const storedBill = await this.billsApplication.createBill(
|
||||
tenantId,
|
||||
billDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: storedBill.id,
|
||||
message: 'The bill has been created successfully.',
|
||||
@@ -241,13 +257,13 @@ export default class BillsController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editBill(req: Request, res: Response, next: NextFunction) {
|
||||
private async editBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: billId } = req.params;
|
||||
const { tenantId, user } = req;
|
||||
const billDTO: IBillEditDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.billsService.editBill(tenantId, billId, billDTO, user);
|
||||
await this.billsApplication.editBill(tenantId, billId, billDTO, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: billId,
|
||||
@@ -263,12 +279,12 @@ export default class BillsController extends BaseController {
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async openBill(req: Request, res: Response, next: NextFunction) {
|
||||
private async openBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: billId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.billsService.openBill(tenantId, billId);
|
||||
await this.billsApplication.openBill(tenantId, billId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: billId,
|
||||
@@ -285,14 +301,14 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getBill(req: Request, res: Response, next: NextFunction) {
|
||||
private async getBill(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: billId } = req.params;
|
||||
|
||||
try {
|
||||
const bill = await this.billsService.getBill(tenantId, billId);
|
||||
const bill = await this.billsApplication.getBill(tenantId, billId);
|
||||
|
||||
return res.status(200).send(this.transfromToResponse({ bill }));
|
||||
return res.status(200).send({ bill });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@@ -304,12 +320,12 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteBill(req: Request, res: Response, next: NextFunction) {
|
||||
private async deleteBill(req: Request, res: Response, next: NextFunction) {
|
||||
const billId = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.billsService.deleteBill(tenantId, billId);
|
||||
await this.billsApplication.deleteBill(tenantId, billId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: billId,
|
||||
@@ -326,7 +342,7 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
public async billsList(req: Request, res: Response, next: NextFunction) {
|
||||
private async billsList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
page: 1,
|
||||
@@ -337,14 +353,11 @@ export default class BillsController extends BaseController {
|
||||
};
|
||||
|
||||
try {
|
||||
const { bills, pagination, filterMeta } =
|
||||
await this.billsService.getBills(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
bills: this.transfromToResponse(bills),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
const billsWithPagination = await this.billsApplication.getBills(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(billsWithPagination);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@@ -356,12 +369,13 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public async getDueBills(req: Request, res: Response, next: NextFunction) {
|
||||
private async getDueBills(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { vendorId } = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const bills = await this.billsService.getDueBills(tenantId, vendorId);
|
||||
const bills = await this.billsApplication.getDueBills(tenantId, vendorId);
|
||||
|
||||
return res.status(200).send({ bills });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -374,7 +388,7 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getBillPaymentsTransactions = async (
|
||||
private getBillPaymentsTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
@@ -383,7 +397,7 @@ export default class BillsController extends BaseController {
|
||||
const { id: billId } = req.params;
|
||||
|
||||
try {
|
||||
const billPayments = await this.billPayments.getBillPayments(
|
||||
const billPayments = await this.billsApplication.getBillPayments(
|
||||
tenantId,
|
||||
billId
|
||||
);
|
||||
@@ -541,6 +555,26 @@ export default class BillsController extends BaseController {
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', code: 1800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND', code: 1900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT',
|
||||
code: 2000,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user