mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-31 15:42:03 -04:00
Compare commits
734 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0a6c7e44dc | ||
|
53d92a67a9 | ||
|
752324cced | ||
|
92ba61be51 | ||
|
6afd7a8970 | ||
|
1fd186e1ec | ||
|
23ad59967e | ||
|
eebf72d042 | ||
|
9977f2c47e | ||
|
5731763071 | ||
|
4daa321ecf | ||
|
79ef8aea18 | ||
|
2aba873d16 | ||
|
6f644f3070 | ||
|
8dffba42d6 | ||
|
a675dd685a | ||
|
19cf39786e | ||
|
ba6a63c63e | ||
|
2d316d439d | ||
|
196d785f19 | ||
|
984a8666aa | ||
|
ee637b2555 | ||
|
20b04a266f | ||
|
114b58cdc4 | ||
|
b1d92d8354 | ||
|
715a9f34a6 | ||
|
abe8bbcd5d | ||
|
b1f1eb0ccd | ||
|
c9db1688fd | ||
|
1a438ecd64 | ||
|
56cbc3f58b | ||
|
6a7dd0351b | ||
|
cf28dffab8 | ||
|
0c79897987 | ||
|
77b5e220c6 | ||
|
c94fc8a6da | ||
|
ab9b96d7f2 | ||
|
15ed82e5b2 | ||
|
818e47adff | ||
|
a9e3533647 | ||
|
a2f371e814 | ||
|
6d67333431 | ||
|
36b5a595f1 | ||
|
cdacdc851e | ||
|
40ebad7dca | ||
|
209d335df5 | ||
|
f8698c0270 | ||
|
db7c21e761 | ||
|
ff695cdad0 | ||
|
3f0f5663a9 | ||
|
0c61555739 | ||
|
9c6568aae1 | ||
|
bfbf7f5c6a | ||
|
737e18aa42 | ||
|
50faf3261b | ||
|
fb46889bb4 | ||
|
6f25be937d | ||
|
2221353622 | ||
|
5c0f228cb7 | ||
|
10f6977023 | ||
|
3aa17bd6cd | ||
|
9ced189262 | ||
|
812bc91947 | ||
|
c77da63b62 | ||
|
37f28d8a31 | ||
|
506f7b5d7a | ||
|
02d93356aa | ||
|
c812be495c | ||
|
d93e3a77b3 | ||
|
50f0f7d3ec | ||
|
7892c6554d | ||
|
6c83ccc1dd | ||
|
fc3ca1c124 | ||
|
f11f204792 | ||
|
730d55a31e | ||
|
3703ec1b36 | ||
|
0c22f9b27a | ||
|
a55155f1fb | ||
|
c87f25cc72 | ||
|
ca187bb1da | ||
|
2a890fe6f8 | ||
|
eea3f396c6 | ||
|
ec5b4243a2 | ||
|
3d67fe327b | ||
|
e08fcbaf07 | ||
|
305e3daeb4 | ||
|
4d72a312d6 | ||
|
953a8b632b | ||
|
69c166c3e9 | ||
|
0c8e5b2e37 | ||
|
53e295df4c | ||
|
4f46fa1029 | ||
|
b715c7dd20 | ||
|
6177483791 | ||
|
1172e4bfc1 | ||
|
d2d807dd7d | ||
|
3689931624 | ||
|
0e070ed5c1 | ||
|
b568f09da2 | ||
|
0d495041e7 | ||
|
c4f11fcc7f | ||
|
54afae4d8b | ||
|
2e114692e7 | ||
|
2c6c7cc3b8 | ||
|
ae73f0aa0d | ||
|
03bb4877e2 | ||
|
c8af154288 | ||
|
c1fc4ad358 | ||
|
590616a780 | ||
|
1f3c37a76b | ||
|
b7de04de0d | ||
|
c27d8acc9a | ||
|
7142f1e51a | ||
|
db8bafac3c | ||
|
a9aa6aea87 | ||
|
e721351a4b | ||
|
c0483d727f | ||
|
78367c1e2d | ||
|
a5317ff093 | ||
|
587159c8c0 | ||
|
1c855b42d5 | ||
|
3c226a524c | ||
|
9053e6ac7e | ||
|
6d2638c603 | ||
|
67407103c7 | ||
|
734878a73d | ||
|
6876a647ca | ||
|
454f7df01e | ||
|
54c1831228 | ||
|
30ff490977 | ||
|
3de9c99d34 | ||
|
9c8afcd434 | ||
|
6a5b559983 | ||
|
43b2bef464 | ||
|
0017307e39 | ||
|
7444355dee | ||
|
1d45a3944b | ||
|
3a609f19b1 | ||
|
b4ee01cc60 | ||
|
ee399ae30b | ||
|
de87b03952 | ||
|
7bf7ae1d31 | ||
|
2082d2bf8a | ||
|
31da9cf5ab | ||
|
59ba737f5d | ||
|
45553416de | ||
|
63f3115a02 | ||
|
076f168707 | ||
|
c74fac68f4 | ||
|
500f289647 | ||
|
5b290a99b4 | ||
|
b05e58beb9 | ||
|
59bcd30b77 | ||
|
8268dc332b | ||
|
dfd12adb56 | ||
|
57f1eebb30 | ||
|
babd4580d2 | ||
|
b4d1504c9b | ||
|
236c55de33 | ||
|
eb914df7ac | ||
|
1add1b392f | ||
|
c341dfd3cd | ||
|
414cfca1af | ||
|
65491a6e5e | ||
|
f6333fcae0 | ||
|
0e8da70f77 | ||
|
f45c476d5b | ||
|
3b9f1e55fd | ||
|
f564e7529d | ||
|
b4810434c2 | ||
|
c01bc9bda8 | ||
|
1f2bb6adf7 | ||
|
cacd3e0790 | ||
|
83ced6d340 | ||
|
8fbd8a9593 | ||
|
d68b75641a | ||
|
49d96e57bb | ||
|
fc80287429 | ||
|
cbd852a0fc | ||
|
503e95a691 | ||
|
5a5abdaeed | ||
|
eb1576aa7c | ||
|
339a92fa6f | ||
|
e7f41c1dba | ||
|
364a57c857 | ||
|
a4592d666b | ||
|
8d4ab0cd69 | ||
|
e70de19c9d | ||
|
78f79a341e | ||
|
0605154d73 | ||
|
96a3599b7e | ||
|
6983104f40 | ||
|
ae2bf51028 | ||
|
caeed4456b | ||
|
157841da9b | ||
|
5aede4e36f | ||
|
f242bddcb4 | ||
|
9b24eb5f94 | ||
|
621cd1e169 | ||
|
a47040aa15 | ||
|
cfe04bfa7c | ||
|
985cdafdc2 | ||
|
76f6c51a1b | ||
|
c7d6b50778 | ||
|
0f27b8474c | ||
|
72009d2269 | ||
|
f9465b6288 | ||
|
6d356293c1 | ||
|
3adbbea28c | ||
|
492577f9db | ||
|
72c818bed0 | ||
|
690ed95c9a | ||
|
35c79dbc50 | ||
|
cba76cd608 | ||
|
4e59f4d8a8 | ||
|
bca672a1fb | ||
|
d11dcc27b7 | ||
|
c6d5676d3e | ||
|
9b2d745bcf | ||
|
e1ec2928a9 | ||
|
bd406136d6 | ||
|
c71d7ffeec | ||
|
e491231090 | ||
|
5b9af52ba1 | ||
|
12efe721d1 | ||
|
f893a75e9b | ||
|
607f2ff69c | ||
|
26d55d112d | ||
|
bff5b627fd | ||
|
0138d8d246 | ||
|
c2121a051d | ||
|
93be683995 | ||
|
792d982f03 | ||
|
5319bbe653 | ||
|
40b209f8e0 | ||
|
fa7ce1bc24 | ||
|
2fd861d65c | ||
|
f4bea090a8 | ||
|
73c09d30e3 | ||
|
9acced72e1 | ||
|
4a74adb178 | ||
|
777c3d5d66 | ||
|
fe2dbebdf5 | ||
|
aa036c6e40 | ||
|
f85e96c3a6 | ||
|
8b8a5e3148 | ||
|
b37dfe63e9 | ||
|
a45dcc5580 | ||
|
4f22fb9812 | ||
|
ada65c8da9 | ||
|
6c0b451c23 | ||
|
0695e5bfc1 | ||
|
fa369e79cf | ||
|
f83af0bf30 | ||
|
18fbcf75f7 | ||
|
9cc7723949 | ||
|
11d32d2569 | ||
|
f117e79d43 | ||
|
9c4462d534 | ||
|
0fc7a27cf4 | ||
|
e7479d6a41 | ||
|
e5e7a17d82 | ||
|
06bb71f2ae | ||
|
d4663fc469 | ||
|
c67d2693b5 | ||
|
d153ecbe1f | ||
|
a809828dc1 | ||
|
ac3073fb4c | ||
|
565a4d377d | ||
|
b58e5f5bb7 | ||
|
516533c374 | ||
|
94da1d9ded | ||
|
c52bc3dae7 | ||
|
f77bfe2bc7 | ||
|
f69cfb0ffa | ||
|
8eb26085f0 | ||
|
1241d8226d | ||
|
a4a2e237db | ||
|
cb0da8d349 | ||
|
adf93942b2 | ||
|
93c382eae1 | ||
|
638c823722 | ||
|
c1f45a8319 | ||
|
37892cbe7f | ||
|
84202e77a1 | ||
|
f01c92fbcf | ||
|
ac19d07c24 | ||
|
465bf3f9d0 | ||
|
8f88c408f7 | ||
|
4ea7c7ef63 | ||
|
918258948b | ||
|
52630f06e3 | ||
|
73ed01a9bc | ||
|
8c2b97dce5 | ||
|
6eaf3632d9 | ||
|
e2365ee0f9 | ||
|
9f7ff7dc59 | ||
|
ac5718b6e8 | ||
|
caf22d19c0 | ||
|
27a8d205ca | ||
|
4fa57dfc59 | ||
|
7e5a1d1893 | ||
|
14685a50a2 | ||
|
caade65be3 | ||
|
0b4eaf9d4f | ||
|
7ff7ce6f19 | ||
|
984af8750a | ||
|
5ecb2033db | ||
|
6c0c895ccd | ||
|
2a28d8c976 | ||
|
5da3ae7770 | ||
|
29068cb01e | ||
|
ee8a4b6ab6 | ||
|
144d52b444 | ||
|
2c7bf4f44d | ||
|
b9f56f0e30 | ||
|
1f92ea0108 | ||
|
13dae4f524 | ||
|
fa38433de9 | ||
|
b4c9caa4c7 | ||
|
17602f5fcd | ||
|
3ad93ac8aa | ||
|
5bc756bc83 | ||
|
48fdf6a85c | ||
|
bc1510582e | ||
|
5f43c5e2a2 | ||
|
0905bd2f58 | ||
|
b94d54f3dd | ||
|
148f4c2c6d | ||
|
a58967dcab | ||
|
3df5e6ca5c | ||
|
02cbcbd5ed | ||
|
620bdc61dd | ||
|
3e39a58c8e | ||
|
376b728e68 | ||
|
47311c5f2c | ||
|
999ccfd990 | ||
|
df914450e2 | ||
|
0fa4aa6a1f | ||
|
fba29de459 | ||
|
73bc91f743 | ||
|
d074dd7edb | ||
|
5f32baceef | ||
|
4e5ebc235d | ||
|
7bd02ab097 | ||
|
98326f1315 | ||
|
406cb5a334 | ||
|
7f226fe5d2 | ||
|
8f82b27e50 | ||
|
19556eb970 | ||
|
b43c9ee59b | ||
|
7745d6db17 | ||
|
656a115eba | ||
|
3569cef5dd | ||
|
3d69a0b88c | ||
|
18864e06be | ||
|
8076eb6d06 | ||
|
b1f73f97de | ||
|
6145dbdb4d | ||
|
4eee3286f2 | ||
|
4e8de2d8ce | ||
|
1e812d686a | ||
|
0dad4d2743 | ||
|
b24d843164 | ||
|
ac742cc893 | ||
|
f8d9dd52d3 | ||
|
2eff70c85a | ||
|
6f374b3c43 | ||
|
13b24cc03e | ||
|
829451641b | ||
|
8160af4ef4 | ||
|
74411da11f | ||
|
c9eb5750b4 | ||
|
036aca49a4 | ||
|
af92ce8d37 | ||
|
e59a652263 | ||
|
3f5f2d66cc | ||
|
6607b23046 | ||
|
f258bdb78e | ||
|
d36515367b | ||
|
fd6d60327b | ||
|
044b4bce72 | ||
|
764545a39d | ||
|
4bf1527abe | ||
|
28e493d167 | ||
|
7aeb7c6bc7 | ||
|
4fed2d201f | ||
|
bf31abce09 | ||
|
387582b019 | ||
|
e5795563f3 | ||
|
e63c50f58f | ||
|
dfca75229d | ||
|
c894cdff8b | ||
|
c231c9186f | ||
|
f8e06d7ca3 | ||
|
9af3ffb77c | ||
|
9c225458c6 | ||
|
ed84689ca0 | ||
|
37822d7a0d | ||
|
18bd957167 | ||
|
662989169f | ||
|
28de616bbf | ||
|
827eb8de19 | ||
|
87639f3c73 | ||
|
c904a49aaf | ||
|
f34a1eb755 | ||
|
e8eaba44ed | ||
|
42c727e995 | ||
|
0615ba25ed | ||
|
609d464ac7 | ||
|
aed30cb572 | ||
|
4e8197a623 | ||
|
af73fc2e51 | ||
|
05c063e4d3 | ||
|
0500f39fa6 | ||
|
979b8ddcb2 | ||
|
72938bf5ad | ||
|
73e596d29c | ||
|
43b95125f0 | ||
|
1ab0cbcc63 | ||
|
756ea9521c | ||
|
0e2edde2a5 | ||
|
b251132f09 | ||
|
c02bedfd97 | ||
|
d9f8cba6d8 | ||
|
66df34b586 | ||
|
8d546ce1f5 | ||
|
3e0c6f2986 | ||
|
d85f0b98fd | ||
|
ee77bb11fc | ||
|
fda7c693a3 | ||
|
2e3b89288d | ||
|
7e8d82384a | ||
|
193e35e21b | ||
|
b4f8c0a7ba | ||
|
05d7211c4c | ||
|
57866333a8 | ||
|
073cf60030 | ||
|
9bdb3d77b7 | ||
|
824787b2f2 | ||
|
40ecb6a248 | ||
|
2481a44c6d | ||
|
f938f371e5 | ||
|
766a1955ee | ||
|
ba0765ac6f | ||
|
d0acea818a | ||
|
e1dc5d1176 | ||
|
8c62026db3 | ||
|
645aca9f63 | ||
|
4d51bbc9a2 | ||
|
9dcf0fac77 | ||
|
7f9010b9a5 | ||
|
b057908164 | ||
|
47c5498fbe | ||
|
8f3de46811 | ||
|
0777c6e43c | ||
|
f59006aedd | ||
|
2ffe9bb83a | ||
|
a58398c093 | ||
|
2449757021 | ||
|
9554de63b2 | ||
|
913a3e3ad9 | ||
|
a8c7d49528 | ||
|
be00f173c4 | ||
|
5fbf1eea1e | ||
|
92f1ef98d0 | ||
|
344603123a | ||
|
489476feeb | ||
|
ae75f7228c | ||
|
8b5db9827a | ||
|
ea45200526 | ||
|
ec144b4181 | ||
|
2baebffc32 | ||
|
c4f93e4d12 | ||
|
fb57d4d9de | ||
|
6f4428f6ab | ||
|
852efd3989 | ||
|
4e608eb242 | ||
|
6aefbbf2c6 | ||
|
f966a2b846 | ||
|
59666dc5db | ||
|
6351e209d2 | ||
|
398e9d3a98 | ||
|
dd3e98eb30 | ||
|
0b01bacb78 | ||
|
dc401a4da6 | ||
|
b63e413d6e | ||
|
0fff78b0a0 | ||
|
f2755aa1f4 | ||
|
6cea02684c | ||
|
fe2248d097 | ||
|
6d0952231a | ||
|
2fc4b3d419 | ||
|
ba40fc54ba | ||
|
d79dce6669 | ||
|
c7817f9bfd | ||
|
286b2087cd | ||
|
a8e313b5e9 | ||
|
5bd411b394 | ||
|
c44a3a971b | ||
|
5bbee220a2 | ||
|
3d9cbf5b91 | ||
|
7abd210623 | ||
|
0234da3d63 | ||
|
f0d802839e | ||
|
20899e2bea | ||
|
00ad4bf864 | ||
|
775a60b71d | ||
|
ac701863dc | ||
|
00d6c13344 | ||
|
2214b546ed | ||
|
50a9a917b3 | ||
|
6fa753a149 | ||
|
29867c3c77 | ||
|
19cfee10a0 | ||
|
2027cb1bc0 | ||
|
2f03a13d34 | ||
|
36fef6beb3 | ||
|
7c6732e896 | ||
|
7acb369408 | ||
|
2a4dd7e593 | ||
|
a8362bf5b0 | ||
|
02c85e9330 | ||
|
08f9443c82 | ||
|
b81eb57744 | ||
|
20573b362c | ||
|
90eccfcaa0 | ||
|
a2dc354e2c | ||
|
dea57e0780 | ||
|
d69d465abf | ||
|
7e986b82c7 | ||
|
689fe9218d | ||
|
0edb03943b | ||
|
7d34eab0f0 | ||
|
2214455364 | ||
|
52ba09044d | ||
|
5e9ad43281 | ||
|
e309933d2e | ||
|
2373f6e508 | ||
|
ed0cb15013 | ||
|
d7dfd4ab65 | ||
|
620bd18cf9 | ||
|
32c0006825 | ||
|
bacd5a0f0d | ||
|
eaa12e8b31 | ||
|
5a28a81f0f | ||
|
08f33ac3e7 | ||
|
7c1f892ec2 | ||
|
32aad06308 | ||
|
ab5e05485e | ||
|
b3ab9e468c | ||
|
af47d5d8e4 | ||
|
6f2d139f45 | ||
|
014a3c1f5c | ||
|
7b60c2c5b9 | ||
|
ce77eff7cb | ||
|
404933625f | ||
|
6d40891c74 | ||
|
90b6907e71 | ||
|
3ba4533b28 | ||
|
d5e499febd | ||
|
79ed325a29 | ||
|
85220bf58f | ||
|
4c3829b8c1 | ||
|
ee334fd974 | ||
|
6e587f96cf | ||
|
241e70863f | ||
|
1d1a01fcd1 | ||
|
7200591b1e | ||
|
245fc2ad77 | ||
|
630c48193d | ||
|
8e5c6509c2 | ||
|
207c3ec80a | ||
|
b43aa77ff8 | ||
|
473f9ea0a0 | ||
|
7520b6318d | ||
|
b1d2f85af3 | ||
|
596ed1a662 | ||
|
519bb57368 | ||
|
e3135a8e99 | ||
|
0de492d96f | ||
|
1a4cd7d3f6 | ||
|
2df99a312e | ||
|
4cde94f203 | ||
|
a1d3247d52 | ||
|
282fe44c85 | ||
|
57a62a054b | ||
|
0941d1ae32 | ||
|
4d431935e1 | ||
|
367d27258c | ||
|
53b6f65add | ||
|
abfd99fe7f | ||
|
c2ef811faa | ||
|
d11cc1d010 | ||
|
4531df2759 | ||
|
7a750f930a | ||
|
1f7904e317 | ||
|
18cd647e43 | ||
|
e63598271b | ||
|
2a1947ea6e | ||
|
f07fde39aa | ||
|
36745bfabc | ||
|
f25469a288 | ||
|
ee90539bd8 | ||
|
01312996d8 | ||
|
73c1f1b767 | ||
|
aa5695ac45 | ||
|
6f5a08f87d | ||
|
e1be33314e | ||
|
4a3f8278ea | ||
|
825d18fbae | ||
|
5761f6dc34 | ||
|
a83cb0c3b2 | ||
|
b85e846de5 | ||
|
dbcf32da26 | ||
|
19cdfcf094 | ||
|
c7cc0e084f | ||
|
682147a26e | ||
|
54af9cc667 | ||
|
7189c494ea | ||
|
fd7c28880c | ||
|
b8abf12fd4 | ||
|
ec0df164ce | ||
|
f44559869a | ||
|
8616adb437 | ||
|
96b60f4ee2 | ||
|
7e9e33b846 | ||
|
74b7581354 | ||
|
a626736f2d | ||
|
a31e810ec9 | ||
|
4a56c114d0 | ||
|
c315911da4 | ||
|
3e760e6cc6 | ||
|
371eb68856 | ||
|
682d4883ff | ||
|
e3da9914ba | ||
|
aa12fe3976 | ||
|
f480108c25 | ||
|
2a7eb83965 | ||
|
fc7d84dc24 | ||
|
46f3fd3866 | ||
|
d0cad92fce | ||
|
45e2b6aeb8 | ||
|
e586455be3 | ||
|
b13e54f49b | ||
|
cb4e348c15 | ||
|
e204788ef6 | ||
|
8e850ea2a6 | ||
|
befbdc399d | ||
|
22bc9425a2 | ||
|
ef60992843 | ||
|
9c9c4a998c | ||
|
e08e377b34 | ||
|
96d4a250cf | ||
|
8378f9c1d1 | ||
|
21aa38c52f | ||
|
5a5df06cda | ||
|
b2110de110 | ||
|
f530d95f59 | ||
|
2d091f3cb6 | ||
|
9aa97d7dda | ||
|
7b29ee8ebf | ||
|
8547af74ab | ||
|
c1ec89b0a7 | ||
|
4ac3289819 | ||
|
c02a4fb08d | ||
|
9a733f06c0 | ||
|
832397a98e | ||
|
b7378b9b10 | ||
|
b7a29bb2c7 | ||
|
f0d61a8261 | ||
|
f0ba86b699 | ||
|
8e1dc42bfb | ||
|
a1338cbbab | ||
|
60861e0afa | ||
|
ab8d960384 | ||
|
0983330187 | ||
|
a1b35909b0 | ||
|
1db0350273 | ||
|
157037f56f | ||
|
91a048009a | ||
|
e888cde7dd | ||
|
d712191012 | ||
|
ad964ca61a | ||
|
2cbeb23d8f | ||
|
8031f1c277 | ||
|
a6d09a2ce5 | ||
|
f43909141f | ||
|
9792ab71dc | ||
|
f5dfedf8fa | ||
|
16b46bc74d | ||
|
73bcf928e4 | ||
|
0aaaf27314 | ||
|
74fca7de2e | ||
|
a5b3215ff7 | ||
|
7cc5ff11d4 | ||
|
c80e3bf921 | ||
|
2f4f547eed | ||
|
21d569bd2b | ||
|
6bb86caff3 | ||
|
1d92b91fc6 | ||
|
678da384a9 | ||
|
b3986df788 | ||
|
3fc35aefe7 | ||
|
eea6c3458c | ||
|
ad009b5837 | ||
|
77a615adb6 | ||
|
21a8f4da76 | ||
|
80a9d51ffb | ||
|
450569e5c4 | ||
|
5efb33d1d3 | ||
|
08662c1595 | ||
|
7245de8c4c | ||
|
5d70daaaaa | ||
|
f0415b6b20 | ||
|
8d43eb6664 | ||
|
9ef5a2501b | ||
|
ad480a5b32 | ||
|
c393fdaa9b | ||
|
e05db36841 | ||
|
0e24ac0a14 | ||
|
168fef71c7 | ||
|
36279be694 | ||
|
dd9727a701 | ||
|
58a7456087 | ||
|
6df213956f | ||
|
5adb799100 | ||
|
8d6925afd3 | ||
|
a6f9275144 | ||
|
8bd9f34d98 | ||
|
d8064a9a35 | ||
|
b065542020 | ||
|
1125f57a7f | ||
|
b2b1cce085 |
@ -24,6 +24,7 @@ QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -8,7 +8,7 @@ assignees: ''
|
||||
---
|
||||
|
||||
<!-- Before posting please check our "Troubleshooting" category in the docs:
|
||||
https://invoiceninja.github.io/docs/self-host-troubleshooting/ -->
|
||||
https://invoiceninja.github.io/en/self-host-troubleshooting/ -->
|
||||
|
||||
## Setup
|
||||
- Version: <!-- i.e. v4.5.25 / v5.0.30 -->
|
||||
|
3
.github/workflows/phpunit.yml
vendored
3
.github/workflows/phpunit.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
phpunit-versions: ['latest']
|
||||
ci_node_total: [ 8 ]
|
||||
ci_node_index: [ 0, 1, 2, 3, 4, 5, 6, 7]
|
||||
laravel: [10.*]
|
||||
laravel: [11.*]
|
||||
dependency-version: [prefer-stable]
|
||||
|
||||
env:
|
||||
@ -103,7 +103,6 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.php }}-composer-
|
||||
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: |
|
||||
composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }}
|
||||
|
@ -51,6 +51,8 @@ All Pro and Enterprise features from the hosted app are included in the open-sou
|
||||
* [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/)
|
||||
* [Cloudron](https://www.cloudron.io/store/com.invoiceninja.cloudronapp2.html)
|
||||
* [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja)
|
||||
* [Elestio](https://elest.io/open-source/invoiceninja)
|
||||
* [YunoHost](https://apps.yunohost.org/app/invoiceninja5)
|
||||
|
||||
### Recommended Providers
|
||||
* [Stripe](https://stripe.com/)
|
||||
|
@ -1 +1 @@
|
||||
5.10.17
|
||||
5.10.29
|
50
app/Casts/QuickbooksSettingsCast.php
Normal file
50
app/Casts/QuickbooksSettingsCast.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use App\DataMapper\QuickbooksSettings;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
class QuickbooksSettingsCast implements CastsAttributes
|
||||
{
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
$data = json_decode($value, true);
|
||||
|
||||
if(!is_array($data))
|
||||
return null;
|
||||
|
||||
$qb = new QuickbooksSettings();
|
||||
$qb->accessTokenKey = $data['accessTokenKey'];
|
||||
$qb->refresh_token = $data['refresh_token'];
|
||||
$qb->realmID = $data['realmID'];
|
||||
$qb->accessTokenExpiresAt = $data['accessTokenExpiresAt'];
|
||||
$qb->refreshTokenExpiresAt = $data['refreshTokenExpiresAt'];
|
||||
$qb->settings = $data['settings'] ?? [];
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
return [
|
||||
$key => json_encode([
|
||||
'accessTokenKey' => $value->accessTokenKey,
|
||||
'refresh_token' => $value->refresh_token,
|
||||
'realmID' => $value->realmID,
|
||||
'accessTokenExpiresAt' => $value->accessTokenExpiresAt,
|
||||
'refreshTokenExpiresAt' => $value->refreshTokenExpiresAt,
|
||||
'settings' => $value->settings,
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
@ -1169,7 +1169,7 @@ class CheckData extends Command
|
||||
->whereNull('exchange_rate')
|
||||
->orWhere('exchange_rate', 0)
|
||||
->cursor()
|
||||
->each(function ($expense){
|
||||
->each(function ($expense) {
|
||||
$expense->exchange_rate = 1;
|
||||
$expense->saveQuietly();
|
||||
|
||||
|
83
app/DataMapper/Analytics/LegalEntityCreated.php
Normal file
83
app/DataMapper/Analytics/LegalEntityCreated.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper\Analytics;
|
||||
|
||||
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
|
||||
|
||||
class LegalEntityCreated extends GenericMixedMetric
|
||||
{
|
||||
/**
|
||||
* The type of Sample.
|
||||
*
|
||||
* Monotonically incrementing counter
|
||||
*
|
||||
* - counter
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type = 'mixed_metric';
|
||||
|
||||
/**
|
||||
* The name of the counter.
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'einvoice.legal_entity.created';
|
||||
|
||||
/**
|
||||
* The datetime of the counter measurement.
|
||||
*
|
||||
* date("Y-m-d H:i:s")
|
||||
*
|
||||
*/
|
||||
public $datetime;
|
||||
|
||||
/**
|
||||
* The Class failure name
|
||||
* set to 0.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $string_metric5 = 'stub';
|
||||
|
||||
/**
|
||||
* The exception string
|
||||
* set to 0.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $string_metric6 = 'stub';
|
||||
|
||||
/**
|
||||
* The counter
|
||||
* set to 1.
|
||||
*
|
||||
*/
|
||||
public $int_metric1 = 1;
|
||||
|
||||
/**
|
||||
* Company Key
|
||||
* @var string
|
||||
*/
|
||||
public $string_metric7 = '';
|
||||
|
||||
/**
|
||||
* Subject
|
||||
* @var string
|
||||
*/
|
||||
public $string_metric8 = '';
|
||||
|
||||
public function __construct($string_metric7 = '', $string_metric8 = '')
|
||||
{
|
||||
$this->string_metric7 = $string_metric7;
|
||||
$this->string_metric8 = $string_metric8;
|
||||
}
|
||||
}
|
@ -16,13 +16,6 @@ namespace App\DataMapper;
|
||||
*/
|
||||
class BaseSettings
|
||||
{
|
||||
// //@deprecated
|
||||
// public function __construct($obj)
|
||||
// {
|
||||
// // foreach ($obj as $key => $value) {
|
||||
// // $obj->{$key} = $value;
|
||||
// // }
|
||||
// }
|
||||
|
||||
public static function setCasts($obj, $casts)
|
||||
{
|
||||
|
@ -516,9 +516,13 @@ class CompanySettings extends BaseSettings
|
||||
public $quote_late_fee_amount1 = 0;
|
||||
public $quote_late_fee_percent1 = 0;
|
||||
|
||||
public string $payment_flow = 'default'; //smooth
|
||||
|
||||
public string $email_subject_payment_failed = '';
|
||||
public string $email_template_payment_failed = '';
|
||||
|
||||
public static $casts = [
|
||||
'payment_flow' => 'string',
|
||||
'enable_quote_reminder1' => 'bool',
|
||||
'quote_num_days_reminder1' => 'int',
|
||||
'quote_schedule_reminder1' => 'string',
|
||||
@ -768,6 +772,8 @@ class CompanySettings extends BaseSettings
|
||||
'portal_custom_js' => 'string',
|
||||
'client_portal_enable_uploads' => 'bool',
|
||||
'purchase_order_number_counter' => 'integer',
|
||||
'email_template_payment_failed' => 'string',
|
||||
'email_subject_payment_failed' => 'string',
|
||||
];
|
||||
|
||||
public static $free_plan_casts = [
|
||||
|
@ -30,6 +30,7 @@ class EmailTemplateDefaults
|
||||
'email_template_custom2',
|
||||
'email_template_custom3',
|
||||
'email_template_purchase_order',
|
||||
'email_template_payment_failed'
|
||||
];
|
||||
|
||||
public static function getDefaultTemplate($template, $locale)
|
||||
@ -39,6 +40,8 @@ class EmailTemplateDefaults
|
||||
switch ($template) {
|
||||
/* Template */
|
||||
|
||||
case 'email_template_payment_failed':
|
||||
return self::emailPaymentFailedTemplate();
|
||||
case 'email_template_invoice':
|
||||
return self::emailInvoiceTemplate();
|
||||
case 'email_template_quote':
|
||||
@ -73,6 +76,9 @@ class EmailTemplateDefaults
|
||||
case 'email_subject_invoice':
|
||||
return self::emailInvoiceSubject();
|
||||
|
||||
case 'email_subject_payment_failed':
|
||||
return self::emailPaymentFailedSubject();
|
||||
|
||||
case 'email_subject_quote':
|
||||
return self::emailQuoteSubject();
|
||||
|
||||
@ -127,6 +133,16 @@ class EmailTemplateDefaults
|
||||
}
|
||||
}
|
||||
|
||||
public static function emailPaymentFailedSubject()
|
||||
{
|
||||
return ctrans('texts.notification_invoice_payment_failed_subject', ['invoice' => '$number']);
|
||||
}
|
||||
|
||||
public static function emailPaymentFailedTemplate()
|
||||
{
|
||||
return '<p>$client<br><br>'.ctrans('texts.client_payment_failure_body', ['invoice' => '$number', 'amount' => '$amount']).'</p><div>$payment_error</div><br><div>$view_button</div>';
|
||||
}
|
||||
|
||||
public static function emailQuoteReminder1Subject()
|
||||
{
|
||||
return ctrans('texts.quote_reminder_subject', ['quote' => '$number', 'company' => '$company.name']);
|
||||
@ -135,9 +151,7 @@ class EmailTemplateDefaults
|
||||
public static function emailQuoteReminder1Body()
|
||||
{
|
||||
|
||||
$invoice_message = '<p>$client<br><br>'.self::transformText('quote_reminder_message').'</p><div class="center">$view_button</div>';
|
||||
|
||||
return $invoice_message;
|
||||
return '<p>$client<br><br>'.self::transformText('quote_reminder_message').'</p><div>$view_button</div>';
|
||||
|
||||
}
|
||||
|
||||
@ -163,14 +177,14 @@ class EmailTemplateDefaults
|
||||
|
||||
public static function emailInvoiceTemplate()
|
||||
{
|
||||
$invoice_message = '<p>$client<br><br>'.self::transformText('invoice_message').'</p><div class="center">$view_button</div>';
|
||||
$invoice_message = '<p>$client<br><br>'.self::transformText('invoice_message').'</p><div>$view_button</div>';
|
||||
|
||||
return $invoice_message;
|
||||
}
|
||||
|
||||
public static function emailInvoiceReminderTemplate()
|
||||
{
|
||||
$invoice_message = '<p>$client<br><br>'.self::transformText('reminder_message').'</p><div class="center">$view_button</div>';
|
||||
$invoice_message = '<p>$client<br><br>'.self::transformText('reminder_message').'</p><div>$view_button</div>';
|
||||
|
||||
return $invoice_message;
|
||||
}
|
||||
@ -182,7 +196,7 @@ class EmailTemplateDefaults
|
||||
|
||||
public static function emailQuoteTemplate()
|
||||
{
|
||||
$quote_message = '<p>$client<br><br>'.self::transformText('quote_message').'</p><div class="center">$view_button</div>';
|
||||
$quote_message = '<p>$client<br><br>'.self::transformText('quote_message').'</p><div>$view_button</div>';
|
||||
|
||||
return $quote_message;
|
||||
}
|
||||
@ -199,28 +213,28 @@ class EmailTemplateDefaults
|
||||
|
||||
public static function emailPurchaseOrderTemplate()
|
||||
{
|
||||
$purchase_order_message = '<p>$vendor<br><br>'.self::transformText('purchase_order_message').'</p><div class="center">$view_button</div>';
|
||||
$purchase_order_message = '<p>$vendor<br><br>'.self::transformText('purchase_order_message').'</p><div>$view_button</div>';
|
||||
|
||||
return $purchase_order_message;
|
||||
}
|
||||
|
||||
public static function emailPaymentTemplate()
|
||||
{
|
||||
$payment_message = '<p>$client<br><br>'.self::transformText('payment_message').'<br><br>$invoices</p><div class="center">$view_button</div>';
|
||||
$payment_message = '<p>$client<br><br>'.self::transformText('payment_message').'<br><br>$invoices</p><div>$view_button</div>';
|
||||
|
||||
return $payment_message;
|
||||
}
|
||||
|
||||
public static function emailCreditTemplate()
|
||||
{
|
||||
$credit_message = '<p>$client<br><br>'.self::transformText('credit_message').'</p><div class="center">$view_button</div>';
|
||||
$credit_message = '<p>$client<br><br>'.self::transformText('credit_message').'</p><div>$view_button</div>';
|
||||
|
||||
return $credit_message;
|
||||
}
|
||||
|
||||
public static function emailPaymentPartialTemplate()
|
||||
{
|
||||
$payment_message = '<p>$client<br><br>'.self::transformText('payment_message').'<br><br>$invoices</p><div class="center">$view_button</div>';
|
||||
$payment_message = '<p>$client<br><br>'.self::transformText('payment_message').'<br><br>$invoices</p><div>$view_button</div>';
|
||||
|
||||
return $payment_message;
|
||||
}
|
||||
|
61
app/DataMapper/QuickbooksSettings.php
Normal file
61
app/DataMapper/QuickbooksSettings.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
use App\Casts\QuickbooksSettingsCast;
|
||||
|
||||
/**
|
||||
* QuickbooksSettings.
|
||||
*/
|
||||
class QuickbooksSettings implements Castable
|
||||
{
|
||||
public string $accessTokenKey;
|
||||
|
||||
public string $refresh_token;
|
||||
|
||||
public string $realmID;
|
||||
|
||||
public int $accessTokenExpiresAt;
|
||||
|
||||
public int $refreshTokenExpiresAt;
|
||||
|
||||
public string $baseURL;
|
||||
/**
|
||||
* entity client,invoice,quote,purchase_order,vendor,payment
|
||||
* sync true/false
|
||||
* update_record true/false
|
||||
* direction push/pull/birectional
|
||||
* */
|
||||
public array $settings = [
|
||||
'client' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'vendor' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'invoice' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'sales' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'quote' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'purchase_order' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'product' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'payment' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Get the name of the caster class to use when casting from / to this cast target.
|
||||
*
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public static function castUsing(array $arguments): string
|
||||
{
|
||||
return QuickbooksSettingsCast::class;
|
||||
}
|
||||
|
||||
}
|
@ -17,6 +17,7 @@ use App\Models\Invoice;
|
||||
use App\Models\Product;
|
||||
use App\DataProviders\USStates;
|
||||
use App\DataMapper\Tax\ZipTax\Response;
|
||||
use App\Models\RecurringInvoice;
|
||||
|
||||
class BaseRule implements RuleInterface
|
||||
{
|
||||
@ -47,6 +48,9 @@ class BaseRule implements RuleInterface
|
||||
'DK', // Denmark
|
||||
'EE', // Estonia
|
||||
'ES', // Spain
|
||||
'ES-CN', // Canary Islands
|
||||
'ES-CE', // Ceuta
|
||||
'ES-ML', // Melilla
|
||||
'FI', // Finland
|
||||
'FR', // France
|
||||
'GR', // Greece
|
||||
@ -77,6 +81,9 @@ class BaseRule implements RuleInterface
|
||||
'DK' => 'EU', // Denmark
|
||||
'EE' => 'EU', // Estonia
|
||||
'ES' => 'EU', // Spain
|
||||
'ES-CN' => 'EU', // Canary Islands
|
||||
'ES-CE' => 'EU', // Ceuta
|
||||
'ES-ML' => 'EU', // Melilla
|
||||
'FI' => 'EU', // Finland
|
||||
'FR' => 'EU', // France
|
||||
'GR' => 'EU', // Greece
|
||||
@ -132,7 +139,7 @@ class BaseRule implements RuleInterface
|
||||
|
||||
public function shouldCalcTax(): bool
|
||||
{
|
||||
return $this->should_calc_tax;
|
||||
return $this->should_calc_tax && $this->checkIfInvoiceLocked();
|
||||
}
|
||||
/**
|
||||
* Initializes the tax rule for the entity.
|
||||
@ -215,7 +222,7 @@ class BaseRule implements RuleInterface
|
||||
|
||||
$this->invoice->tax_data = $tax_data;
|
||||
|
||||
if(\DB::transactionLevel() == 0) {
|
||||
if(\DB::transactionLevel() == 0 && isset($this->invoice->id)) {
|
||||
|
||||
try {
|
||||
$this->invoice->saveQuietly();
|
||||
@ -400,4 +407,40 @@ class BaseRule implements RuleInterface
|
||||
return ! in_array($iso_3166_2, array_merge($this->eu_country_codes, array_keys($this->region_codes)));
|
||||
}
|
||||
|
||||
private function checkIfInvoiceLocked(): bool
|
||||
{
|
||||
$lock_invoices = $this->client->getSetting('lock_invoices');
|
||||
|
||||
if($this->invoice instanceof RecurringInvoice) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch ($lock_invoices) {
|
||||
case 'off':
|
||||
return true;
|
||||
case 'when_sent':
|
||||
if ($this->invoice->status_id == Invoice::STATUS_SENT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case 'when_paid':
|
||||
if ($this->invoice->status_id == Invoice::STATUS_PAID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
//if now is greater than the end of month the invoice was dated - do not modify
|
||||
case 'end_of_month':
|
||||
if(\Carbon\Carbon::parse($this->invoice->date)->setTimezone($this->invoice->company->timezone()->name)->endOfMonth()->lte(now())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -43,6 +43,8 @@ class Rule extends BaseRule implements RuleInterface
|
||||
public float $reduced_tax_rate = 0;
|
||||
|
||||
public string $tax_name1 = 'MwSt.';
|
||||
|
||||
private string $tax_name;
|
||||
/**
|
||||
* Initializes the rules and builds any required data.
|
||||
*
|
||||
@ -50,6 +52,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*/
|
||||
public function init(): self
|
||||
{
|
||||
$this->tax_name = $this->tax_name1;
|
||||
$this->calculateRates();
|
||||
|
||||
return $this;
|
||||
@ -91,6 +94,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*/
|
||||
public function reverseTax($item): self
|
||||
{
|
||||
$this->tax_name1 = $this->tax_name;
|
||||
$this->tax_rate1 = 0;
|
||||
|
||||
return $this;
|
||||
@ -103,6 +107,8 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*/
|
||||
public function taxReduced($item): self
|
||||
{
|
||||
|
||||
$this->tax_name1 = $this->tax_name;
|
||||
$this->tax_rate1 = $this->reduced_tax_rate;
|
||||
|
||||
return $this;
|
||||
@ -115,6 +121,8 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*/
|
||||
public function zeroRated($item): self
|
||||
{
|
||||
|
||||
$this->tax_name1 = $this->tax_name;
|
||||
$this->tax_rate1 = 0;
|
||||
|
||||
return $this;
|
||||
@ -142,6 +150,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
public function taxDigital($item): self
|
||||
{
|
||||
|
||||
$this->tax_name1 = $this->tax_name;
|
||||
$this->tax_rate1 = $this->tax_rate;
|
||||
|
||||
return $this;
|
||||
@ -155,6 +164,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
public function taxService($item): self
|
||||
{
|
||||
|
||||
$this->tax_name1 = $this->tax_name;
|
||||
$this->tax_rate1 = $this->tax_rate;
|
||||
|
||||
return $this;
|
||||
@ -168,6 +178,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
public function taxShipping($item): self
|
||||
{
|
||||
|
||||
$this->tax_name1 = $this->tax_name;
|
||||
$this->tax_rate1 = $this->tax_rate;
|
||||
|
||||
return $this;
|
||||
@ -181,6 +192,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
public function taxPhysical($item): self
|
||||
{
|
||||
|
||||
$this->tax_name1 = $this->tax_name;
|
||||
$this->tax_rate1 = $this->tax_rate;
|
||||
|
||||
return $this;
|
||||
@ -229,8 +241,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
// nlog("tax exempt");
|
||||
$this->tax_rate = 0;
|
||||
$this->reduced_tax_rate = 0;
|
||||
} elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->vat_number && $this->eu_business_tax_exempt) {
|
||||
// elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt)
|
||||
} elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->vat_number && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt) {
|
||||
// nlog("euro zone and tax exempt");
|
||||
$this->tax_rate = 0;
|
||||
$this->reduced_tax_rate = 0;
|
||||
@ -240,8 +251,8 @@ class Rule extends BaseRule implements RuleInterface
|
||||
$this->reduced_tax_rate = 0;
|
||||
} elseif(!in_array($this->client_subregion, $this->eu_country_codes)) {
|
||||
$this->defaultForeign();
|
||||
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && !$this->client->vat_number) { //eu country / no valid vat
|
||||
if(($this->client->company->tax_data->seller_subregion != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold) {
|
||||
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && ((strlen($this->client->vat_number ?? '') == 1) || !$this->client->has_valid_vat_number)) { //eu country / no valid vat
|
||||
if($this->client->company->tax_data->seller_subregion != $this->client_subregion) {
|
||||
// nlog("eu zone with sales above threshold");
|
||||
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate ?? 0;
|
||||
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate ?? 0;
|
||||
|
44
app/DataMapper/Tax/ES_CE/Rule.php
Normal file
44
app/DataMapper/Tax/ES_CE/Rule.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper\Tax\ES_CE;
|
||||
|
||||
use App\DataMapper\Tax\DE\Rule as DERule;
|
||||
|
||||
class Rule extends DERule
|
||||
{
|
||||
/** @var string $seller_region */
|
||||
public string $seller_region = 'EU';
|
||||
|
||||
/** @var bool $consumer_tax_exempt */
|
||||
public bool $consumer_tax_exempt = false;
|
||||
|
||||
/** @var bool $business_tax_exempt */
|
||||
public bool $business_tax_exempt = false;
|
||||
|
||||
/** @var bool $eu_business_tax_exempt */
|
||||
public bool $eu_business_tax_exempt = true;
|
||||
|
||||
/** @var bool $foreign_business_tax_exempt */
|
||||
public bool $foreign_business_tax_exempt = false;
|
||||
|
||||
/** @var bool $foreign_consumer_tax_exempt */
|
||||
public bool $foreign_consumer_tax_exempt = false;
|
||||
|
||||
/** @var float $tax_rate */
|
||||
public float $tax_rate = 0;
|
||||
|
||||
/** @var float $reduced_tax_rate */
|
||||
public float $reduced_tax_rate = 0;
|
||||
|
||||
public string $tax_name1 = 'IGIC';
|
||||
|
||||
}
|
44
app/DataMapper/Tax/ES_CN/Rule.php
Normal file
44
app/DataMapper/Tax/ES_CN/Rule.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper\Tax\ES_CN;
|
||||
|
||||
use App\DataMapper\Tax\DE\Rule as DERule;
|
||||
|
||||
class Rule extends DERule
|
||||
{
|
||||
/** @var string $seller_region */
|
||||
public string $seller_region = 'EU';
|
||||
|
||||
/** @var bool $consumer_tax_exempt */
|
||||
public bool $consumer_tax_exempt = false;
|
||||
|
||||
/** @var bool $business_tax_exempt */
|
||||
public bool $business_tax_exempt = false;
|
||||
|
||||
/** @var bool $eu_business_tax_exempt */
|
||||
public bool $eu_business_tax_exempt = true;
|
||||
|
||||
/** @var bool $foreign_business_tax_exempt */
|
||||
public bool $foreign_business_tax_exempt = false;
|
||||
|
||||
/** @var bool $foreign_consumer_tax_exempt */
|
||||
public bool $foreign_consumer_tax_exempt = false;
|
||||
|
||||
/** @var float $tax_rate */
|
||||
public float $tax_rate = 0;
|
||||
|
||||
/** @var float $reduced_tax_rate */
|
||||
public float $reduced_tax_rate = 0;
|
||||
|
||||
public string $tax_name1 = 'IGIC';
|
||||
|
||||
}
|
44
app/DataMapper/Tax/ES_ML/Rule.php
Normal file
44
app/DataMapper/Tax/ES_ML/Rule.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper\Tax\ES_ML;
|
||||
|
||||
use App\DataMapper\Tax\DE\Rule as DERule;
|
||||
|
||||
class Rule extends DERule
|
||||
{
|
||||
/** @var string $seller_region */
|
||||
public string $seller_region = 'EU';
|
||||
|
||||
/** @var bool $consumer_tax_exempt */
|
||||
public bool $consumer_tax_exempt = false;
|
||||
|
||||
/** @var bool $business_tax_exempt */
|
||||
public bool $business_tax_exempt = false;
|
||||
|
||||
/** @var bool $eu_business_tax_exempt */
|
||||
public bool $eu_business_tax_exempt = true;
|
||||
|
||||
/** @var bool $foreign_business_tax_exempt */
|
||||
public bool $foreign_business_tax_exempt = false;
|
||||
|
||||
/** @var bool $foreign_consumer_tax_exempt */
|
||||
public bool $foreign_consumer_tax_exempt = false;
|
||||
|
||||
/** @var float $tax_rate */
|
||||
public float $tax_rate = 0;
|
||||
|
||||
/** @var float $reduced_tax_rate */
|
||||
public float $reduced_tax_rate = 0;
|
||||
|
||||
public string $tax_name1 = 'IGIC';
|
||||
|
||||
}
|
@ -17,7 +17,7 @@ class TaxModel
|
||||
public string $seller_subregion = 'CA';
|
||||
|
||||
/** @var string $version */
|
||||
public string $version = 'beta';
|
||||
public string $version = 'gamma';
|
||||
|
||||
/** @var object $regions */
|
||||
public object $regions;
|
||||
@ -48,8 +48,7 @@ class TaxModel
|
||||
public function migrate(): self
|
||||
{
|
||||
|
||||
if($this->version == 'alpha')
|
||||
{
|
||||
if($this->version == 'alpha') {
|
||||
$this->regions->EU->subregions->PL = new \stdClass();
|
||||
$this->regions->EU->subregions->PL->tax_rate = 23;
|
||||
$this->regions->EU->subregions->PL->tax_name = 'VAT';
|
||||
@ -59,6 +58,32 @@ class TaxModel
|
||||
$this->version = 'beta';
|
||||
}
|
||||
|
||||
if($this->version == 'beta') {
|
||||
|
||||
//CEUTA
|
||||
$this->regions->EU->subregions->{'ES-CE'} = new \stdClass();
|
||||
$this->regions->EU->subregions->{'ES-CE'}->tax_rate = 4;
|
||||
$this->regions->EU->subregions->{'ES-CE'}->tax_name = 'IGIC';
|
||||
$this->regions->EU->subregions->{'ES-CE'}->reduced_tax_rate = 4;
|
||||
$this->regions->EU->subregions->{'ES-CE'}->apply_tax = false;
|
||||
|
||||
//MELILLA ML 4
|
||||
$this->regions->EU->subregions->{'ES-ML'} = new \stdClass();
|
||||
$this->regions->EU->subregions->{'ES-ML'}->tax_rate = 4;
|
||||
$this->regions->EU->subregions->{'ES-ML'}->tax_name = 'IGIC';
|
||||
$this->regions->EU->subregions->{'ES-ML'}->reduced_tax_rate = 4;
|
||||
$this->regions->EU->subregions->{'ES-ML'}->apply_tax = false;
|
||||
|
||||
//CANARIAS CN 7/3
|
||||
$this->regions->EU->subregions->{'ES-CN'} = new \stdClass();
|
||||
$this->regions->EU->subregions->{'ES-CN'}->tax_rate = 7;
|
||||
$this->regions->EU->subregions->{'ES-CN'}->tax_name = 'IGIC';
|
||||
$this->regions->EU->subregions->{'ES-CN'}->reduced_tax_rate = 4;
|
||||
$this->regions->EU->subregions->{'ES-CN'}->apply_tax = false;
|
||||
|
||||
$this->version = 'gamma';
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -420,6 +445,25 @@ class TaxModel
|
||||
$this->regions->EU->subregions->ES->reduced_tax_rate = 10;
|
||||
$this->regions->EU->subregions->ES->apply_tax = false;
|
||||
|
||||
$this->regions->EU->subregions->{'ES-CE'} = new \stdClass();
|
||||
$this->regions->EU->subregions->{'ES-CE'}->tax_rate = 4;
|
||||
$this->regions->EU->subregions->{'ES-CE'}->tax_name = 'IGIC';
|
||||
$this->regions->EU->subregions->{'ES-CE'}->reduced_tax_rate = 4;
|
||||
$this->regions->EU->subregions->{'ES-CE'}->apply_tax = false;
|
||||
|
||||
$this->regions->EU->subregions->{'ES-ML'} = new \stdClass();
|
||||
$this->regions->EU->subregions->{'ES-ML'}->tax_rate = 4;
|
||||
$this->regions->EU->subregions->{'ES-ML'}->tax_name = 'IGIC';
|
||||
$this->regions->EU->subregions->{'ES-ML'}->reduced_tax_rate = 4;
|
||||
$this->regions->EU->subregions->{'ES-ML'}->apply_tax = false;
|
||||
|
||||
$this->regions->EU->subregions->{'ES-CN'} = new \stdClass();
|
||||
$this->regions->EU->subregions->{'ES-CN'}->tax_rate = 7;
|
||||
$this->regions->EU->subregions->{'ES-CN'}->tax_name = 'IGIC';
|
||||
$this->regions->EU->subregions->{'ES-CN'}->reduced_tax_rate = 3;
|
||||
$this->regions->EU->subregions->{'ES-CN'}->apply_tax = false;
|
||||
|
||||
|
||||
$this->regions->EU->subregions->FI = new \stdClass();
|
||||
$this->regions->EU->subregions->FI->tax_rate = 24;
|
||||
$this->regions->EU->subregions->FI->tax_name = 'ALV';
|
||||
|
@ -1,8 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataProviders;
|
||||
|
||||
final class CAProvinces {
|
||||
final class CAProvinces
|
||||
{
|
||||
/**
|
||||
* The provinces and territories of Canada
|
||||
*
|
||||
@ -30,7 +40,8 @@ final class CAProvinces {
|
||||
* @param string $abbreviation
|
||||
* @return string
|
||||
*/
|
||||
public static function getName($abbreviation) {
|
||||
public static function getName($abbreviation)
|
||||
{
|
||||
return self::$provinces[$abbreviation];
|
||||
}
|
||||
|
||||
@ -39,7 +50,8 @@ final class CAProvinces {
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get() {
|
||||
public static function get()
|
||||
{
|
||||
return self::$provinces;
|
||||
}
|
||||
|
||||
@ -49,7 +61,8 @@ final class CAProvinces {
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public static function getAbbreviation($name) {
|
||||
public static function getAbbreviation($name)
|
||||
{
|
||||
return array_search(ucwords($name), self::$provinces);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\DataProviders;
|
||||
|
||||
use Omnipay\Rotessa\Object\Frequency;
|
||||
|
||||
final class Frequencies
|
||||
{
|
||||
public static function get() : array {
|
||||
return Frequency::getTypes();
|
||||
}
|
||||
|
||||
public static function getFromType() {
|
||||
|
||||
}
|
||||
public static function getOnePayment() {
|
||||
return Frequency::ONCE;
|
||||
}
|
||||
}
|
22
app/Enum/HttpVerb.php
Normal file
22
app/Enum/HttpVerb.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum HttpVerb: string
|
||||
{
|
||||
case POST = 'post';
|
||||
case PUT = 'put';
|
||||
case GET = 'get';
|
||||
case PATCH = 'patch';
|
||||
case DELETE = 'delete';
|
||||
}
|
35
app/Events/Invoice/InvoiceAutoBillFailed.php
Normal file
35
app/Events/Invoice/InvoiceAutoBillFailed.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Events\Invoice;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Class InvoiceAutoBillFailed.
|
||||
*/
|
||||
class InvoiceAutoBillFailed
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
* @param Company $company
|
||||
* @param array $event_vars
|
||||
*/
|
||||
public function __construct(public Invoice $invoice, public Company $company, public array $event_vars, public ?string $notes)
|
||||
{
|
||||
}
|
||||
}
|
35
app/Events/Invoice/InvoiceAutoBillSuccess.php
Normal file
35
app/Events/Invoice/InvoiceAutoBillSuccess.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Events\Invoice;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Class InvoiceAutoBillSuccess.
|
||||
*/
|
||||
class InvoiceAutoBillSuccess
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
* @param Company $company
|
||||
* @param array $event_vars
|
||||
*/
|
||||
public function __construct(public Invoice $invoice, public Company $company, public array $event_vars)
|
||||
{
|
||||
}
|
||||
}
|
34
app/Exceptions/PeppolValidationException.php
Normal file
34
app/Exceptions/PeppolValidationException.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PeppolValidationException extends Exception
|
||||
{
|
||||
|
||||
protected string $field = '';
|
||||
|
||||
public function __construct($message, $field, $code = 0, Exception $previous = null)
|
||||
{
|
||||
// Store the custom data
|
||||
$this->field = $field;
|
||||
|
||||
// Ensure that everything is assigned properly by calling the parent constructor
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getInvalidField()
|
||||
{
|
||||
return $this->field;
|
||||
}
|
||||
}
|
@ -101,7 +101,10 @@ class ActivityExport extends BaseExport
|
||||
$t = app('translator');
|
||||
$t->replace(Ninja::transformTranslations($this->company->settings));
|
||||
|
||||
$this->date_format = DateFormat::find($this->company->settings->date_format_id)->format;
|
||||
/** @var \App\Models\DateFormat $df */
|
||||
$df = DateFormat::query()->find($this->company->settings->date_format_id);
|
||||
|
||||
$this->date_format = $df->format;
|
||||
|
||||
if (count($this->input['report_keys']) == 0) {
|
||||
$this->input['report_keys'] = array_values($this->entity_keys);
|
||||
@ -130,7 +133,7 @@ class ActivityExport extends BaseExport
|
||||
$query->cursor()
|
||||
->each(function ($entity) {
|
||||
|
||||
/** @var \App\Models\Activity $entity */
|
||||
/** @var \App\Models\Activity $entity */
|
||||
|
||||
$this->buildRow($entity);
|
||||
});
|
||||
|
@ -451,6 +451,7 @@ class BaseExport
|
||||
'project' => 'task.project_id',
|
||||
'billable' => 'task.billable',
|
||||
'item_notes' => 'task.item_notes',
|
||||
'time_log' => 'task.time_log',
|
||||
];
|
||||
|
||||
protected array $forced_client_fields = [
|
||||
@ -844,7 +845,7 @@ class BaseExport
|
||||
/**
|
||||
* Apply Product Filters
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
@ -869,7 +870,7 @@ class BaseExport
|
||||
/**
|
||||
* Add Client Filter
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param mixed $clients
|
||||
*
|
||||
* @return Builder
|
||||
@ -892,7 +893,7 @@ class BaseExport
|
||||
/**
|
||||
* Add Vendor Filter
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $vendors
|
||||
*
|
||||
* @return Builder
|
||||
@ -916,7 +917,7 @@ class BaseExport
|
||||
/**
|
||||
* AddProjectFilter
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $projects
|
||||
*
|
||||
* @return Builder
|
||||
@ -940,7 +941,7 @@ class BaseExport
|
||||
/**
|
||||
* Add Category Filter
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $expense_categories
|
||||
*
|
||||
* @return Builder
|
||||
@ -965,7 +966,7 @@ class BaseExport
|
||||
/**
|
||||
* Add Payment Status Filters
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $status
|
||||
*
|
||||
* @return Builder
|
||||
@ -1023,10 +1024,10 @@ class BaseExport
|
||||
/**
|
||||
* Add RecurringInvoice Status Filter
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $status
|
||||
*
|
||||
* @return Builder
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function addRecurringInvoiceStatusFilter(Builder $query, string $status): Builder
|
||||
{
|
||||
@ -1040,7 +1041,7 @@ class BaseExport
|
||||
|
||||
$recurring_filters = [];
|
||||
|
||||
if($this->company->getSetting('report_include_drafts')){
|
||||
if($this->company->getSetting('report_include_drafts')) {
|
||||
$recurring_filters[] = RecurringInvoice::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
@ -1066,7 +1067,7 @@ class BaseExport
|
||||
/**
|
||||
* Add QuoteStatus Filter
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $status
|
||||
*
|
||||
* @return Builder
|
||||
@ -1132,7 +1133,7 @@ class BaseExport
|
||||
/**
|
||||
* Add PurchaseOrder Status Filter
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $status
|
||||
*
|
||||
* @return Builder
|
||||
@ -1182,7 +1183,7 @@ class BaseExport
|
||||
/**
|
||||
* Add Invoice Status Filter
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $status
|
||||
* @return Builder
|
||||
*/
|
||||
@ -1248,7 +1249,7 @@ class BaseExport
|
||||
/**
|
||||
* Add Date Range
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param ?string $table_name
|
||||
* @return Builder
|
||||
*/
|
||||
|
@ -133,7 +133,7 @@ class ClientExport extends BaseExport
|
||||
$query->where('is_deleted', 0);
|
||||
}
|
||||
|
||||
$query = $this->addDateRange($query,' clients');
|
||||
$query = $this->addDateRange($query, ' clients');
|
||||
|
||||
if($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
@ -157,7 +157,7 @@ class ClientExport extends BaseExport
|
||||
$query->cursor()
|
||||
->each(function ($client) {
|
||||
|
||||
/** @var \App\Models\Client $client */
|
||||
/** @var \App\Models\Client $client */
|
||||
$this->csv->insertOne($this->buildRow($client));
|
||||
});
|
||||
|
||||
|
@ -101,7 +101,7 @@ class DocumentExport extends BaseExport
|
||||
|
||||
$query->cursor()
|
||||
->each(function ($entity) {
|
||||
/** @var mixed $entity */
|
||||
/** @var mixed $entity */
|
||||
$this->csv->insertOne($this->buildRow($entity));
|
||||
});
|
||||
|
||||
|
@ -269,8 +269,7 @@ class ExpenseExport extends BaseExport
|
||||
|
||||
if($expense->uses_inclusive_taxes) {
|
||||
$entity['expense.net_amount'] = round($expense->amount, $precision) - $total_tax_amount;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$entity['expense.net_amount'] = round($expense->amount, $precision);
|
||||
}
|
||||
|
||||
|
@ -229,10 +229,6 @@ class InvoiceItemExport extends BaseExport
|
||||
// $entity['currency'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code;
|
||||
// }
|
||||
|
||||
// if(array_key_exists('type', $entity)) {
|
||||
// $entity['type'] = $invoice->typeIdString($entity['type']);
|
||||
// }
|
||||
|
||||
// if(array_key_exists('tax_category', $entity)) {
|
||||
// $entity['tax_category'] = $invoice->taxTypeString($entity['tax_category']);
|
||||
// }
|
||||
|
@ -115,7 +115,7 @@ class PaymentExport extends BaseExport
|
||||
$query->cursor()
|
||||
->each(function ($entity) {
|
||||
|
||||
/** @var \App\Models\Payment $entity */
|
||||
/** @var \App\Models\Payment $entity */
|
||||
$this->csv->insertOne($this->buildRow($entity));
|
||||
});
|
||||
|
||||
|
@ -106,8 +106,8 @@ class ProductExport extends BaseExport
|
||||
$query->cursor()
|
||||
->each(function ($entity) {
|
||||
|
||||
/** @var \App\Models\Product $entity */
|
||||
$this->csv->insertOne($this->buildRow($entity));
|
||||
/** @var \App\Models\Product $entity */
|
||||
$this->csv->insertOne($this->buildRow($entity));
|
||||
});
|
||||
|
||||
return $this->csv->toString();
|
||||
|
@ -122,8 +122,8 @@ class PurchaseOrderExport extends BaseExport
|
||||
$query->cursor()
|
||||
->each(function ($purchase_order) {
|
||||
|
||||
/** @var \App\Models\PurchaseOrder $purchase_order */
|
||||
$this->csv->insertOne($this->buildRow($purchase_order));
|
||||
/** @var \App\Models\PurchaseOrder $purchase_order */
|
||||
$this->csv->insertOne($this->buildRow($purchase_order));
|
||||
});
|
||||
|
||||
return $this->csv->toString();
|
||||
|
@ -102,14 +102,14 @@ class PurchaseOrderItemExport extends BaseExport
|
||||
$query->cursor()
|
||||
->each(function ($resource) {
|
||||
|
||||
/** @var \App\Models\PurchaseOrder $resource */
|
||||
$this->iterateItems($resource);
|
||||
/** @var \App\Models\PurchaseOrder $resource */
|
||||
$this->iterateItems($resource);
|
||||
|
||||
foreach($this->storage_array as $row) {
|
||||
$this->storage_item_array[] = $this->processItemMetaData($row, $resource);
|
||||
}
|
||||
foreach($this->storage_array as $row) {
|
||||
$this->storage_item_array[] = $this->processItemMetaData($row, $resource);
|
||||
}
|
||||
|
||||
$this->storage_array = [];
|
||||
$this->storage_array = [];
|
||||
|
||||
});
|
||||
|
||||
@ -130,8 +130,8 @@ class PurchaseOrderItemExport extends BaseExport
|
||||
$query->cursor()
|
||||
->each(function ($purchase_order) {
|
||||
|
||||
/** @var \App\Models\PurchaseOrder $purchase_order */
|
||||
$this->iterateItems($purchase_order);
|
||||
/** @var \App\Models\PurchaseOrder $purchase_order */
|
||||
$this->iterateItems($purchase_order);
|
||||
});
|
||||
|
||||
$this->csv->insertAll($this->storage_array);
|
||||
@ -213,10 +213,6 @@ class PurchaseOrderItemExport extends BaseExport
|
||||
// $entity['currency'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code;
|
||||
// }
|
||||
|
||||
// if(array_key_exists('type', $entity)) {
|
||||
// $entity['type'] = $purchase_order->typeIdString($entity['type']);
|
||||
// }
|
||||
|
||||
// if(array_key_exists('tax_category', $entity)) {
|
||||
// $entity['tax_category'] = $purchase_order->taxTypeString($entity['tax_category']);
|
||||
// }
|
||||
|
@ -107,8 +107,8 @@ class TaskExport extends BaseExport
|
||||
$query->cursor()
|
||||
->each(function ($entity) {
|
||||
|
||||
/** @var \App\Models\Task $entity*/
|
||||
$this->buildRow($entity);
|
||||
/** @var \App\Models\Task $entity*/
|
||||
$this->buildRow($entity);
|
||||
});
|
||||
|
||||
$this->csv->insertAll($this->storage_array);
|
||||
@ -156,7 +156,7 @@ class TaskExport extends BaseExport
|
||||
$entity[$key] = $transformed_entity[$parts[1]];
|
||||
} elseif (array_key_exists($key, $transformed_entity)) {
|
||||
$entity[$key] = $transformed_entity[$key];
|
||||
} elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes'])) {
|
||||
} elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes', 'task.time_log'])) {
|
||||
$entity[$key] = '';
|
||||
} else {
|
||||
$entity[$key] = $this->decorator->transform($key, $task);
|
||||
@ -207,6 +207,9 @@ class TaskExport extends BaseExport
|
||||
$seconds = $task->calcDuration();
|
||||
$entity['task.duration'] = $seconds;
|
||||
$entity['task.duration_words'] = $seconds > 86400 ? CarbonInterval::seconds($seconds)->locale($this->company->locale())->cascade()->forHumans() : now()->startOfDay()->addSeconds($seconds)->format('H:i:s');
|
||||
|
||||
$entity['task.time_log'] = (isset($item[1]) && $item[1] != 0) ? $item[1] - $item[0] : ctrans('texts.is_running');
|
||||
|
||||
}
|
||||
|
||||
if (in_array('task.billable', $this->input['report_keys']) || in_array('billable', $this->input['report_keys'])) {
|
||||
|
@ -110,8 +110,8 @@ class VendorExport extends BaseExport
|
||||
$query->cursor()
|
||||
->each(function ($vendor) {
|
||||
|
||||
/** @var \App\Models\Vendor $vendor */
|
||||
$this->csv->insertOne($this->buildRow($vendor));
|
||||
/** @var \App\Models\Vendor $vendor */
|
||||
$this->csv->insertOne($this->buildRow($vendor));
|
||||
});
|
||||
|
||||
return $this->csv->toString();
|
||||
|
@ -16,7 +16,7 @@ use App\Models\Credit;
|
||||
|
||||
class CreditFactory
|
||||
{
|
||||
public static function create(int $company_id, int $user_id, object $settings = null, Client $client = null): Credit
|
||||
public static function create(int $company_id, int $user_id): Credit
|
||||
{
|
||||
$credit = new Credit();
|
||||
$credit->status_id = Credit::STATUS_DRAFT;
|
||||
|
@ -76,6 +76,26 @@ class InvoiceItemFactory
|
||||
$data[] = $item;
|
||||
}
|
||||
|
||||
|
||||
$item = self::create();
|
||||
$item->quantity = $faker->numberBetween(1, 10);
|
||||
$item->cost = $faker->randomFloat(2, 1, 1000);
|
||||
$item->line_total = $item->quantity * $item->cost;
|
||||
$item->is_amount_discount = true;
|
||||
$item->discount = $faker->numberBetween(1, 10);
|
||||
$item->notes = str_replace(['"',"'"], ['',""], $faker->realText(20));
|
||||
$item->product_key = $faker->word();
|
||||
// $item->custom_value1 = $faker->realText(10);
|
||||
// $item->custom_value2 = $faker->realText(10);
|
||||
// $item->custom_value3 = $faker->realText(10);
|
||||
// $item->custom_value4 = $faker->realText(10);
|
||||
$item->tax_name1 = 'GST';
|
||||
$item->tax_rate1 = 10.00;
|
||||
$item->type_id = '2';
|
||||
|
||||
$data[] = $item;
|
||||
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
@ -149,43 +149,43 @@ class RecurringExpenseToExpenseFactory
|
||||
}
|
||||
|
||||
// if (Str::contains($match, '|')) {
|
||||
$parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ]
|
||||
$parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ]
|
||||
|
||||
$left = substr($parts[0], 1); // 'MONTH'
|
||||
$right = substr($parts[1], 0, -1); // MONTH+2
|
||||
$left = substr($parts[0], 1); // 'MONTH'
|
||||
$right = substr($parts[1], 0, -1); // MONTH+2
|
||||
|
||||
// If left side is not part of replacements, skip.
|
||||
if (! array_key_exists($left, $replacements['ranges'])) {
|
||||
continue;
|
||||
}
|
||||
// If left side is not part of replacements, skip.
|
||||
if (! array_key_exists($left, $replacements['ranges'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$_left = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
|
||||
$_right = '';
|
||||
$_left = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
|
||||
$_right = '';
|
||||
|
||||
// If right side doesn't have any calculations, replace with raw ranges keyword.
|
||||
if (! Str::contains($right, ['-', '+', '/', '*'])) {
|
||||
$_right = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
|
||||
}
|
||||
// If right side doesn't have any calculations, replace with raw ranges keyword.
|
||||
if (! Str::contains($right, ['-', '+', '/', '*'])) {
|
||||
$_right = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
|
||||
}
|
||||
|
||||
// If right side contains one of math operations, calculate.
|
||||
if (Str::contains($right, ['+'])) {
|
||||
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $right, $_matches);
|
||||
// If right side contains one of math operations, calculate.
|
||||
if (Str::contains($right, ['+'])) {
|
||||
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $right, $_matches);
|
||||
|
||||
$_operation = array_shift($_matches)[0]; // + -
|
||||
$_operation = array_shift($_matches)[0]; // + -
|
||||
|
||||
$_value = explode($_operation, $right); // [MONTHYEAR, 4]
|
||||
$_value = explode($_operation, $right); // [MONTHYEAR, 4]
|
||||
|
||||
$_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y'); //@phpstan-ignore-line
|
||||
}
|
||||
$_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y'); //@phpstan-ignore-line
|
||||
}
|
||||
|
||||
$replacement = sprintf('%s to %s', $_left, $_right);
|
||||
$replacement = sprintf('%s to %s', $_left, $_right);
|
||||
|
||||
$value = preg_replace(
|
||||
sprintf('/%s/', preg_quote($match)),
|
||||
$replacement,
|
||||
$value,
|
||||
1
|
||||
);
|
||||
$value = preg_replace(
|
||||
sprintf('/%s/', preg_quote($match)),
|
||||
$replacement,
|
||||
$value,
|
||||
1
|
||||
);
|
||||
// }
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ class BankTransactionFilters extends QueryFilters
|
||||
*/
|
||||
public function client_status(string $value = ''): Builder
|
||||
{
|
||||
if (strlen($value) == 0) {
|
||||
if (strlen($value ?? '') == 0) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
@ -108,13 +108,24 @@ class BankTransactionFilters extends QueryFilters
|
||||
}
|
||||
|
||||
if (count($debit_or_withdrawal_array) >= 1) {
|
||||
$query->orWhereIn('base_type', $debit_or_withdrawal_array);
|
||||
$query->whereIn('base_type', $debit_or_withdrawal_array);
|
||||
}
|
||||
});
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
public function active_banks(string $value = ''): Builder
|
||||
{
|
||||
|
||||
if (strlen($value) == 0 || $value != 'true') {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
return $this->builder->whereHas('bank_integration', function ($query){
|
||||
$query->where('is_deleted', 0)->whereNull('deleted_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the list based on Bank Accounts.
|
||||
|
@ -98,7 +98,14 @@ class CreditFilters extends QueryFilters
|
||||
->orWhere('last_name', 'like', '%'.$filter.'%')
|
||||
->orWhere('email', 'like', '%'.$filter.'%');
|
||||
})
|
||||
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
|
||||
->orWhereRaw("
|
||||
JSON_UNQUOTE(JSON_EXTRACT(
|
||||
JSON_ARRAY(
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
|
||||
), '$[*]')
|
||||
) LIKE ?", ['%'.$filter.'%']);
|
||||
// ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -99,6 +99,12 @@ class ExpenseFilters extends QueryFilters
|
||||
});
|
||||
}
|
||||
|
||||
if (in_array('uninvoiced', $status_parameters)) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->whereNull('invoice_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (in_array('paid', $status_parameters)) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->whereNotNull('payment_date');
|
||||
@ -158,6 +164,19 @@ class ExpenseFilters extends QueryFilters
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
public function categories(string $categories = ''): Builder
|
||||
{
|
||||
$categories_exploded = explode(",", $categories);
|
||||
|
||||
if(empty($categories) || count(array_filter($categories_exploded)) == 0) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
$categories_keys = $this->transformKeys($categories_exploded);
|
||||
|
||||
return $this->builder->whereIn('category_id', $categories_keys);
|
||||
}
|
||||
|
||||
public function number(string $number = ''): Builder
|
||||
{
|
||||
if (strlen($number) == 0) {
|
||||
@ -205,6 +224,11 @@ class ExpenseFilters extends QueryFilters
|
||||
->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]);
|
||||
}
|
||||
|
||||
if ($sort_col[0] == 'payment_date' && in_array($sort_col[1], ['asc', 'desc'])) {
|
||||
return $this->builder
|
||||
->orderByRaw('ISNULL(payment_date), payment_date '. $sort_col[1]);
|
||||
}
|
||||
|
||||
if($sort_col[0] == 'number') {
|
||||
return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir);
|
||||
}
|
||||
|
@ -125,7 +125,14 @@ class InvoiceFilters extends QueryFilters
|
||||
->orWhere('last_name', 'like', '%'.$filter.'%')
|
||||
->orWhere('email', 'like', '%'.$filter.'%');
|
||||
})
|
||||
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
|
||||
->orWhereRaw("
|
||||
JSON_UNQUOTE(JSON_EXTRACT(
|
||||
JSON_ARRAY(
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
|
||||
), '$[*]')
|
||||
) LIKE ?", ['%'.$filter.'%']);
|
||||
// ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
|
||||
});
|
||||
}
|
||||
|
||||
@ -259,58 +266,6 @@ class InvoiceFilters extends QueryFilters
|
||||
return $this->builder->where('due_date', '>=', $date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by date range
|
||||
*
|
||||
* @param string $date_range
|
||||
* @return Builder
|
||||
*/
|
||||
public function date_range(string $date_range = ''): Builder
|
||||
{
|
||||
$parts = explode(",", $date_range);
|
||||
|
||||
if (count($parts) != 2) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$start_date = Carbon::parse($parts[0]);
|
||||
$end_date = Carbon::parse($parts[1]);
|
||||
|
||||
return $this->builder->whereBetween('date', [$start_date, $end_date]);
|
||||
} catch(\Exception $e) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by due date range
|
||||
*
|
||||
* @param string $date_range
|
||||
* @return Builder
|
||||
*/
|
||||
public function due_date_range(string $date_range = ''): Builder
|
||||
{
|
||||
$parts = explode(",", $date_range);
|
||||
|
||||
if (count($parts) != 2) {
|
||||
return $this->builder;
|
||||
}
|
||||
try {
|
||||
|
||||
$start_date = Carbon::parse($parts[0]);
|
||||
$end_date = Carbon::parse($parts[1]);
|
||||
|
||||
return $this->builder->whereBetween('due_date', [$start_date, $end_date]);
|
||||
} catch(\Exception $e) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sorts the list based on $sort.
|
||||
*
|
||||
|
@ -60,7 +60,7 @@ class ProjectFilters extends QueryFilters
|
||||
{
|
||||
$sort_col = explode('|', $sort);
|
||||
|
||||
if (!is_array($sort_col) || count($sort_col) != 2) {
|
||||
if (!is_array($sort_col) || count($sort_col) != 2 || !in_array($sort_col[0], \Illuminate\Support\Facades\Schema::getColumnListing('projects'))) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,14 @@ class PurchaseOrderFilters extends QueryFilters
|
||||
->orWhere('custom_value4', 'like', '%'.$filter.'%')
|
||||
->orWhereHas('vendor', function ($q) use ($filter) {
|
||||
$q->where('name', 'like', '%'.$filter.'%');
|
||||
});
|
||||
})
|
||||
->orWhereRaw("
|
||||
JSON_UNQUOTE(JSON_EXTRACT(
|
||||
JSON_ARRAY(
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
|
||||
), '$[*]')
|
||||
) LIKE ?", ['%'.$filter.'%']);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -331,4 +331,61 @@ abstract class QueryFilters
|
||||
->orderByRaw("{$this->with_property} = ? DESC", [$value])
|
||||
->company();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter by date range
|
||||
*
|
||||
* @param string $date_range
|
||||
* @return Builder
|
||||
*/
|
||||
public function date_range(string $date_range = ''): Builder
|
||||
{
|
||||
$parts = explode(",", $date_range);
|
||||
|
||||
if (count($parts) != 2 || !in_array('date', \Illuminate\Support\Facades\Schema::getColumnListing($this->builder->getModel()->getTable()))) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$start_date = Carbon::parse($parts[0]);
|
||||
$end_date = Carbon::parse($parts[1]);
|
||||
|
||||
return $this->builder->whereBetween('date', [$start_date, $end_date]);
|
||||
} catch(\Exception $e) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by due date range
|
||||
*
|
||||
* @param string $date_range
|
||||
* @return Builder
|
||||
*/
|
||||
public function due_date_range(string $date_range = ''): Builder
|
||||
{
|
||||
|
||||
$parts = explode(",", $date_range);
|
||||
|
||||
if (count($parts) != 2 || !in_array('due_date', \Illuminate\Support\Facades\Schema::getColumnListing($this->builder->getModel()->getTable()))) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$start_date = Carbon::parse($parts[0]);
|
||||
$end_date = Carbon::parse($parts[1]);
|
||||
|
||||
return $this->builder->whereBetween('due_date', [$start_date, $end_date]);
|
||||
} catch(\Exception $e) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -46,7 +46,14 @@ class QuoteFilters extends QueryFilters
|
||||
->orWhere('last_name', 'like', '%'.$filter.'%')
|
||||
->orWhere('email', 'like', '%'.$filter.'%');
|
||||
})
|
||||
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
|
||||
->orWhereRaw("
|
||||
JSON_UNQUOTE(JSON_EXTRACT(
|
||||
JSON_ARRAY(
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
|
||||
), '$[*]')
|
||||
) LIKE ?", ['%'.$filter.'%']);
|
||||
// ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,14 @@ class RecurringInvoiceFilters extends QueryFilters
|
||||
->orWhere('last_name', 'like', '%'.$filter.'%')
|
||||
->orWhere('email', 'like', '%'.$filter.'%');
|
||||
})
|
||||
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
|
||||
->orWhereRaw("
|
||||
JSON_UNQUOTE(JSON_EXTRACT(
|
||||
JSON_ARRAY(
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
|
||||
), '$[*]')
|
||||
) LIKE ?", ['%'.$filter.'%']);
|
||||
//->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
|
||||
});
|
||||
}
|
||||
|
||||
@ -134,7 +141,7 @@ class RecurringInvoiceFilters extends QueryFilters
|
||||
return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir);
|
||||
}
|
||||
|
||||
if($sort_col[0] == 'status_id'){
|
||||
if($sort_col[0] == 'status_id') {
|
||||
return $this->builder->orderBy('status_id', $dir)->orderBy('last_sent_date', $dir);
|
||||
}
|
||||
|
||||
|
@ -85,6 +85,10 @@ class TaskFilters extends QueryFilters
|
||||
$this->builder->whereNull('invoice_id');
|
||||
}
|
||||
|
||||
if (in_array('is_running', $status_parameters)) {
|
||||
$this->builder->where('is_running', true);
|
||||
}
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
|
@ -99,11 +99,12 @@ class TransactionTransformer implements BankRevenueInterface
|
||||
} elseif (array_key_exists('internalTransactionId', $transaction)) {
|
||||
$transactionId = $transaction["internalTransactionId"];
|
||||
} else {
|
||||
nlog(`Invalid Input for nordigen transaction transformer: ` . $transaction);
|
||||
nlog('Invalid Input for nordigen transaction transformer: ' . $transaction);
|
||||
throw new \Exception('invalid dataset: missing transactionId - Please report this error to the developer');
|
||||
}
|
||||
|
||||
$amount = (float) $transaction["transactionAmount"]["amount"];
|
||||
$base_type = $amount < 0 ? 'DEBIT' : 'CREDIT';
|
||||
|
||||
// description could be in varios places
|
||||
$description = '';
|
||||
@ -140,7 +141,7 @@ class TransactionTransformer implements BankRevenueInterface
|
||||
return [
|
||||
'transaction_id' => 0,
|
||||
'nordigen_transaction_id' => $transactionId,
|
||||
'amount' => $amount,
|
||||
'amount' => abs($amount),
|
||||
'currency_id' => $this->convertCurrency($transaction["transactionAmount"]["currency"]),
|
||||
'category_id' => null,
|
||||
'category_type' => array_key_exists('additionalInformation', $transaction) ? $transaction["additionalInformation"] : '',
|
||||
@ -148,7 +149,7 @@ class TransactionTransformer implements BankRevenueInterface
|
||||
'description' => $description,
|
||||
'participant' => $participant,
|
||||
'participant_name' => $participant_name,
|
||||
'base_type' => $amount < 0 ? 'DEBIT' : 'CREDIT',
|
||||
'base_type' => $base_type,
|
||||
];
|
||||
|
||||
}
|
||||
|
@ -42,6 +42,9 @@ class InvoiceItemSum
|
||||
'DK', // Denmark
|
||||
'EE', // Estonia
|
||||
'ES', // Spain
|
||||
'ES-CE',
|
||||
'ES-CN',
|
||||
'ES-ML',
|
||||
'FI', // Finland
|
||||
'FR', // France
|
||||
'GR', // Greece
|
||||
@ -72,6 +75,9 @@ class InvoiceItemSum
|
||||
'DK', // Denmark
|
||||
'EE', // Estonia
|
||||
'ES', // Spain
|
||||
'ES-CE',
|
||||
'ES-CN',
|
||||
'ES-ML',
|
||||
'FI', // Finland
|
||||
'FR', // France
|
||||
'GR', // Greece
|
||||
@ -182,7 +188,7 @@ class InvoiceItemSum
|
||||
|
||||
|
||||
/** @var \App\DataMapper\Tax\BaseRule $class */
|
||||
$class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule";
|
||||
$class = "App\DataMapper\Tax\\".str_replace("-","_",$this->client->company->country()->iso_3166_2)."\\Rule";
|
||||
|
||||
$this->rule = new $class();
|
||||
|
||||
|
@ -23,12 +23,12 @@ class ProRata
|
||||
* the time interval and the frequency duration
|
||||
*
|
||||
* @param float $amount
|
||||
* @param Carbon $from_date
|
||||
* @param Carbon $to_date
|
||||
* @param \Illuminate\Support\Carbon | \Carbon\Carbon $from_date
|
||||
* @param \Illuminate\Support\Carbon | \Carbon\Carbon $to_date
|
||||
* @param int $frequency
|
||||
* @return float
|
||||
*/
|
||||
public function refund(float $amount, Carbon $from_date, Carbon $to_date, int $frequency): float
|
||||
public function refund(float $amount, $from_date, $to_date, int $frequency): float
|
||||
{
|
||||
$days = intval(abs($from_date->copy()->diffInDays($to_date)));
|
||||
$days_in_frequency = $this->getDaysInFrequency($frequency);
|
||||
@ -41,12 +41,12 @@ class ProRata
|
||||
* the time interval and the frequency duration
|
||||
*
|
||||
* @param float $amount
|
||||
* @param Carbon $from_date
|
||||
* @param Carbon $to_date
|
||||
* @param \Illuminate\Support\Carbon | \Carbon\Carbon $from_date
|
||||
* @param \Illuminate\Support\Carbon | \Carbon\Carbon $to_date
|
||||
* @param int $frequency
|
||||
* @return float
|
||||
*/
|
||||
public function charge(float $amount, Carbon $from_date, Carbon $to_date, int $frequency): float
|
||||
public function charge(float $amount, $from_date, $to_date, int $frequency): float
|
||||
{
|
||||
$days = intval(abs($from_date->copy()->diffInDays($to_date)));
|
||||
$days_in_frequency = $this->getDaysInFrequency($frequency);
|
||||
|
28
app/Helpers/Sanitizer.php
Normal file
28
app/Helpers/Sanitizer.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class Sanitizer
|
||||
{
|
||||
|
||||
public static function removeBlanks($input): array
|
||||
{
|
||||
foreach ($input as &$value) {
|
||||
if (is_array($value)) {
|
||||
// Recursively apply the filter to nested arrays
|
||||
$value = self::removeBlanks($value);
|
||||
}
|
||||
}
|
||||
// Use array_filter to remove empty or null values
|
||||
return array_filter($input);
|
||||
}
|
||||
}
|
@ -22,5 +22,5 @@
|
||||
*/
|
||||
function ctrans(string $string, $replace = [], $locale = null): string
|
||||
{
|
||||
return trans($string, $replace, $locale);
|
||||
return html_entity_decode(trans($string, $replace, $locale));
|
||||
}
|
||||
|
@ -254,17 +254,20 @@ class ActivityController extends BaseController
|
||||
$activity->client_id = $entity->client_id;
|
||||
$activity->project_id = $entity->project_id;
|
||||
$activity->vendor_id = $entity->vendor_id;
|
||||
// no break
|
||||
case Task::class:
|
||||
$activity->task_id = $entity->id;
|
||||
$activity->expense_id = $entity->id;
|
||||
$activity->client_id = $entity->client_id;
|
||||
$activity->project_id = $entity->project_id;
|
||||
$activity->vendor_id = $entity->vendor_id;
|
||||
// no break
|
||||
case Payment::class:
|
||||
$activity->payment_id = $entity->id;
|
||||
$activity->expense_id = $entity->id;
|
||||
$activity->client_id = $entity->client_id;
|
||||
$activity->project_id = $entity->project_id;
|
||||
// no break
|
||||
default:
|
||||
# code...
|
||||
break;
|
||||
|
@ -41,8 +41,9 @@ class ContactLoginController extends Controller
|
||||
$company = false;
|
||||
$account = false;
|
||||
|
||||
if($request->query('intended'))
|
||||
if($request->query('intended')) {
|
||||
$request->session()->put('url.intended', $request->query('intended'));
|
||||
}
|
||||
|
||||
if ($request->session()->has('company_key')) {
|
||||
MultiDB::findAndSetDbByCompanyKey($request->session()->get('company_key'));
|
||||
@ -142,8 +143,9 @@ class ContactLoginController extends Controller
|
||||
|
||||
$this->setRedirectPath();
|
||||
|
||||
if($intended)
|
||||
if($intended) {
|
||||
$this->redirectTo = $intended;
|
||||
}
|
||||
|
||||
return $request->wantsJson()
|
||||
? new JsonResponse([], 204)
|
||||
|
@ -934,7 +934,7 @@ class BaseController extends Controller
|
||||
} elseif (in_array($this->entity_type, [Design::class, GroupSetting::class, PaymentTerm::class, TaskStatus::class])) {
|
||||
// nlog($this->entity_type);
|
||||
} else {
|
||||
$query->where(function ($q) use ($user){ //grouping these together improves query performance significantly)
|
||||
$query->where(function ($q) use ($user) { //grouping these together improves query performance significantly)
|
||||
$q->where('user_id', '=', $user->id)->orWhere('assigned_user_id', $user->id);
|
||||
});
|
||||
}
|
||||
@ -996,7 +996,7 @@ class BaseController extends Controller
|
||||
|
||||
if(request()->has('einvoice')) {
|
||||
|
||||
if(class_exists(Schema::class)){
|
||||
if(class_exists(Schema::class)) {
|
||||
$ro = new Schema();
|
||||
$response_data['einvoice_schema'] = $ro('Peppol');
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\Brevo\ProcessBrevoInboundWebhook;
|
||||
use App\Jobs\Brevo\ProcessBrevoWebhook;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -19,20 +20,19 @@ use Illuminate\Http\Request;
|
||||
*/
|
||||
class BrevoController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Postmark Webhook.
|
||||
* Process Brevo Webhook.
|
||||
*
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/postmark_webhook",
|
||||
* operationId="postmarkWebhook",
|
||||
* tags={"postmark"},
|
||||
* summary="Processing webhooks from PostMark",
|
||||
* path="/api/v1/brevo_webhook",
|
||||
* operationId="brevoWebhook",
|
||||
* tags={"brevo"},
|
||||
* summary="Processing webhooks from Brevo",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
@ -60,12 +60,150 @@ class BrevoController extends BaseController
|
||||
*/
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
if ($request->has('token') && $request->get('token') == config('services.brevo.key')) {
|
||||
ProcessBrevoWebhook::dispatch($request->all())->delay(10);
|
||||
if ($request->has('token') && $request->get('token') == config('services.brevo.secret')) {
|
||||
ProcessBrevoWebhook::dispatch($request->all())->delay(rand(2, 10));
|
||||
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process Brevo Inbound Webhook.
|
||||
*
|
||||
* IMPORTANT NOTICE: brevo strips old sended emails, therefore only current attachements are present
|
||||
*
|
||||
* IMPORTANT NOTICE: brevo saves the message and attachemnts for later retrieval, therefore we can process it within a async job for performance reasons
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/brevo_inbound_webhook",
|
||||
* operationId="brevoInboundWebhook",
|
||||
* tags={"brevo"},
|
||||
* summary="Processing inbound webhooks from Brevo",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the saved credit object",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Credit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*
|
||||
* array (
|
||||
* 'items' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Uuid' =>
|
||||
* array (
|
||||
* 0 => 'd9f48d52-a344-42a4-9056-9733488d9fa3',
|
||||
* ),
|
||||
* 'Recipients' =>
|
||||
* array (
|
||||
* 0 => 'test@test.de',
|
||||
* ),
|
||||
* 'MessageId' => '<CADfEuNvumhUdqAUa0j6MxzVp0ooMYqdb_KZ7nZqHNAfdDqwWEQ@mail.gmail.com>',
|
||||
* 'InReplyTo' => NULL,
|
||||
* 'From' =>
|
||||
* array (
|
||||
* 'Name' => 'Max Mustermann',
|
||||
* 'Address' => 'max@mustermann.de',
|
||||
* ),
|
||||
* 'To' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Name' => NULL,
|
||||
* 'Address' => 'test@test.de',
|
||||
* ),
|
||||
* ),
|
||||
* 'Cc' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'Bcc' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'ReplyTo' => NULL,
|
||||
* 'SentAtDate' => 'Sat, 23 Mar 2024 18:18:20 +0100',
|
||||
* 'Subject' => 'TEST',
|
||||
* 'Attachments' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Name' => 'flag--sv-1x1.svg',
|
||||
* 'ContentType' => 'image/svg+xml',
|
||||
* 'ContentLength' => 79957,
|
||||
* 'ContentID' => 'f_lu4ct6s20',
|
||||
* 'DownloadToken' => 'eyJmb2xkZXIiOiIyMDI0MDMyMzE3MTgzNi45OS43OTgwMDM4MDQiLCJmaWxlbmFtZSI6ImZsYWctLXN2LTF4MS5zdmcifQ',
|
||||
* ),
|
||||
* ),
|
||||
* 'Headers' =>
|
||||
* array (
|
||||
* 'Received' => 'by mail-ed1-f51.google.com with SMTP id 4fb4d7f45d1cf-56b0af675deso3877288a12.1 for <test@test.de>; Sat, 23 Mar 2024 10:18:36 -0700 (PDT)',
|
||||
* 'DKIM-Signature' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=mustermann.de; s=google; t=1711214316; x=1711819116; darn=test.de; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=eBSl5M0zvmTd+dFXGXMMSWrQ4nCvUdyVx+1Xpl+YuX8=; b=ackw3d+qTvZk4JKxomvH626MvfwmH23mikOUc2hWwYiO6unmQgPs2w5spnkmD9aCZ9 G+3nPSYKntugOmqWstZH3z4B063U4Y6j5hTc19WtCyyb9UR+XD+C6L10yc6ez8QUhlZT uAGqDoJ+E8+dBxiMul2pow19lC88t3QxRXU+i8zScniV7SFkwzziCEODaB61yI0DXsZB bUkx5Gx6cztKaNVF2QgguF2nQnJFUnD2nabVFsihyJ5r6y61rkSM/YTfMJuES772lnhv IeF+vwiFNEPKafrchce6YJcvo5Vd5lYFK4LtHyCy3mwJpX2QY+WnWAfferZ2YfgEL0Sf K3Pw==',
|
||||
* 'X-Google-DKIM-Signature' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1711214316; x=1711819116; h=to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=eBSl5M0zvmTd+dFXGXMMSWrQ4nCvUdyVx+1Xpl+YuX8=; b=fg4tXZnstRBexYlC6MD7C7is0kQj+xY66cSJ78tSa7PtSFQzY0zajDMsepMCGiiWmN /Pc/tRtk53pru/OtfzRT9pbM6mhM1arIt+QaQBQGU5xZVV5JXfPmdnPzXqAbQztyeHrk UcEkz+qDN3JNoidw2dJhhdt5MxdKssR572NwtBrn/rN7f1o/ThWzEz+P0o06GVBpxVYP wM0EkvcJj2SUOcn36kmp1ccbMUwYCU2h1JmniEFY8RTqu2il13iXoBvG4YPxe0c0hJ6z zw1N5rONeQM113N1rpbQzS1QLSngczuOhN24M3TOwrHJIec/BxrOW6KWl/uPUqiZAf65 f0tg==',
|
||||
* 'X-Gm-Message-State' => 'AOJu0YzKhR1HY1oUXoq++LLpl6UOz1S60NfPxuPXBLcP+6aACYle8rqQ fYHe2rQYTpg4KWiOswu858STOW8qmiewXD6gH/LbmEFs7sknRyDPNr/+L0cv828A3o+SOvXu3uP SY6H1aNSwIpqTRhJ+nNjTuSUpuSoABd9fYXFwPuivV0DtBhoVmpE=',
|
||||
* 'X-Google-Smtp-Source' => 'AGHT+IHdA9ZhW0dQxgOYx2OXBGmu4pzSR/zwJ0vcPNXFSqttKCPS2oTw1a9b2mMdhyUeoRAwP5TmhHlAtqUUrOPwkgg=',
|
||||
* 'X-Received' => 'by 2002:a50:d74c:0:b0:567:3c07:8bbc with SMTP id i12-20020a50d74c000000b005673c078bbcmr2126401edj.21.1711214316135; Sat, 23 Mar 2024 10:18:36 -0700 (PDT)',
|
||||
* 'MIME-Version' => '1.0',
|
||||
* 'From' => 'Max Mustermann <max@mustermann.de>',
|
||||
* 'Date' => 'Sat, 23 Mar 2024 18:18:20 +0100',
|
||||
* 'Message-ID' => '<CADfEuNvumhUdqAUa0j6MxzVp0ooMYqdb_KZ7nZqHNAfdDqwWEQ@mail.gmail.com>',
|
||||
* 'Subject' => 'TEST',
|
||||
* 'To' => 'test@test.de',
|
||||
* 'Content-Type' => 'multipart/mixed',
|
||||
* ),
|
||||
* 'SpamScore' => 2.8,
|
||||
* 'ExtractedMarkdownMessage' => 'TEST',
|
||||
* 'ExtractedMarkdownSignature' => NULL,
|
||||
* 'RawHtmlBody' => '<div dir="ltr">TEST</div>',
|
||||
* 'RawTextBody' => 'TEST',
|
||||
* 'EMLDownloadToken' => 'eyJmb2xkZXIiOiIyMDI0MDMyMzE3MTgzNi45OS43OTgwMDM4MDQiLCJmaWxlbmFtZSI6InNtdHAuZW1sIn0',
|
||||
* ),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function inboundWebhook(Request $request)
|
||||
{
|
||||
$input = $request->all();
|
||||
|
||||
if (!($request->has('token') && $request->get('token') == config('ninja.inbound_mailbox.inbound_webhook_token')))
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
|
||||
if (!array_key_exists('items', $input)) {
|
||||
nlog('Failed: Message could not be parsed, because required parameters are missing.');
|
||||
return response()->json(['message' => 'Failed. Invalid Parameters.'], 400);
|
||||
}
|
||||
|
||||
foreach ($input["items"] as $item) {
|
||||
|
||||
if (!array_key_exists('Recipients', $item) || !array_key_exists('MessageId', $item)) {
|
||||
nlog('Failed: Message could not be parsed, because required parameters are missing. At least one item was invalid.');
|
||||
return response()->json(['message' => 'Failed. Invalid Parameters. At least one item was invalid.'], 400);
|
||||
}
|
||||
|
||||
ProcessBrevoInboundWebhook::dispatch($item)->delay(rand(2, 10));
|
||||
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Success'], 201);
|
||||
}
|
||||
}
|
||||
|
@ -115,8 +115,9 @@ class InvitationController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
if(!auth()->guard('contact')->check()){
|
||||
if(!auth()->guard('contact')->check()) {
|
||||
$this->middleware('auth:contact');
|
||||
/** @var \App\Models\InvoiceInvitation | \App\Models\QuoteInvitation | \App\Models\CreditInvitation | \App\Models\RecurringInvoiceInvitation $invitation */
|
||||
return redirect()->route('client.login', ['intended' => route('client.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->{$key}), 'silent' => $is_silent])]);
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,7 @@ class InvoiceController extends Controller
|
||||
|
||||
$invitation = $invoice->invitations()->where('client_contact_id', auth()->guard('contact')->user()->id)->first();
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
if ($invitation && auth()->guard('contact') && ! session()->get('is_silent') && ! $invitation->viewed_date) {
|
||||
$invitation->markViewed();
|
||||
|
||||
@ -77,13 +78,20 @@ class InvoiceController extends Controller
|
||||
'key' => $invitation ? $invitation->key : false,
|
||||
'hash' => $hash,
|
||||
'variables' => $variables,
|
||||
'invoices' => [$invoice->hashed_id],
|
||||
'db' => $invoice->company->db,
|
||||
];
|
||||
|
||||
if ($request->query('mode') === 'fullscreen') {
|
||||
return render('invoices.show-fullscreen', $data);
|
||||
}
|
||||
|
||||
return $this->render('invoices.show', $data);
|
||||
if(!$invoice->isPayable())
|
||||
return $this->render('invoices.show',$data);
|
||||
|
||||
return auth()->guard('contact')->user()->client->getSetting('payment_flow') == 'default' ? $this->render('invoices.show', $data) : $this->render('invoices.show_smooth', $data);
|
||||
|
||||
// return $this->render('invoices.show_smooth', $data);
|
||||
}
|
||||
|
||||
public function showBlob($hash)
|
||||
@ -235,9 +243,12 @@ class InvoiceController extends Controller
|
||||
'hashed_ids' => $invoices->pluck('hashed_id'),
|
||||
'total' => $total,
|
||||
'variables' => $variables,
|
||||
'invitation' => $invitation,
|
||||
'db' => $invitation->company->db,
|
||||
];
|
||||
|
||||
return $this->render('invoices.payment', $data);
|
||||
// return $this->render('invoices.payment', $data);
|
||||
return auth()->guard('contact')->user()->client->getSetting('payment_flow') === 'default' ? $this->render('invoices.payment', $data) : $this->render('invoices.show_smooth_multi', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,6 +88,8 @@ class NinjaPlanController extends Controller
|
||||
{
|
||||
$trial_started = "Trial Started @ ".now()->format('Y-m-d H:i:s');
|
||||
|
||||
auth()->guard('contact')->user()->fill($request->only(['first_name','last_name']))->save();
|
||||
|
||||
$client = auth()->guard('contact')->user()->client;
|
||||
$client->private_notes = $trial_started;
|
||||
$client->fill($request->all());
|
||||
|
@ -108,12 +108,6 @@ class PaymentController extends Controller
|
||||
*/
|
||||
public function process(Request $request)
|
||||
{
|
||||
// $request->validate([
|
||||
// 'contact_first_name' => ['required'],
|
||||
// 'contact_last_name' => ['required'],
|
||||
// 'contact_email' => ['required', 'email'],
|
||||
// ]);
|
||||
|
||||
return (new InstantPayment($request))->run();
|
||||
}
|
||||
|
||||
@ -123,13 +117,7 @@ class PaymentController extends Controller
|
||||
$gateway = CompanyGateway::findOrFail($request->input('company_gateway_id'));
|
||||
$payment_hash = PaymentHash::with('fee_invoice')->where('hash', $request->payment_hash)->firstOrFail();
|
||||
|
||||
// if($payment_hash)
|
||||
$invoice = $payment_hash->fee_invoice;
|
||||
// else
|
||||
// $invoice = Invoice::with('client')->where('id',$payment_hash->fee_invoice_id)->orderBy('id','desc')->first();
|
||||
|
||||
// $invoice = Invoice::with('client')->find($payment_hash->fee_invoice_id);
|
||||
|
||||
|
||||
$client = $invoice ? $invoice->client : auth()->guard('contact')->user()->client;
|
||||
|
||||
|
@ -56,8 +56,8 @@ class PaymentMethodController extends Controller
|
||||
|
||||
$data['gateway'] = $gateway;
|
||||
|
||||
/** @var \App\Models\ClientContact auth()->user() **/
|
||||
$client_contact = auth()->user();
|
||||
/** @var \App\Models\ClientContact auth()->guard('contact')->user() **/
|
||||
$client_contact = auth()->guard('contact')->user();
|
||||
$data['client'] = $client_contact->client;
|
||||
|
||||
return $gateway
|
||||
@ -77,8 +77,8 @@ class PaymentMethodController extends Controller
|
||||
{
|
||||
$gateway = $this->getClientGateway();
|
||||
|
||||
/** @var \App\Models\ClientContact auth()->user() **/
|
||||
$client_contact = auth()->user();
|
||||
/** @var \App\Models\ClientContact auth()->guard('contact')->user() **/
|
||||
$client_contact = auth()->guard('contact')->user();
|
||||
|
||||
return $gateway
|
||||
->driver($client_contact->client)
|
||||
@ -103,8 +103,8 @@ class PaymentMethodController extends Controller
|
||||
public function verify(ClientGatewayToken $payment_method)
|
||||
{
|
||||
|
||||
/** @var \App\Models\ClientContact auth()->user() **/
|
||||
$client_contact = auth()->user();
|
||||
/** @var \App\Models\ClientContact auth()->guard('contact')->user() **/
|
||||
$client_contact = auth()->guard('contact')->user();
|
||||
|
||||
return $payment_method->gateway
|
||||
->driver($client_contact->client)
|
||||
@ -114,8 +114,8 @@ class PaymentMethodController extends Controller
|
||||
|
||||
public function processVerification(Request $request, ClientGatewaytoken $payment_method)
|
||||
{
|
||||
/** @var \App\Models\ClientContact auth()->user() **/
|
||||
$client_contact = auth()->user();
|
||||
/** @var \App\Models\ClientContact auth()->guard('contact')->user() **/
|
||||
$client_contact = auth()->guard('contact')->user();
|
||||
|
||||
return $payment_method->gateway
|
||||
->driver($client_contact->client)
|
||||
@ -131,8 +131,8 @@ class PaymentMethodController extends Controller
|
||||
*/
|
||||
public function destroy(ClientGatewayToken $payment_method)
|
||||
{
|
||||
/** @var \App\Models\ClientContact auth()->user() **/
|
||||
$client_contact = auth()->user();
|
||||
/** @var \App\Models\ClientContact auth()->guard('contact')->user() **/
|
||||
$client_contact = auth()->guard('contact')->user();
|
||||
|
||||
if ($payment_method->gateway()->exists()) {
|
||||
$payment_method->gateway
|
||||
@ -145,9 +145,19 @@ class PaymentMethodController extends Controller
|
||||
event(new MethodDeleted($payment_method, auth()->guard('contact')->user()->company, Ninja::eventVars(auth()->guard('contact')->user()->id)));
|
||||
|
||||
$payment_method->is_deleted = true;
|
||||
$payment_method->is_default = false;
|
||||
$payment_method->delete();
|
||||
$payment_method->save();
|
||||
|
||||
|
||||
$def_cgt = auth()->guard('contact')->user()->client->gateway_tokens()->orderBy('id','desc')->first();
|
||||
|
||||
if($def_cgt)
|
||||
{
|
||||
$def_cgt->is_default = true;
|
||||
$def_cgt->save();
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
nlog($e->getMessage());
|
||||
|
||||
@ -161,8 +171,8 @@ class PaymentMethodController extends Controller
|
||||
|
||||
private function getClientGateway()
|
||||
{
|
||||
/** @var \App\Models\ClientContact auth()->user() **/
|
||||
$client_contact = auth()->user();
|
||||
/** @var \App\Models\ClientContact auth()->guard('contact')->user() **/
|
||||
$client_contact = auth()->guard('contact')->user();
|
||||
|
||||
if (request()->query('method') == GatewayType::CREDIT_CARD) {
|
||||
return $client_contact->client->getCreditCardGateway();
|
||||
|
@ -35,11 +35,16 @@ class PrePaymentController extends Controller
|
||||
/**
|
||||
* Show the list of payments.
|
||||
*
|
||||
* @return Factory|View
|
||||
* @return Factory|View|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
|
||||
$client = auth()->guard('contact')->user()->client;
|
||||
|
||||
if(!$client->getSetting('client_initiated_payments'))
|
||||
return redirect()->route('client.dashboard');
|
||||
|
||||
$minimum = $client->getSetting('client_initiated_payments_minimum');
|
||||
$minimum_amount = $minimum == 0 ? "" : Number::formatMoney($minimum, $client);
|
||||
|
||||
|
@ -150,6 +150,10 @@ class DocumentController extends BaseController
|
||||
$document->fill($request->all());
|
||||
$document->save();
|
||||
|
||||
if($document->documentable) { //@phpstan-ignore-line
|
||||
$document->documentable->touch();
|
||||
}
|
||||
|
||||
return $this->itemResponse($document->fresh());
|
||||
}
|
||||
|
||||
|
26
app/Http/Controllers/EInvoice/SelfhostController.php
Normal file
26
app/Http/Controllers/EInvoice/SelfhostController.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\EInvoice;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\EInvoice\SignupRequest;
|
||||
|
||||
class SelfhostController extends Controller
|
||||
{
|
||||
|
||||
public function index(SignupRequest $request)
|
||||
{
|
||||
return view('einvoice.index');
|
||||
}
|
||||
|
||||
}
|
39
app/Http/Controllers/EInvoiceController.php
Normal file
39
app/Http/Controllers/EInvoiceController.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\EInvoice\ValidateEInvoiceRequest;
|
||||
use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel;
|
||||
|
||||
class EInvoiceController extends BaseController
|
||||
{
|
||||
|
||||
public function validateEntity(ValidateEInvoiceRequest $request)
|
||||
{
|
||||
$el = new EntityLevel();
|
||||
|
||||
$data = [];
|
||||
|
||||
match($request->entity){
|
||||
'invoices' => $data = $el->checkInvoice($request->getEntity()),
|
||||
'clients' => $data = $el->checkClient($request->getEntity()),
|
||||
'companies' => $data = $el->checkCompany($request->getEntity()),
|
||||
default => $data['passes'] = false,
|
||||
};
|
||||
|
||||
nlog($data);
|
||||
|
||||
return response()->json($data, $data['passes'] ? 200 : 400);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -51,9 +51,9 @@ class EmailHistoryController extends BaseController
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
|
||||
$data = SystemLog::where('company_id', $user->company()->id)
|
||||
->where('category_id', SystemLog::CATEGORY_MAIL)
|
||||
->whereJsonContains('log->history->entity', $request->entity)
|
||||
->whereJsonContains('log->history->entity_id', $this->encodePrimaryKey($request->entity_id))
|
||||
->orderBy('id', 'DESC')
|
||||
->cursor()
|
||||
|
@ -497,7 +497,7 @@ class ExpenseController extends BaseController
|
||||
|
||||
$expenses = Expense::withTrashed()->find($request->ids);
|
||||
|
||||
if($request->action == 'bulk_categorize' && $user->can('edit', $expenses->first())) {
|
||||
if ($request->action == 'bulk_categorize' && $user->can('edit', $expenses->first())) {
|
||||
$this->expense_repo->categorize($expenses, $request->category_id);
|
||||
$expenses = collect([]);
|
||||
}
|
||||
@ -573,7 +573,7 @@ class ExpenseController extends BaseController
|
||||
*/
|
||||
public function upload(UploadExpenseRequest $request, Expense $expense)
|
||||
{
|
||||
if (! $this->checkFeature(Account::FEATURE_DOCUMENTS)) {
|
||||
if (!$this->checkFeature(Account::FEATURE_DOCUMENTS)) {
|
||||
return $this->featureFailure();
|
||||
}
|
||||
|
||||
@ -584,14 +584,60 @@ class ExpenseController extends BaseController
|
||||
return $this->itemResponse($expense->fresh());
|
||||
}
|
||||
|
||||
public function edocument(EDocumentRequest $request): string
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/expenses/edocument",
|
||||
* operationId="edocumentExpense",
|
||||
* tags={"expenses"},
|
||||
* summary="Uploads an electronic document to a expense",
|
||||
* description="Handles the uploading of an electronic document to a expense",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\RequestBody(
|
||||
* description="User credentials",
|
||||
* required=true,
|
||||
* @OA\MediaType(
|
||||
* mediaType="multipart/form-data",
|
||||
* @OA\Schema(
|
||||
* type="array",
|
||||
* @OA\Items(
|
||||
* type="string",
|
||||
* format="binary",
|
||||
* description="The files to be uploaded",
|
||||
* ),
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns a HTTP status",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function edocument(EDocumentRequest $request)
|
||||
{
|
||||
if ($request->hasFile("documents")) {
|
||||
return (new ImportEDocument($request->file("documents")[0]->get(), $request->file("documents")[0]->getClientOriginalName()))->handle();
|
||||
}
|
||||
else {
|
||||
return "No file found";
|
||||
$user = auth()->user();
|
||||
|
||||
foreach ($request->file("documents") as $file) {
|
||||
ImportEDocument::dispatch($file->get(), $file->getClientOriginalName(), $request->file("documents")->getMimeType(), $user->company());
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Processing....'], 200);
|
||||
}
|
||||
}
|
||||
|
@ -59,9 +59,9 @@ class ExportController extends BaseController
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$hash = Str::uuid();
|
||||
$hash = Str::uuid()->toString();
|
||||
$url = \Illuminate\Support\Facades\URL::temporarySignedRoute('protected_download', now()->addHour(), ['hash' => $hash]);
|
||||
Cache::put($hash, $url, now()->addHour());
|
||||
Cache::put($hash, $url, 3600);
|
||||
|
||||
CompanyExport::dispatch($user->getCompany(), $user, $hash);
|
||||
|
||||
|
@ -20,6 +20,7 @@ class GoCardlessController extends Controller
|
||||
{
|
||||
public function ibpRedirect(IbpRequest $request)
|
||||
{
|
||||
|
||||
return $request
|
||||
->getCompanyGateway()
|
||||
->driver($request->getClient())
|
||||
|
@ -118,9 +118,36 @@ class ImportController extends Controller
|
||||
|
||||
})->toArray();
|
||||
|
||||
|
||||
//Exact string match
|
||||
foreach($headers as $key => $value) {
|
||||
|
||||
foreach($translated_keys as $tkey => $tvalue) {
|
||||
|
||||
$concat_needle = str_ireplace(" ", "", $tvalue['index'].$tvalue['label']);
|
||||
$concat_value = str_ireplace(" ", "", $value);
|
||||
|
||||
if($this->testMatch($concat_value, $concat_needle)) {
|
||||
|
||||
$hit = $tvalue['key'];
|
||||
$hints[$key] = $hit;
|
||||
unset($translated_keys[$tkey]);
|
||||
break;
|
||||
|
||||
} else {
|
||||
$hints[$key] = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Label Match
|
||||
foreach($headers as $key => $value) {
|
||||
|
||||
if(isset($hints[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach($translated_keys as $tkey => $tvalue) {
|
||||
|
||||
if($this->testMatch($value, $tvalue['label'])) {
|
||||
@ -134,10 +161,9 @@ class ImportController extends Controller
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//second pass using the index of the translation here
|
||||
//Index matching pass using the index of the translation here
|
||||
foreach($headers as $key => $value) {
|
||||
if(isset($hints[$key])) {
|
||||
continue;
|
||||
|
63
app/Http/Controllers/ImportQuickbooksController.php
Normal file
63
app/Http/Controllers/ImportQuickbooksController.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\Quickbooks\AuthorizedQuickbooksRequest;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Http\Requests\Quickbooks\AuthQuickbooksRequest;
|
||||
use App\Services\Quickbooks\QuickbooksService;
|
||||
|
||||
class ImportQuickbooksController extends BaseController
|
||||
{
|
||||
// private array $import_entities = [
|
||||
// 'client' => 'Customer',
|
||||
// 'invoice' => 'Invoice',
|
||||
// 'product' => 'Item',
|
||||
// 'payment' => 'Payment'
|
||||
// ];
|
||||
|
||||
public function onAuthorized(AuthorizedQuickbooksRequest $request)
|
||||
{
|
||||
|
||||
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
||||
$company = $request->getCompany();
|
||||
$qb = new QuickbooksService($company);
|
||||
|
||||
$realm = $request->query('realmId');
|
||||
$access_token_object = $qb->sdk()->accessTokenFromCode($request->query('code'), $realm);
|
||||
$qb->sdk()->saveOAuthToken($access_token_object);
|
||||
|
||||
return redirect(config('ninja.react_url'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
*/
|
||||
public function authorizeQuickbooks(AuthQuickbooksRequest $request, string $token)
|
||||
{
|
||||
|
||||
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
||||
$company = $request->getCompany();
|
||||
$qb = new QuickbooksService($company);
|
||||
|
||||
$authorizationUrl = $qb->sdk()->getAuthorizationUrl();
|
||||
$state = $qb->sdk()->getState();
|
||||
|
||||
Cache::put($state, $token, 190);
|
||||
|
||||
return redirect()->to($authorizationUrl);
|
||||
}
|
||||
|
||||
}
|
@ -475,7 +475,10 @@ class InvoiceController extends BaseController
|
||||
*/
|
||||
public function destroy(DestroyInvoiceRequest $request, Invoice $invoice)
|
||||
{
|
||||
$this->invoice_repo->delete($invoice);
|
||||
|
||||
if (!$invoice->is_deleted) {
|
||||
$this->invoice_repo->delete($invoice);
|
||||
}
|
||||
|
||||
return $this->itemResponse($invoice->fresh());
|
||||
}
|
||||
@ -503,7 +506,7 @@ class InvoiceController extends BaseController
|
||||
|
||||
$invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
|
||||
|
||||
if ($invoices->count() == 0 ) {
|
||||
if ($invoices->count() == 0) {
|
||||
return response()->json(['message' => 'No Invoices Found']);
|
||||
}
|
||||
|
||||
|
147
app/Http/Controllers/MailgunController.php
Normal file
147
app/Http/Controllers/MailgunController.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Jobs\Mailgun\ProcessMailgunWebhook;
|
||||
use App\Jobs\Mailgun\ProcessMailgunInboundWebhook;
|
||||
|
||||
/**
|
||||
* Class MailgunController.
|
||||
*/
|
||||
class MailgunController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Mailgun Webhook.
|
||||
*
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/mailgun_webhook",
|
||||
* operationId="mailgunWebhook",
|
||||
* tags={"mailgun"},
|
||||
* summary="Processing webhooks from Mailgun",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the saved credit object",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Credit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
|
||||
$input = $request->all();
|
||||
|
||||
nlog($input);
|
||||
|
||||
if (\abs(\time() - $request['signature']['timestamp']) > 15) {
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
|
||||
if (\hash_equals(\hash_hmac('sha256', $input['signature']['timestamp'] . $input['signature']['token'], config('services.mailgun.webhook_signing_key')), $input['signature']['signature'])) {
|
||||
ProcessMailgunWebhook::dispatch($request->all())->delay(rand(2, 10));
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Success.'], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Mailgun Inbound Webhook.
|
||||
*
|
||||
* IMPORTANT NOTICE: mailgun does NOT strip old sended emails, therefore all past attachements are present
|
||||
*
|
||||
* IMPORTANT NOTICE: mailgun saves the message and attachemnts for later retrieval, therefore we can process it within a async job for performance reasons
|
||||
*
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/mailgun_inbound_webhook",
|
||||
* operationId="mailgunInboundWebhook",
|
||||
* tags={"mailgun"},
|
||||
* summary="Processing inbound webhooks from Mailgun",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the saved credit object",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Credit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function inboundWebhook(Request $request)
|
||||
{
|
||||
$input = $request->all();
|
||||
|
||||
nlog($input);
|
||||
|
||||
if (!array_key_exists('sender', $input) || !array_key_exists('recipient', $input) || !array_key_exists('message-url', $input)) {
|
||||
nlog('Failed: Message could not be parsed, because required parameters are missing. Please ensure contacting this api-endpoint with a store & notify operation instead of a forward operation!');
|
||||
return response()->json(['message' => 'Failed. Missing Parameters. Use store and notify!'], 400);
|
||||
}
|
||||
|
||||
// @turbo124 TODO: how to check for services.mailgun.webhook_signing_key on company level, when custom credentials are defined
|
||||
// TODO: validation for client mail credentials by recipient
|
||||
$authorizedByHash = \hash_equals(\hash_hmac('sha256', $input['timestamp'] . $input['token'], config('services.mailgun.webhook_signing_key')), $input['signature']);
|
||||
$authorizedByToken = $request->has('token') && $request->get('token') == config('ninja.inbound_mailbox.inbound_webhook_token');
|
||||
if (!$authorizedByHash && !$authorizedByToken)
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
|
||||
/** @var \App\Models\Company $company */
|
||||
$company = MultiDB::findAndSetDbByExpenseMailbox($input["recipient"]);
|
||||
|
||||
if(!$company)
|
||||
return response()->json(['message' => 'Ok'], 200); // Fail gracefully
|
||||
|
||||
ProcessMailgunInboundWebhook::dispatch($input["sender"], $input["recipient"], $input["message-url"], $company)->delay(rand(2, 10));
|
||||
|
||||
return response()->json(['message' => 'Success.'], 200);
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\Mailgun\ProcessMailgunWebhook;
|
||||
use App\Jobs\PostMark\ProcessPostmarkWebhook;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Class MailgunWebhookController.
|
||||
*/
|
||||
class MailgunWebhookController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
|
||||
$input = $request->all();
|
||||
|
||||
if (\abs(\time() - $request['signature']['timestamp']) > 15) {
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
|
||||
if(\hash_equals(\hash_hmac('sha256', $input['signature']['timestamp'] . $input['signature']['token'], config('services.mailgun.webhook_signing_key')), $input['signature']['signature'])) {
|
||||
ProcessMailgunWebhook::dispatch($request->all())->delay(rand(2,10));
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Success.'], 200);
|
||||
}
|
||||
}
|
@ -22,7 +22,6 @@ use Illuminate\Support\Str;
|
||||
|
||||
class OneTimeTokenController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
@ -74,6 +73,10 @@ class OneTimeTokenController extends BaseController
|
||||
'is_react' => $request->hasHeader('X-REACT') ? true : false,
|
||||
];
|
||||
|
||||
if($request->institution_id) {
|
||||
$data['institution_id'] = $request->institution_id;
|
||||
}
|
||||
|
||||
Cache::put($hash, $data, 3600);
|
||||
|
||||
return response()->json(['hash' => $hash], 200);
|
||||
|
@ -12,6 +12,11 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\PostMark\ProcessPostmarkWebhook;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Services\InboundMail\InboundMail;
|
||||
use App\Services\InboundMail\InboundMailEngine;
|
||||
use App\Utils\TempFile;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
@ -19,7 +24,6 @@ use Illuminate\Http\Request;
|
||||
*/
|
||||
class PostMarkController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
@ -61,11 +65,271 @@ class PostMarkController extends BaseController
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
if ($request->header('X-API-SECURITY') && $request->header('X-API-SECURITY') == config('services.postmark.token')) {
|
||||
ProcessPostmarkWebhook::dispatch($request->all())->delay(10);
|
||||
ProcessPostmarkWebhook::dispatch($request->all())->delay(rand(2, 10));
|
||||
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Postmark Webhook.
|
||||
*
|
||||
* IMPORTANT NOTICE: postmark does NOT strip old sended emails, therefore also all past attachements are present
|
||||
*
|
||||
* IMPORTANT NOTICE: postmark does not saves attachements for later retrieval, therefore we cannot process it within a async job
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/postmark_inbound_webhook",
|
||||
* operationId="postmarkInboundWebhook",
|
||||
* tags={"postmark"},
|
||||
* summary="Processing inbound webhooks from PostMark",
|
||||
* description="Adds an credit to the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the saved credit object",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Credit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*
|
||||
* array (
|
||||
* 'FromName' => 'Max Mustermann',
|
||||
* 'MessageStream' => 'inbound',
|
||||
* 'From' => 'max@mustermann.de',
|
||||
* 'FromFull' =>
|
||||
* array (
|
||||
* 'Email' => 'max@mustermann.de',
|
||||
* 'Name' => 'Max Mustermann',
|
||||
* 'MailboxHash' => NULL,
|
||||
* ),
|
||||
* 'To' => '370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com',
|
||||
* 'ToFull' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Email' => '370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com',
|
||||
* 'Name' => NULL,
|
||||
* 'MailboxHash' => NULL,
|
||||
* ),
|
||||
* ),
|
||||
* 'Cc' => NULL,
|
||||
* 'CcFull' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'Bcc' => NULL,
|
||||
* 'BccFull' =>
|
||||
* array (
|
||||
* ),
|
||||
* 'OriginalRecipient' => '370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com',
|
||||
* 'Subject' => 'Re: adaw',
|
||||
* 'MessageID' => 'd37fde00-b4cf-4b64-ac64-e9f6da523c25',
|
||||
* 'ReplyTo' => NULL,
|
||||
* 'MailboxHash' => NULL,
|
||||
* 'Date' => 'Sun, 24 Mar 2024 13:17:52 +0100',
|
||||
* 'TextBody' => 'wadwad
|
||||
*
|
||||
* Am So., 24. März 2024 um 13:17 Uhr schrieb Max Mustermann <max@mustermann.de>:
|
||||
*
|
||||
* > test
|
||||
* >
|
||||
*
|
||||
* --
|
||||
* test.de - Max Mustermann <https://test.de/>kontakt@test.de
|
||||
* <mailto:kontakt@test.de>',
|
||||
* 'HtmlBody' => '<div dir="ltr">wadwad</div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">Am So., 24. März 2024 um 13:17 Uhr schrieb Max Mustermann <<a href="mailto:max@mustermann.de">max@mustermann.de</a>>:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr">test</div>
|
||||
* </blockquote></div>
|
||||
*
|
||||
* <br>
|
||||
* <font size="3"><a href="https://test.de/" target="_blank">test.de - Max Mustermann</a></font><div><a href="mailto:kontakt@test.de" style="font-size:medium" target="_blank">kontakt@test.de</a><br></div>',
|
||||
* 'StrippedTextReply' => 'wadwad
|
||||
*
|
||||
* Am So., 24. März 2024 um 13:17 Uhr schrieb Max Mustermann <max@mustermann.de>:',
|
||||
* 'Tag' => NULL,
|
||||
* 'Headers' =>
|
||||
* array (
|
||||
* 0 =>
|
||||
* array (
|
||||
* 'Name' => 'Return-Path',
|
||||
* 'Value' => '<max@mustermann.de>',
|
||||
* ),
|
||||
* 1 =>
|
||||
* array (
|
||||
* 'Name' => 'Received',
|
||||
* 'Value' => 'by p-pm-inboundg02a-aws-euwest1a.inbound.postmarkapp.com (Postfix, from userid 996) id 8ED1A453CA4; Sun, 24 Mar 2024 12:18:10 +0000 (UTC)',
|
||||
* ),
|
||||
* 2 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Spam-Checker-Version',
|
||||
* 'Value' => 'SpamAssassin 3.4.0 (2014-02-07) on p-pm-inboundg02a-aws-euwest1a',
|
||||
* ),
|
||||
* 3 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Spam-Status',
|
||||
* 'Value' => 'No',
|
||||
* ),
|
||||
* 4 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Spam-Score',
|
||||
* 'Value' => '-0.1',
|
||||
* ),
|
||||
* 5 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Spam-Tests',
|
||||
* 'Value' => 'DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,HTML_MESSAGE, RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,RCVD_IN_ZEN_BLOCKED_OPENDNS, SPF_HELO_NONE,SPF_PASS,URIBL_DBL_BLOCKED_OPENDNS,URIBL_ZEN_BLOCKED_OPENDNS',
|
||||
* ),
|
||||
* 6 =>
|
||||
* array (
|
||||
* 'Name' => 'Received-SPF',
|
||||
* 'Value' => 'pass (test.de: Sender is authorized to use \'max@mustermann.de\' in \'mfrom\' identity (mechanism \'include:_spf.google.com\' matched)) receiver=p-pm-inboundg02a-aws-euwest1a; identity=mailfrom; envelope-from="max@mustermann.de"; helo=mail-lf1-f51.google.com; client-ip=209.85.167.51',
|
||||
* ),
|
||||
* 7 =>
|
||||
* array (
|
||||
* 'Name' => 'Received',
|
||||
* 'Value' => 'from mail-lf1-f51.google.com (mail-lf1-f51.google.com [209.85.167.51]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by p-pm-inboundg02a-aws-euwest1a.inbound.postmarkapp.com (Postfix) with ESMTPS id 437BD453CA2 for <370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com>; Sun, 24 Mar 2024 12:18:10 +0000 (UTC)',
|
||||
* ),
|
||||
* 8 =>
|
||||
* array (
|
||||
* 'Name' => 'Received',
|
||||
* 'Value' => 'by mail-lf1-f51.google.com with SMTP id 2adb3069b0e04-513cf9bacf1so4773866e87.0 for <370c69ad9e41d616fc914b3c60250224@inbound.postmarkapp.com>; Sun, 24 Mar 2024 05:18:10 -0700 (PDT)',
|
||||
* ),
|
||||
* 9 =>
|
||||
* array (
|
||||
* 'Name' => 'DKIM-Signature',
|
||||
* 'Value' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=test.de; s=google; t=1711282689; x=1711887489; darn=inbound.postmarkapp.com; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :from:to:cc:subject:date:message-id:reply-to; bh=NvjmqLXF/5L5ZrpToR/6FgVOhTOGC9j0/B2Na5Ke6J8=; b=AMXIEoh6yGrOT6X3eBBClQ3NXFNuEoqxeM6aPONsqbpShAcT24iAJmqXylaLHv3fyX Hm6mwp3a029NnrLP/VRyKZbzIMBN2iycidtrEMXF/Eg2e42Q/08/2dZ7nxH6NqE/jz01 3M7qvwHvuoZ2Knhj7rnZc6I5m/nFxBsZc++Aj0Vv9sFoWZZooqAeTXbux1I5NyE17MrL D6byca43iINARZN7XOkoChRRZoZbOqZEtc2Va5yw7v+aYguLB4HHrIFC7G+L8hAJ0IAo 3R3DFeBw58M1xtxXCREI8Y6qMQTw60XyFw0gVmZzqR4hZiTerBSJJsZLZOBgmXxq3WLS +xVQ==',
|
||||
* ),
|
||||
* 10 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Google-DKIM-Signature',
|
||||
* 'Value' => 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1711282689; x=1711887489; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=NvjmqLXF/5L5ZrpToR/6FgVOhTOGC9j0/B2Na5Ke6J8=; b=uKoMhir+MX/wycNEr29Sffj45ooKksCJ1OfSRkIIGHk0rnHn8Vh+c7beYipwRPW4F2 h46K64vtIX00guYMdL2Qo2eY96+wALTqHCy67PGhvotVTROz21yxjx62pCDPGs5tefOu IkyxoybpIK8zAfLoDTd9p2GIrr5brKJyB2w1NQc1htxTQ5D4RgBxUAOKv4uVEr8r47iA MIo5d8/AifA+vCOAh7iJ7EmvDQ1R+guhQyH9m1Jo8PLapiYuHXggpBJvooyGuflKqbnt gJ/dscEr4d5aWJbw/x1dmIJ5gyJPGdBWq8NRqV/qbkXQW3H/gylifDUPXbki+EQBD5Yu EuLQ==',
|
||||
* ),
|
||||
* 11 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Gm-Message-State',
|
||||
* 'Value' => 'AOJu0Yxpbp1sRh17lNzg+pLnIx1jCn8ZFJQMgFuHK+6Z8RqFS5KKKTxR 8onpEbxWYYVUbrJFExNBHPD/3jdxqifCVVNaDmbpwHgmW5lHLJmA5vYRq5NFZ9OA6zKx/N6Gipr iXE4fXmSqghFNTzy9V/RT08Zp+F5RiFh/Ta6ltQl8XfCPFfSawLz6cagUgt8bBuF4RqdrYmWwzj ty86V5Br1htRNEFYivoXnNmaRcsD0tca1D23ny62O6RwWugrj1IpAYhViNyTZAWu+loKgfjJJoI MsyiSU=',
|
||||
* ),
|
||||
* 12 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Google-Smtp-Source',
|
||||
* 'Value' => 'AGHT+IEdtZqbVI6j7WLeaSL3dABGSnWIXaSjbYqXvFvE2H+f2zsn0gknQ4OdTJecQRCabpypVF2ue91Jb7aKl6RiyEQ=',
|
||||
* ),
|
||||
* 13 =>
|
||||
* array (
|
||||
* 'Name' => 'X-Received',
|
||||
* 'Value' => 'by 2002:a19:385a:0:b0:513:c876:c80a with SMTP id d26-20020a19385a000000b00513c876c80amr2586776lfj.34.1711282689140; Sun, 24 Mar 2024 05:18:09 -0700 (PDT)',
|
||||
* ),
|
||||
* 14 =>
|
||||
* array (
|
||||
* 'Name' => 'MIME-Version',
|
||||
* 'Value' => '1.0',
|
||||
* ),
|
||||
* 15 =>
|
||||
* array (
|
||||
* 'Name' => 'References',
|
||||
* 'Value' => '<CADfEuNsNFmNNCJDPjpS36amoLv2XEm41HmgYJT7Tj=R96PkxnA@mail.gmail.com>',
|
||||
* ),
|
||||
* 16 =>
|
||||
* array (
|
||||
* 'Name' => 'In-Reply-To',
|
||||
* 'Value' => '<CADfEuNsNFmNNCJDPjpS36amoLv2XEm41HmgYJT7Tj=R96PkxnA@mail.gmail.com>',
|
||||
* ),
|
||||
* 17 =>
|
||||
* array (
|
||||
* 'Name' => 'Message-ID',
|
||||
* 'Value' => '<CADfEuNvyCLsnp=CwJ3BF=-L6rn=o+DmUOPP6Cp4F-SO0p0hVwQ@mail.gmail.com>',
|
||||
* ),
|
||||
* ),
|
||||
* 'Attachments' =>
|
||||
* array (
|
||||
* array (
|
||||
* 'Content' => "base64-String",
|
||||
* 'ContentLength' => 60164,
|
||||
* 'Name' => 'Unbenannt.png',
|
||||
* 'ContentType' => 'image/png',
|
||||
* 'ContentID' => 'ii_luh2h8lg0',
|
||||
* )
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function inboundWebhook(Request $request)
|
||||
{
|
||||
|
||||
$input = $request->all();
|
||||
|
||||
if (!$request->has('token') || $request->token != config('ninja.inbound_mailbox.inbound_webhook_token'))
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
|
||||
if (!(array_key_exists("MessageStream", $input) && $input["MessageStream"] == "inbound") || !array_key_exists("To", $input) || !array_key_exists("From", $input) || !array_key_exists("MessageID", $input)) {
|
||||
nlog('Failed: Message could not be parsed, because required parameters are missing.');
|
||||
return response()->json(['message' => 'Failed. Missing/Invalid Parameters.'], 400);
|
||||
}
|
||||
|
||||
$company = MultiDB::findAndSetDbByExpenseMailbox($input["ToFull"][0]["Email"]);
|
||||
|
||||
if (!$company) {
|
||||
nlog('[PostmarkInboundWebhook] unknown Expense Mailbox occured while handling an inbound email from postmark: ' . $input["To"]);
|
||||
return response()->json(['message' => 'Ok'], 200);
|
||||
}
|
||||
|
||||
$inboundEngine = new InboundMailEngine($company);
|
||||
|
||||
if ($inboundEngine->isInvalidOrBlocked($input["From"], $input["ToFull"][0]["Email"])) {
|
||||
return response()->json(['message' => 'Blocked.'], 403);
|
||||
}
|
||||
|
||||
try { // important to save meta if something fails here to prevent spam
|
||||
|
||||
// prepare data for ingresEngine
|
||||
$inboundMail = new InboundMail();
|
||||
|
||||
$inboundMail->from = $input["From"] ?? '';
|
||||
$inboundMail->to = $input["To"]; // usage of data-input, because we need a single email here
|
||||
$inboundMail->subject = $input["Subject"] ?? '';
|
||||
$inboundMail->body = $input["HtmlBody"] ?? '';
|
||||
$inboundMail->text_body = $input["TextBody"] ?? '';
|
||||
$inboundMail->date = Carbon::createFromTimeString($input["Date"]);
|
||||
|
||||
// parse documents as UploadedFile from webhook-data
|
||||
foreach ($input["Attachments"] as $attachment) {
|
||||
|
||||
$inboundMail->documents[] = TempFile::UploadedFileFromBase64($attachment["Content"], $attachment["Name"], $attachment["ContentType"]);
|
||||
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$inboundEngine->saveMeta($input["From"], $input["To"]); // important to save this, to protect from spam
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// perform
|
||||
try {
|
||||
|
||||
$inboundEngine->handleExpenseMailbox($inboundMail);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
if ($e->getCode() == 409)
|
||||
return response()->json(['message' => $e->getMessage()], 409);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +148,9 @@ class PreviewController extends BaseController
|
||||
! empty(request()->input('entity')) &&
|
||||
! empty(request()->input('entity_id'))) {
|
||||
|
||||
if($request->input('entity') == 'purchase_order')
|
||||
return $preview = app(\App\Http\Controllers\PreviewPurchaseOrderController::class)->show($request);
|
||||
|
||||
$design_object = json_decode(json_encode(request()->input('design')));
|
||||
|
||||
if (! is_object($design_object)) {
|
||||
@ -293,10 +296,14 @@ class PreviewController extends BaseController
|
||||
$ts = (new TemplateService());
|
||||
|
||||
try {
|
||||
|
||||
$ts->setCompany($company)
|
||||
->setTemplate($design_object)
|
||||
->mock();
|
||||
|
||||
} catch(SyntaxError $e) {
|
||||
} catch(\Exception $e) {
|
||||
return response()->json(['message' => 'invalid data access', 'errors' => ['design.design.body' => $e->getMessage()]], 422);
|
||||
}
|
||||
|
||||
if (request()->query('html') == 'true') {
|
||||
|
@ -36,6 +36,7 @@ use App\Utils\Traits\Pdf\PageNumbering;
|
||||
use App\Utils\VendorHtmlEngine;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Turbo124\Beacon\Facades\LightLogs;
|
||||
|
||||
@ -83,7 +84,7 @@ class PreviewPurchaseOrderController extends BaseController
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function show()
|
||||
public function show($request)
|
||||
{
|
||||
if (request()->has('entity') &&
|
||||
request()->has('entity_id') &&
|
||||
|
@ -517,7 +517,7 @@ class QuoteController extends BaseController
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
nlog("booop");
|
||||
$action = $request->input('action');
|
||||
|
||||
$ids = $request->input('ids');
|
||||
|
@ -181,8 +181,9 @@ class SelfUpdateController extends BaseController
|
||||
|
||||
public function checkVersion()
|
||||
{
|
||||
if(Ninja::isHosted())
|
||||
if(Ninja::isHosted()) {
|
||||
return '5.10.SaaS';
|
||||
}
|
||||
|
||||
return trim(file_get_contents(config('ninja.version_url')));
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ class SetupController extends Controller
|
||||
return response('Oops, something went wrong. Check your logs.'); /* We should never reach this block, but just in case. */
|
||||
}
|
||||
|
||||
$mail_driver = $request->input('mail_driver');
|
||||
$mail_driver = $request->input('mail_driver', 'smtp');
|
||||
|
||||
$url = $request->input('url');
|
||||
$db_host = $request->input('db_host');
|
||||
|
@ -11,23 +11,25 @@
|
||||
|
||||
namespace App\Http\Controllers\VendorPortal;
|
||||
|
||||
use App\Events\Misc\InvitationWasViewed;
|
||||
use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
|
||||
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Webhook;
|
||||
use Illuminate\View\View;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use App\Jobs\Entity\CreateRawPdf;
|
||||
use App\Jobs\Util\WebhookHandler;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest;
|
||||
use App\Jobs\Invoice\InjectSignature;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use App\Models\PurchaseOrderInvitation;
|
||||
use App\Events\Misc\InvitationWasViewed;
|
||||
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
|
||||
use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
|
||||
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrderRequest;
|
||||
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrdersRequest;
|
||||
use App\Jobs\Entity\CreateRawPdf;
|
||||
use App\Jobs\Invoice\InjectSignature;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\PurchaseOrderInvitation;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest;
|
||||
|
||||
class PurchaseOrderController extends Controller
|
||||
{
|
||||
@ -187,6 +189,9 @@ class PurchaseOrderController extends Controller
|
||||
}
|
||||
|
||||
event(new PurchaseOrderWasAccepted($purchase_order, auth()->guard('vendor')->user(), $purchase_order->company, Ninja::eventVars()));
|
||||
|
||||
WebhookHandler::dispatch(Webhook::EVENT_ACCEPTED_PURCHASE_ORDER, $purchase_order, $purchase_order->company, 'vendor')->delay(0);
|
||||
|
||||
});
|
||||
|
||||
if ($purchase_count_query->count() == 1) {
|
||||
|
@ -14,7 +14,7 @@ namespace App\Http\Controllers\VendorPortal;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\VendorContact;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Utils\TranslationHelper;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VendorContactController extends Controller
|
||||
{
|
||||
@ -58,14 +58,14 @@ class VendorContactController extends Controller
|
||||
'settings' => $vendor_contact->vendor->company->settings,
|
||||
'company' => $vendor_contact->vendor->company,
|
||||
'sidebar' => $this->sidebarMenu(),
|
||||
'countries' => TranslationHelper::getCountries(),
|
||||
'countries' => app('countries'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(VendorContact $vendor_contact)
|
||||
public function update(Request $request, VendorContact $vendor_contact)
|
||||
{
|
||||
$vendor_contact->fill(request()->all());
|
||||
$vendor_contact->vendor->fill(request()->all());
|
||||
$vendor_contact->fill($request->all());
|
||||
$vendor_contact->vendor->fill($request->all());
|
||||
$vendor_contact->push();
|
||||
|
||||
return back()->withSuccess(ctrans('texts.profile_updated_successfully'));
|
||||
@ -76,16 +76,10 @@ class VendorContactController extends Controller
|
||||
$enabled_modules = auth()->guard('vendor')->user()->company->enabled_modules;
|
||||
$data = [];
|
||||
|
||||
// TODO: Enable dashboard once it's completed.
|
||||
// $this->settings->enable_client_portal_dashboard
|
||||
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
|
||||
|
||||
if (self::MODULE_PURCHASE_ORDERS & $enabled_modules) {
|
||||
$data[] = ['title' => ctrans('texts.purchase_orders'), 'url' => 'vendor.purchase_orders.index', 'icon' => 'file-text'];
|
||||
}
|
||||
|
||||
// $data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download'];
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ class ContactRegister
|
||||
// As a fallback for self-hosted, it will use default company in the system
|
||||
// if key isn't provided in the url.
|
||||
if (! $request->route()->parameter('company_key') && Ninja::isSelfHost()) {
|
||||
$company = Account::query()->first()->default_company;
|
||||
$company = Account::query()->first()->default_company ?? Account::query()->first()->companies->first();
|
||||
|
||||
if (! $company->client_can_register) {
|
||||
abort(400, 'Registration disabled');
|
||||
|
@ -4,15 +4,15 @@ namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Cache\RateLimiter;
|
||||
use Illuminate\Contracts\Redis\Factory as Redis;
|
||||
use Illuminate\Redis\Limiters\DurationLimiter;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
|
||||
class ThrottleRequestsWithPredis extends ThrottleRequests
|
||||
class ThrottleRequestsWithPredis extends \Illuminate\Routing\Middleware\ThrottleRequests
|
||||
{
|
||||
/**
|
||||
* The Redis factory implementation.
|
||||
*
|
||||
* @var \Illuminate\Redis\Connections\Connection
|
||||
* @var \Illuminate\Contracts\Redis\Factory
|
||||
*/
|
||||
protected $redis;
|
||||
|
||||
@ -32,14 +32,14 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
|
||||
|
||||
/**
|
||||
* Create a new request throttler.
|
||||
*
|
||||
* @param \Illuminate\Cache\RateLimiter $limiter
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(RateLimiter $limiter)
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function __construct(RateLimiter $limiter, Redis $redis)
|
||||
{
|
||||
parent::__construct($limiter);
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
$this->redis = \Illuminate\Support\Facades\Redis::connection('sentinel-cache');
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
|
||||
protected function handleRequest($request, Closure $next, array $limits)
|
||||
{
|
||||
foreach ($limits as $limit) {
|
||||
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decayMinutes)) {
|
||||
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) {
|
||||
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
|
||||
}
|
||||
}
|
||||
@ -79,16 +79,16 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
|
||||
*
|
||||
* @param string $key
|
||||
* @param int $maxAttempts
|
||||
* @param int $decayMinutes
|
||||
* @param int $decaySeconds
|
||||
* @return mixed
|
||||
*/
|
||||
protected function tooManyAttempts($key, $maxAttempts, $decayMinutes)
|
||||
protected function tooManyAttempts($key, $maxAttempts, $decaySeconds)
|
||||
{
|
||||
$limiter = new DurationLimiter(
|
||||
$this->redis,
|
||||
$this->getRedisConnection(),
|
||||
$key,
|
||||
$maxAttempts,
|
||||
$decayMinutes * 60
|
||||
$decaySeconds
|
||||
);
|
||||
|
||||
return tap(! $limiter->acquire(), function () use ($key, $limiter) {
|
||||
@ -121,4 +121,13 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
|
||||
{
|
||||
return $this->decaysAt[$key] - $this->currentTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Redis connection that should be used for throttling.
|
||||
*
|
||||
*/
|
||||
protected function getRedisConnection()
|
||||
{
|
||||
return $this->redis;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreNoteRequest extends Request
|
||||
{
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
@ -68,11 +69,12 @@ class StoreNoteRequest extends Request
|
||||
|
||||
public function getEntity()
|
||||
{
|
||||
if(!$this->entity)
|
||||
if(!$this->entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$class = "\\App\\Models\\".ucfirst(Str::camel(rtrim($this->entity, 's')));
|
||||
return $class::withTrashed()->find(is_string($this->entity_id) ? $this->decodePrimaryKey($this->entity_id) : $this->entity_id);
|
||||
return $class::withTrashed()->find(is_string($this->entity_id) ? $this->decodePrimaryKey($this->entity_id) : $this->entity_id);
|
||||
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,8 @@ class StoreBankTransactionRuleRequest extends Request
|
||||
'rules.*.value' => 'bail|required|nullable',
|
||||
'auto_convert' => 'bail|sometimes|bool',
|
||||
'matches_on_all' => 'bail|sometimes|bool',
|
||||
'applies_to' => 'bail|sometimes|string',
|
||||
'applies_to' => 'bail|sometimes|string|in:CREDIT,DEBIT',
|
||||
'on_credit_match' => 'bail|sometimes|in:create_payment,link_payment'
|
||||
];
|
||||
|
||||
$rules['category_id'] = 'bail|sometimes|nullable|exists:expense_categories,id,company_id,'.$user->company()->id.',is_deleted,0';
|
||||
|
@ -66,8 +66,7 @@ class ShowCalculatedFieldRequest extends Request
|
||||
$input['end_date'] = now()->format('Y-m-d');
|
||||
}
|
||||
|
||||
if(isset($input['period']) && $input['period'] == 'previous')
|
||||
{
|
||||
if(isset($input['period']) && $input['period'] == 'previous') {
|
||||
$dates = $this->calculatePreviousPeriodStartAndEndDates($input, $user->company());
|
||||
$input['start_date'] = $dates[0];
|
||||
$input['end_date'] = $dates[1];
|
||||
|
@ -13,13 +13,11 @@ namespace App\Http\Requests\Client;
|
||||
|
||||
use App\DataMapper\ClientSettings;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Http\ValidationRules\Client\CountryCodeExistsRule;
|
||||
use App\Http\ValidationRules\Ninja\CanStoreClientsRule;
|
||||
use App\Http\ValidationRules\ValidClientGroupSettingsRule;
|
||||
use App\Models\Client;
|
||||
use App\Models\GroupSetting;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreClientRequest extends Request
|
||||
|
@ -136,6 +136,10 @@ class UpdateClientRequest extends Request
|
||||
$input['shipping_country_id'] = $this->getCountryCode($input['shipping_country_code']);
|
||||
}
|
||||
|
||||
if (isset($input['e_invoice']) && is_array($input['e_invoice'])) {
|
||||
//ensure it is normalized first!
|
||||
$input['e_invoice'] = $this->client->filterNullsRecursive($input['e_invoice']);
|
||||
}
|
||||
|
||||
$this->replace($input);
|
||||
}
|
||||
|
@ -11,14 +11,15 @@
|
||||
|
||||
namespace App\Http\Requests\Company;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Company;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Http\ValidationRules\ValidSettingsRule;
|
||||
use App\Http\ValidationRules\Company\ValidSubdomain;
|
||||
use App\Http\ValidationRules\Company\ValidCompanyQuantity;
|
||||
use App\Http\ValidationRules\Company\ValidExpenseMailbox;
|
||||
use App\Http\ValidationRules\Company\ValidSubdomain;
|
||||
use App\Http\ValidationRules\ValidSettingsRule;
|
||||
use App\Models\Company;
|
||||
use App\Utils\Ninja;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
|
||||
class StoreCompanyRequest extends Request
|
||||
{
|
||||
@ -50,12 +51,14 @@ class StoreCompanyRequest extends Request
|
||||
$rules['portal_domain'] = 'sometimes|url';
|
||||
} else {
|
||||
if (Ninja::isHosted()) {
|
||||
$rules['subdomain'] = ['nullable', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9.-]+[a-zA-Z0-9]$/', new ValidSubdomain()];
|
||||
$rules['subdomain'] = ['nullable', 'regex:/^[a-zA-Z0-9-]{1,63}$/', new ValidSubdomain()];
|
||||
} else {
|
||||
$rules['subdomain'] = 'nullable|alpha_num';
|
||||
}
|
||||
}
|
||||
|
||||
$rules['expense_mailbox'] = new ValidExpenseMailbox();
|
||||
|
||||
$rules['smtp_host'] = 'sometimes|string|nullable';
|
||||
$rules['smtp_port'] = 'sometimes|integer|nullable';
|
||||
$rules['smtp_encryption'] = 'sometimes|string';
|
||||
@ -84,23 +87,27 @@ class StoreCompanyRequest extends Request
|
||||
$input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/");
|
||||
}
|
||||
|
||||
if(Ninja::isHosted() && !isset($input['subdomain'])) {
|
||||
if (isset($input['expense_mailbox']) && Ninja::isHosted() && !($this->company->account->isPaid() && $this->company->account->plan == 'enterprise')) {
|
||||
unset($input['expense_mailbox']);
|
||||
}
|
||||
|
||||
if (Ninja::isHosted() && !isset($input['subdomain'])) {
|
||||
$input['subdomain'] = MultiDB::randomSubdomainGenerator();
|
||||
}
|
||||
|
||||
if(isset($input['smtp_username']) && strlen(str_replace("*", "", $input['smtp_username'])) < 2) {
|
||||
if (isset($input['smtp_username']) && strlen(str_replace("*", "", $input['smtp_username'])) < 2) {
|
||||
unset($input['smtp_username']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) {
|
||||
if (isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) {
|
||||
unset($input['smtp_password']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_port'])) {
|
||||
if (isset($input['smtp_port'])) {
|
||||
$input['smtp_port'] = (int) $input['smtp_port'];
|
||||
}
|
||||
|
||||
if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) {
|
||||
if (isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) {
|
||||
$input['smtp_verify_peer'] == 'true' ? true : false;
|
||||
}
|
||||
|
||||
|
@ -14,10 +14,12 @@ namespace App\Http\Requests\Company;
|
||||
use App\Utils\Ninja;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\DataMapper\CompanySettings;
|
||||
use App\Http\ValidationRules\ValidSettingsRule;
|
||||
use App\Http\ValidationRules\EInvoice\ValidCompanyScheme;
|
||||
use App\Http\ValidationRules\Company\ValidSubdomain;
|
||||
use App\Http\ValidationRules\Company\ValidExpenseMailbox;
|
||||
use App\Http\ValidationRules\EInvoice\ValidCompanyScheme;
|
||||
|
||||
class UpdateCompanyRequest extends Request
|
||||
{
|
||||
@ -65,7 +67,7 @@ class UpdateCompanyRequest extends Request
|
||||
$rules['smtp_local_domain'] = 'sometimes|string|nullable';
|
||||
// $rules['smtp_verify_peer'] = 'sometimes|string';
|
||||
|
||||
$rules['e_invoice'] = ['sometimes','nullable', new ValidCompanyScheme()];
|
||||
$rules['e_invoice'] = ['sometimes', 'nullable', new ValidCompanyScheme()];
|
||||
|
||||
if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) {
|
||||
$rules['portal_domain'] = 'bail|nullable|sometimes|url';
|
||||
@ -75,6 +77,15 @@ class UpdateCompanyRequest extends Request
|
||||
$rules['subdomain'] = ['nullable', 'regex:/^[a-zA-Z0-9.-]+[a-zA-Z0-9]$/', new ValidSubdomain()];
|
||||
}
|
||||
|
||||
$rules['expense_mailbox'] = ['sometimes','email', 'nullable', new ValidExpenseMailbox(), Rule::unique('companies')->ignore($this->company->id)];
|
||||
$rules['expense_mailbox_active'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_allow_company_users'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_allow_vendors'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_allow_clients'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_allow_unknown'] = ['sometimes','boolean'];
|
||||
$rules['inbound_mailbox_whitelist'] = ['sometimes', 'string', 'nullable', 'regex:/^[\w\-\.\+]+@([\w-]+\.)+[\w-]{2,4}(,[\w\-\.\+]+@([\w-]+\.)+[\w-]{2,4})*$/'];
|
||||
$rules['inbound_mailbox_blacklist'] = ['sometimes', 'string', 'nullable', 'regex:/^[\w\-\.\+]+@([\w-]+\.)+[\w-]{2,4}(,[\w\-\.\+]+@([\w-]+\.)+[\w-]{2,4})*$/'];
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
@ -87,34 +98,44 @@ class UpdateCompanyRequest extends Request
|
||||
$input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/");
|
||||
}
|
||||
|
||||
// /** Disabled on the hosted platform */
|
||||
// if (isset($input['expense_mailbox']) && Ninja::isHosted() && !($this->company->account->isPaid() && $this->company->account->plan == 'enterprise')) {
|
||||
// unset($input['expense_mailbox']);
|
||||
// }
|
||||
|
||||
if (isset($input['settings'])) {
|
||||
$input['settings'] = (array)$this->filterSaveableSettings($input['settings']);
|
||||
$input['settings'] = (array) $this->filterSaveableSettings($input['settings']);
|
||||
}
|
||||
|
||||
if(isset($input['subdomain']) && $this->company->subdomain == $input['subdomain']) {
|
||||
if (isset($input['subdomain']) && $this->company->subdomain == $input['subdomain']) {
|
||||
unset($input['subdomain']);
|
||||
}
|
||||
|
||||
if(isset($input['e_invoice_certificate_passphrase']) && empty($input['e_invoice_certificate_passphrase'])) {
|
||||
if (isset($input['e_invoice_certificate_passphrase']) && empty($input['e_invoice_certificate_passphrase'])) {
|
||||
unset($input['e_invoice_certificate_passphrase']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_username']) && strlen(str_replace("*", "", $input['smtp_username'])) < 2) {
|
||||
if (isset($input['smtp_username']) && strlen(str_replace("*", "", $input['smtp_username'])) < 2) {
|
||||
unset($input['smtp_username']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) {
|
||||
if (isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) {
|
||||
unset($input['smtp_password']);
|
||||
}
|
||||
|
||||
if(isset($input['smtp_port'])) {
|
||||
$input['smtp_port'] = (int)$input['smtp_port'];
|
||||
if (isset($input['smtp_port'])) {
|
||||
$input['smtp_port'] = (int) $input['smtp_port'];
|
||||
}
|
||||
|
||||
if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) {
|
||||
if (isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) {
|
||||
$input['smtp_verify_peer'] == 'true' ? true : false;
|
||||
}
|
||||
|
||||
if (isset($input['e_invoice']) && is_array($input['e_invoice'])) {
|
||||
//ensure it is normalized first!
|
||||
$input['e_invoice'] = $this->company->filterNullsRecursive($input['e_invoice']);
|
||||
}
|
||||
|
||||
$this->replace($input);
|
||||
}
|
||||
|
||||
@ -139,21 +160,21 @@ class UpdateCompanyRequest extends Request
|
||||
}
|
||||
|
||||
if (isset($settings['email_style_custom'])) {
|
||||
$settings['email_style_custom'] = str_replace(['{!!','!!}','{{','}}','@checked','@dd', '@dump', '@if', '@if(','@endif','@isset','@unless','@auth','@empty','@guest','@env','@section','@switch', '@foreach', '@while', '@include', '@each', '@once', '@push', '@use', '@forelse', '@verbatim', '<?php', '@php', '@for','@class','</sc','<sc','html;base64', '@elseif', '@else', '@endunless', '@endisset', '@endempty', '@endauth', '@endguest', '@endproduction', '@endenv', '@hasSection', '@endhasSection', '@sectionMissing', '@endsectionMissing', '@endfor', '@endforeach', '@empty', '@endforelse', '@endwhile', '@continue', '@break', '@includeIf', '@includeWhen', '@includeUnless', '@includeFirst', '@component', '@endcomponent', '@endsection', '@yield', '@show', '@append', '@overwrite', '@stop', '@extends', '@endpush', '@stack', '@prepend', '@endprepend', '@slot', '@endslot', '@endphp', '@method', '@csrf', '@error', '@enderror', '@json', '@endverbatim', '@inject'], '', $settings['email_style_custom']);
|
||||
$settings['email_style_custom'] = str_replace(['{!!', '!!}', '{{', '}}', '@checked', '@dd', '@dump', '@if', '@if(', '@endif', '@isset', '@unless', '@auth', '@empty', '@guest', '@env', '@section', '@switch', '@foreach', '@while', '@include', '@each', '@once', '@push', '@use', '@forelse', '@verbatim', '<?php', '@php', '@for', '@class', '</sc', '<sc', 'html;base64', '@elseif', '@else', '@endunless', '@endisset', '@endempty', '@endauth', '@endguest', '@endproduction', '@endenv', '@hasSection', '@endhasSection', '@sectionMissing', '@endsectionMissing', '@endfor', '@endforeach', '@empty', '@endforelse', '@endwhile', '@continue', '@break', '@includeIf', '@includeWhen', '@includeUnless', '@includeFirst', '@component', '@endcomponent', '@endsection', '@yield', '@show', '@append', '@overwrite', '@stop', '@extends', '@endpush', '@stack', '@prepend', '@endprepend', '@slot', '@endslot', '@endphp', '@method', '@csrf', '@error', '@enderror', '@json', '@endverbatim', '@inject'], '', $settings['email_style_custom']);
|
||||
}
|
||||
|
||||
if(isset($settings['company_logo']) && strlen($settings['company_logo']) > 2) {
|
||||
if (isset($settings['company_logo']) && strlen($settings['company_logo']) > 2) {
|
||||
$settings['company_logo'] = $this->forceScheme($settings['company_logo']);
|
||||
}
|
||||
|
||||
if (! $account->isFreeHostedClient()) {
|
||||
if (!$account->isFreeHostedClient()) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$saveable_casts = CompanySettings::$free_plan_casts;
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
if (! array_key_exists($key, $saveable_casts)) {
|
||||
if (!array_key_exists($key, $saveable_casts)) {
|
||||
unset($settings->{$key});
|
||||
}
|
||||
}
|
||||
@ -165,7 +186,7 @@ class UpdateCompanyRequest extends Request
|
||||
{
|
||||
if (Ninja::isHosted()) {
|
||||
$url = str_replace('http://', '', $url);
|
||||
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme.$url : $url;
|
||||
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme . $url : $url;
|
||||
}
|
||||
|
||||
return rtrim($url, '/');
|
||||
|
@ -107,6 +107,7 @@ class StoreCreditRequest extends Request
|
||||
$input = $this->decodePrimaryKeys($input);
|
||||
|
||||
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
|
||||
$input['line_items'] = $this->cleanFeeItems($input['line_items']);
|
||||
$input['amount'] = $this->entityTotalAmount($input['line_items']);
|
||||
|
||||
if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) {
|
||||
|
28
app/Http/Requests/EInvoice/SignupRequest.php
Normal file
28
app/Http/Requests/EInvoice/SignupRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\EInvoice;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Http\Requests\Request;
|
||||
|
||||
class SignupRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Ninja::isSelfHost();
|
||||
}
|
||||
}
|
93
app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php
Normal file
93
app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\EInvoice;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Client;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Http\Requests\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ValidateEInvoiceRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$entity = $this->getEntity();
|
||||
|
||||
if($entity instanceof Company)
|
||||
return $entity->id == $user->company()->id;
|
||||
|
||||
return $user->can('view', $entity);
|
||||
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return [
|
||||
'entity' => 'required|bail|in:invoices,clients,companies',
|
||||
'entity_id' => ['required','bail', Rule::exists($this->entity, 'id')
|
||||
->when($this->entity != 'companies', function ($q) use($user){
|
||||
$q->where('company_id', $user->company()->id);
|
||||
})
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
||||
if (isset($input['entity_id']) && $input['entity_id'] != null) {
|
||||
$input['entity_id'] = $this->decodePrimaryKey($input['entity_id']);
|
||||
}
|
||||
|
||||
|
||||
$this->replace($input);
|
||||
}
|
||||
|
||||
public function getEntity()
|
||||
{
|
||||
if(!$this->entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$class = Invoice::class;
|
||||
|
||||
match ($this->entity) {
|
||||
'invoices' => $class = Invoice::class,
|
||||
'clients' => $class = Client::class,
|
||||
'companies' => $class = Company::class,
|
||||
default => $class = Invoice::class,
|
||||
};
|
||||
|
||||
if($this->entity == 'companies')
|
||||
return auth()->user()->company();
|
||||
|
||||
return $class::withTrashed()->find(is_string($this->entity_id) ? $this->decodePrimaryKey($this->entity_id) : $this->entity_id);
|
||||
|
||||
}
|
||||
}
|
@ -25,9 +25,9 @@ class EDocumentRequest extends Request
|
||||
$rules = [];
|
||||
|
||||
if ($this->file('documents') && is_array($this->file('documents'))) {
|
||||
$rules['documents.*'] = $this->fileValidation();
|
||||
$rules['documents.*'] = 'required|file|max:1000000';
|
||||
} elseif ($this->file('documents')) {
|
||||
$rules['documents'] = $this->fileValidation();
|
||||
$rules['documents'] = 'required|file|max:1000000';
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
@ -40,10 +40,11 @@ class BulkInvoiceRequest extends Request
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('action', 0)."|".json_encode($this->input('ids', ''))."|".$user->company()->company_key))
|
||||
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('action', 0)."|".$user->company()->company_key)) {
|
||||
throw new DuplicatePaymentException('Duplicate request.', 429);
|
||||
}
|
||||
|
||||
\Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->input('action', 0)."|".json_encode($this->input('ids', ''))."|".$user->company()->company_key), true, 1);
|
||||
\Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->input('action', 0)."|".$user->company()->company_key), true, 1);
|
||||
|
||||
}
|
||||
|
||||
|
@ -24,4 +24,23 @@ class DestroyInvoiceRequest extends Request
|
||||
{
|
||||
return auth()->user()->can('edit', $this->invoice);
|
||||
}
|
||||
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->invoice->id."|".$user->company()->company_key))
|
||||
throw new \App\Exceptions\DuplicatePaymentException('Duplicate request.', 429);
|
||||
|
||||
\Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->invoice->id."|".$user->company()->company_key), true, 1);
|
||||
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user