Compare commits
499 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
9e17c85f1b | |
|
|
64b6b203a4 | |
|
|
b80a95003f | |
|
|
6cf4fc6ab4 | |
|
|
dec5030268 | |
|
|
bb6482f70e | |
|
|
99fbe0bf2b | |
|
|
fc3ce17d15 | |
|
|
50eea3c6c8 | |
|
|
afa5962596 | |
|
|
392414ae07 | |
|
|
7a7b88721e | |
|
|
a1d8656d86 | |
|
|
02d53f749b | |
|
|
1ed1150703 | |
|
|
6e35257e9d | |
|
|
ae4bee9e2c | |
|
|
8a169c4774 | |
|
|
32b1ca8cb8 | |
|
|
c5c3271a4a | |
|
|
e9680d3a6b | |
|
|
590f5ef614 | |
|
|
4e9a756d52 | |
|
|
c7e69cda71 | |
|
|
718dda403e | |
|
|
315ee47482 | |
|
|
019a688047 | |
|
|
0d44ca6481 | |
|
|
d27b3ffc47 | |
|
|
04722bae9f | |
|
|
49c6de8161 | |
|
|
142bc03658 | |
|
|
03aa4ad334 | |
|
|
873fadc444 | |
|
|
ffceba342a | |
|
|
f297d93f84 | |
|
|
f3be27085a | |
|
|
d955f0b13d | |
|
|
ff628ae848 | |
|
|
d7c03afb0c | |
|
|
a0f5a8f254 | |
|
|
f7769a4358 | |
|
|
e35b47abfe | |
|
|
366b9fb118 | |
|
|
cb700c3c0c | |
|
|
3987c790c7 | |
|
|
00f1aa1da7 | |
|
|
24ba1f3620 | |
|
|
d55ebd433a | |
|
|
e1a3db272b | |
|
|
735ac1ae63 | |
|
|
1ad1a28b03 | |
|
|
3b4d108160 | |
|
|
fd0d31cfc0 | |
|
|
35687f25a0 | |
|
|
b22963b88f | |
|
|
0f6281f37a | |
|
|
e1fdce60cc | |
|
|
d47cf8873e | |
|
|
7a1d1eaae7 | |
|
|
eeb2c6c692 | |
|
|
dcd8681bd6 | |
|
|
5158fae577 | |
|
|
cdd5352b2e | |
|
|
c1471d1846 | |
|
|
a516ce30f1 | |
|
|
67fbd79228 | |
|
|
599daf445d | |
|
|
d4233e1667 | |
|
|
6bfbd646ca | |
|
|
0f97c23503 | |
|
|
ef0f6c38de | |
|
|
d804b920ae | |
|
|
9276fb7978 | |
|
|
577e91fca5 | |
|
|
77f2bb32cf | |
|
|
5fde1166c5 | |
|
|
a813f87fc2 | |
|
|
5dff442403 | |
|
|
f209c1228e | |
|
|
a922cbc577 | |
|
|
1cef456f4b | |
|
|
d9ed34c988 | |
|
|
556cfd4385 | |
|
|
f7c65b3cc9 | |
|
|
74b2b4c14b | |
|
|
80b6fd7690 | |
|
|
c6527b08f3 | |
|
|
9ae145d818 | |
|
|
2bea363d53 | |
|
|
1d8c100cbd | |
|
|
f006960b61 | |
|
|
7b3ed25195 | |
|
|
8c66b195ab | |
|
|
48f1d8395b | |
|
|
37632a6cb7 | |
|
|
ba25555a27 | |
|
|
f32a1ea7ba | |
|
|
56b1abcf54 | |
|
|
694f1476de | |
|
|
81093a3ee3 | |
|
|
6c09ed8a47 | |
|
|
4ec2808f64 | |
|
|
1c01fe5b02 | |
|
|
59ea3df9dd | |
|
|
2f8cf977b0 | |
|
|
92106c0637 | |
|
|
a9cbfb188d | |
|
|
2907752732 | |
|
|
a0745200d8 | |
|
|
f3263b9ce5 | |
|
|
340d5e1f6c | |
|
|
54e2826e37 | |
|
|
644a2d16a5 | |
|
|
31e13d4a7a | |
|
|
00e93582ea | |
|
|
0fb63dec60 | |
|
|
f9ac001580 | |
|
|
64036b9b6b | |
|
|
442a3a990a | |
|
|
cd3cf81f65 | |
|
|
226c881eef | |
|
|
392a7c7736 | |
|
|
e84f44fc15 | |
|
|
3b86e29062 | |
|
|
1b7f59cfc2 | |
|
|
cc4c93db8f | |
|
|
47827a033e | |
|
|
0d991036f4 | |
|
|
2903ac539c | |
|
|
f2cc228586 | |
|
|
3e0ba922b7 | |
|
|
1585ec57f9 | |
|
|
3e9f93f7e8 | |
|
|
4083ed82aa | |
|
|
426cc1f5ad | |
|
|
4e5e8d5187 | |
|
|
062f273da1 | |
|
|
821e0a357c | |
|
|
fa1d20dc8c | |
|
|
e9fb91cd31 | |
|
|
d26314ad06 | |
|
|
9fe81b0e55 | |
|
|
78a9a1baeb | |
|
|
f6696f83c8 | |
|
|
565858da6c | |
|
|
7e11066280 | |
|
|
f2a619c750 | |
|
|
487351c196 | |
|
|
e76b77afe8 | |
|
|
64d473605e | |
|
|
e41e03cd9f | |
|
|
903b279a5f | |
|
|
30dfcb01c8 | |
|
|
1c9fa73d35 | |
|
|
da81025b24 | |
|
|
aa657ee1af | |
|
|
fb83d2fec0 | |
|
|
521a68d732 | |
|
|
cccc1e75d6 | |
|
|
5be344f17e | |
|
|
a070d7e6a0 | |
|
|
334e9c0c78 | |
|
|
bb8d257b41 | |
|
|
b415d47bc5 | |
|
|
7e7d790aaf | |
|
|
2ed4414703 | |
|
|
dc52840418 | |
|
|
85e21221ac | |
|
|
53a88363fd | |
|
|
9f53eec993 | |
|
|
a19f94143c | |
|
|
d679ba93df | |
|
|
f254a2b105 | |
|
|
2bf0326797 | |
|
|
f5f447a3ff | |
|
|
ff280b4c5b | |
|
|
ee7b2d52c5 | |
|
|
3590e89c05 | |
|
|
e0bea26e52 | |
|
|
37d0f151b8 | |
|
|
16fa896d78 | |
|
|
e61a370b95 | |
|
|
35a1da1f23 | |
|
|
e01eddae26 | |
|
|
bb1a0120ab | |
|
|
25d0caa324 | |
|
|
adf1c1e96c | |
|
|
4e37a526d8 | |
|
|
cbb40bd3d1 | |
|
|
3fa404f4e8 | |
|
|
a6501b2dff | |
|
|
d324c20e2e | |
|
|
af94eab84d | |
|
|
a14b5862b7 | |
|
|
002666a308 | |
|
|
eedcdc8551 | |
|
|
00296a6671 | |
|
|
079e78b8d9 | |
|
|
ccce21c0ec | |
|
|
0b22141492 | |
|
|
0e45151927 | |
|
|
57114af04d | |
|
|
b08c575b35 | |
|
|
6f1ebf57a9 | |
|
|
8af6d67dd1 | |
|
|
bb839433eb | |
|
|
d5e2b0fde3 | |
|
|
1da785d4d0 | |
|
|
aa5dd5b4fa | |
|
|
16a380972f | |
|
|
b21c182eda | |
|
|
dc54d01fd1 | |
|
|
9aec1415d8 | |
|
|
3f391b868b | |
|
|
56670e221a | |
|
|
38c0d8dd95 | |
|
|
f51f0c0c37 | |
|
|
ee5509feca | |
|
|
f305fa3076 | |
|
|
8c6f8c3c1d | |
|
|
367c3582bd | |
|
|
a71e76313d | |
|
|
dddd33e6d1 | |
|
|
75e3c8800e | |
|
|
7fa48b6e2c | |
|
|
bf20b44cc1 | |
|
|
584c33de70 | |
|
|
f640417c25 | |
|
|
792790e3f0 | |
|
|
0d81da350b | |
|
|
6daba8822b | |
|
|
e999f9b8bb | |
|
|
466833ee53 | |
|
|
2161b0461f | |
|
|
b174656e87 | |
|
|
9e032ad723 | |
|
|
0e759a6afd | |
|
|
c3d5af190a | |
|
|
690ebf6f13 | |
|
|
db73210904 | |
|
|
42f8881231 | |
|
|
caa025b502 | |
|
|
8615d5bbc3 | |
|
|
efaceba367 | |
|
|
435bb8e999 | |
|
|
8f1566817e | |
|
|
89b2cbeedc | |
|
|
8505f52353 | |
|
|
26094010a8 | |
|
|
bde3da88b9 | |
|
|
15b90d7953 | |
|
|
c0db1de1de | |
|
|
ef5b692424 | |
|
|
9af00a87cf | |
|
|
abb4d128d0 | |
|
|
df3d02bfb7 | |
|
|
6b06bfc9b3 | |
|
|
117ff658ff | |
|
|
77fa0f9e03 | |
|
|
2e80af2b49 | |
|
|
c83ba701f4 | |
|
|
b8987af0af | |
|
|
f98411f81b | |
|
|
345e8fa2ce | |
|
|
9fc9ab933d | |
|
|
00db79a611 | |
|
|
adcffd0ce9 | |
|
|
87e413f558 | |
|
|
259f82e251 | |
|
|
00f4835ddd | |
|
|
aef0180a2f | |
|
|
a65bc07ffe | |
|
|
c6421bb733 | |
|
|
1892564325 | |
|
|
da44460fcb | |
|
|
2532c2ca61 | |
|
|
3204573cd0 | |
|
|
dbc4c1324e | |
|
|
8e58e4dc6c | |
|
|
75594a794b | |
|
|
3b63be39a9 | |
|
|
050048799c | |
|
|
a5cac9814b | |
|
|
d54f677c0b | |
|
|
9796525644 | |
|
|
78d93f5c21 | |
|
|
0cc0add29e | |
|
|
59a7ac132a | |
|
|
6d40c7da83 | |
|
|
f4b98b552c | |
|
|
212ca0c38c | |
|
|
03659e006b | |
|
|
a22fde7fc3 | |
|
|
62be991273 | |
|
|
c4de17d71c | |
|
|
2e45fbe9d5 | |
|
|
bc8c8ec17e | |
|
|
f2d904f2b1 | |
|
|
d6a9169628 | |
|
|
8d6e2df1e6 | |
|
|
769f132928 | |
|
|
42866de4c6 | |
|
|
6caa0b066d | |
|
|
fa36a24954 | |
|
|
40a22a3007 | |
|
|
a6e5675566 | |
|
|
3b9537ad55 | |
|
|
9083544c7b | |
|
|
d9404a1eb5 | |
|
|
38e6e88903 | |
|
|
6b5b988ed4 | |
|
|
318e301308 | |
|
|
557700307c | |
|
|
7bd40243e3 | |
|
|
12f31054c9 | |
|
|
cdc537e38d | |
|
|
6ccf88d5c7 | |
|
|
7a3e0d78f8 | |
|
|
fa563322b4 | |
|
|
d60f979df8 | |
|
|
83d6dc89ae | |
|
|
c9a3100067 | |
|
|
c2cca35964 | |
|
|
0a40d8e456 | |
|
|
22e7d34180 | |
|
|
e627a9ca0f | |
|
|
6a9e5f6166 | |
|
|
fd415889dd | |
|
|
8f98033e91 | |
|
|
b970003353 | |
|
|
9300829ded | |
|
|
bb7c37ed44 | |
|
|
86096e4502 | |
|
|
bd1cefc299 | |
|
|
ebdf6f7919 | |
|
|
abe1a66310 | |
|
|
f5b9af8ba1 | |
|
|
fae786c3e4 | |
|
|
99f58ebd72 | |
|
|
37fc8a61d9 | |
|
|
a35c817388 | |
|
|
e07af07fa4 | |
|
|
daff064af7 | |
|
|
4c081c428f | |
|
|
2dd324c0bf | |
|
|
8c34d0cc5c | |
|
|
0a0978234d | |
|
|
86fb8aeaaa | |
|
|
b1d135387b | |
|
|
5c2eb2fb7c | |
|
|
e73348b69b | |
|
|
5d80798034 | |
|
|
55a11fef05 | |
|
|
0f402d9ee1 | |
|
|
edeb86fe7b | |
|
|
9aa544e66b | |
|
|
dcd03d9990 | |
|
|
3c4b544235 | |
|
|
90a4bcec4d | |
|
|
12df5ea94c | |
|
|
c79f595439 | |
|
|
adf8232147 | |
|
|
40f2741407 | |
|
|
52875f13a5 | |
|
|
1301b96ee5 | |
|
|
5c798e9516 | |
|
|
ccf35c35ab | |
|
|
df57be359a | |
|
|
5ff1b60381 | |
|
|
bb77d935a7 | |
|
|
1cc41ed2cb | |
|
|
cde3ca8af5 | |
|
|
3b63cd4cb3 | |
|
|
e2ca5281c4 | |
|
|
8b9b344c35 | |
|
|
97a43fb4bf | |
|
|
e566f83adc | |
|
|
876a025e7c | |
|
|
3319fd7920 | |
|
|
331be9d309 | |
|
|
2c3ac4aad9 | |
|
|
4eb5582a64 | |
|
|
33397aaf61 | |
|
|
f6bc016f22 | |
|
|
f6aac6b424 | |
|
|
afa4bc461c | |
|
|
1f62b24e25 | |
|
|
f7250f643a | |
|
|
bf3e802dcc | |
|
|
db326941d5 | |
|
|
2222361cca | |
|
|
d3f6f642f2 | |
|
|
f02c6bb277 | |
|
|
f0af52c017 | |
|
|
4d63b1336a | |
|
|
18e46d3c88 | |
|
|
0f24a1dd54 | |
|
|
1e5dadaba4 | |
|
|
faa9193ca6 | |
|
|
06d3255ce0 | |
|
|
4d126a7d1f | |
|
|
c38d107fdb | |
|
|
c8032c834e | |
|
|
d7ddbda9eb | |
|
|
ad10ad0145 | |
|
|
e09c908db4 | |
|
|
37a4f5ba62 | |
|
|
63d5a4ca52 | |
|
|
4feca756b1 | |
|
|
d344d2c702 | |
|
|
9c8544b977 | |
|
|
65a642b143 | |
|
|
e083aac531 | |
|
|
9094000f16 | |
|
|
fb0692b91c | |
|
|
a27d311ae8 | |
|
|
783e589444 | |
|
|
dcb58821ef | |
|
|
56b3f0264c | |
|
|
ab1d91d755 | |
|
|
29bfbaf644 | |
|
|
27dd9907f3 | |
|
|
5a188ac355 | |
|
|
cfc41eb29a | |
|
|
e58f19f593 | |
|
|
e8336c85d7 | |
|
|
117709e551 | |
|
|
70dea557f5 | |
|
|
084f0fea25 | |
|
|
256555c952 | |
|
|
9b98d45d3b | |
|
|
c3b5a972a1 | |
|
|
7e1a6bc1c7 | |
|
|
0a7744a70e | |
|
|
1252cdf7ae | |
|
|
af926a394c | |
|
|
c5c9c4325e | |
|
|
3d3b5f6938 | |
|
|
ff92756dbc | |
|
|
a141ca1549 | |
|
|
bf8041ab7c | |
|
|
8bc1513591 | |
|
|
63e6f75a24 | |
|
|
5482f44bea | |
|
|
81ec3986ca | |
|
|
1a3badf748 | |
|
|
c7e79fe673 | |
|
|
8d23ba14d4 | |
|
|
1836ccc434 | |
|
|
94b628b6eb | |
|
|
67df175525 | |
|
|
47f33c8691 | |
|
|
6a0fff10ae | |
|
|
a447b6a20b | |
|
|
f7961ecb61 | |
|
|
37c74ee18c | |
|
|
555eb80018 | |
|
|
1e8727c4a4 | |
|
|
74f71d61d6 | |
|
|
8a137329d4 | |
|
|
c02c87765b | |
|
|
7393360db3 | |
|
|
f7055b516e | |
|
|
ee775e58a0 | |
|
|
5ff70dbeae | |
|
|
3791469c31 | |
|
|
1a86d5445b | |
|
|
442ff42ceb | |
|
|
b94316dbed | |
|
|
14fd4063f5 | |
|
|
97f2e70f5d | |
|
|
edd0de38ca | |
|
|
d53e1012af | |
|
|
5895c1b0ed | |
|
|
aa918f7ec0 | |
|
|
33078ee86c | |
|
|
6c8c270c2f | |
|
|
5afd3b85bc | |
|
|
dab787c3ae | |
|
|
03a39f33b8 | |
|
|
cbc5cb5f9b | |
|
|
4127eb32f9 | |
|
|
bf5359cb72 | |
|
|
d42735f2ee | |
|
|
ea663394b1 | |
|
|
b93ec6dd93 | |
|
|
a431dd43d4 | |
|
|
1c5c568251 | |
|
|
604ba82f8f | |
|
|
c8e0cdd090 | |
|
|
54ba4349f6 | |
|
|
0865ab3c3e | |
|
|
fc8cb7af36 | |
|
|
46de411160 | |
|
|
ff50e3c9d9 | |
|
|
fdf7d2d3cf | |
|
|
dd60eb3b58 | |
|
|
1f4fae314c |
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
- name: Install composer dependencies
|
||||
run: |
|
||||
composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }}
|
||||
composer install --no-dev
|
||||
composer install --no-dev -o
|
||||
|
||||
- name: Set current date to variable
|
||||
id: set_date
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
5.12.23
|
||||
5.12.36
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www/elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use App\DataMapper\InvoiceBackup;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
class InvoiceBackupCast implements CastsAttributes
|
||||
{
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return new InvoiceBackup();
|
||||
}
|
||||
|
||||
$data = json_decode($value, true) ?? [];
|
||||
|
||||
return InvoiceBackup::fromArray($data);
|
||||
}
|
||||
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return [$key => null];
|
||||
}
|
||||
|
||||
// Ensure we're dealing with our object type
|
||||
if (! $value instanceof InvoiceBackup) {
|
||||
// Attempt to create the instance from legacy data before throwing
|
||||
try {
|
||||
if (is_object($value)) {
|
||||
$value = InvoiceBackup::fromArray((array) $value);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new \InvalidArgumentException('Value must be an InvoiceBackup instance. Legacy data conversion failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
$key => json_encode([
|
||||
'guid' => $value->guid,
|
||||
'cancellation' => $value->cancellation ? [
|
||||
'adjustment' => $value->cancellation->adjustment,
|
||||
'status_id' => $value->cancellation->status_id,
|
||||
] : [],
|
||||
'parent_invoice_id' => $value->parent_invoice_id,
|
||||
'parent_invoice_number' => $value->parent_invoice_number,
|
||||
'document_type' => $value->document_type,
|
||||
'child_invoice_ids' => $value->child_invoice_ids->toArray(),
|
||||
'redirect' => $value->redirect,
|
||||
'adjustable_amount' => $value->adjustable_amount,
|
||||
'notes' => $value->notes,
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ class CheckData extends Command
|
|||
private function checkTaskTimeLogs()
|
||||
{
|
||||
\App\Models\Task::query()->cursor()->each(function ($task) {
|
||||
$time_log = json_decode($task->time_log, true);
|
||||
$time_log = json_decode($task->time_log, true) ?? [];
|
||||
|
||||
foreach($time_log as &$log){
|
||||
if(count($log) > 4){
|
||||
|
|
|
|||
|
|
@ -0,0 +1,437 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Elastic;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Project;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\Quote;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Models\Task;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\VendorContact;
|
||||
use Elastic\Elasticsearch\ClientBuilder;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class RebuildElasticIndexes extends Command
|
||||
{
|
||||
protected $signature = 'elastic:rebuild
|
||||
{--model= : Rebuild only a specific model (e.g., Invoice, Client)}
|
||||
{--force : Force the operation without confirmation}
|
||||
{--dry-run : Show what would be done without making changes}
|
||||
{--chunk=500 : Number of records to import per chunk}
|
||||
{--wait : Wait for our queued jobs to complete after each model (recommended for production)}
|
||||
{--no-queue : Import synchronously instead of queueing (slower but safer)}';
|
||||
|
||||
protected $description = 'Rebuild Elasticsearch indexes one at a time to minimize production impact';
|
||||
|
||||
protected array $searchableModels = [
|
||||
Client::class => 'clients_v2',
|
||||
ClientContact::class => 'client_contacts_v2',
|
||||
Credit::class => 'credits_v2',
|
||||
Expense::class => 'expenses_v2',
|
||||
Invoice::class => 'invoices_v2',
|
||||
Project::class => 'projects_v2',
|
||||
PurchaseOrder::class => 'purchase_orders_v2',
|
||||
Quote::class => 'quotes_v2',
|
||||
RecurringInvoice::class => 'recurring_invoices_v2',
|
||||
Task::class => 'tasks_v2',
|
||||
Vendor::class => 'vendors_v2',
|
||||
VendorContact::class => 'vendor_contacts_v2',
|
||||
];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('===========================================');
|
||||
$this->info(' Elasticsearch Index Rebuild (One-by-One)');
|
||||
$this->info('===========================================');
|
||||
$this->newLine();
|
||||
|
||||
if (!$this->checkElasticsearchConnection()) {
|
||||
$this->error('Cannot connect to Elasticsearch. Please check your configuration.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($modelName = $this->option('model')) {
|
||||
return $this->rebuildSingleModel($modelName);
|
||||
}
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
return $this->performDryRun();
|
||||
}
|
||||
|
||||
if (!$this->option('force')) {
|
||||
$this->warn('This command will rebuild ALL Elasticsearch indexes ONE AT A TIME:');
|
||||
$this->info(' • Each index will be dropped, migrated, and re-imported sequentially');
|
||||
$this->info(' • Search will be unavailable for each model during its rebuild');
|
||||
$this->info(' • Other models remain searchable while one rebuilds');
|
||||
|
||||
if ($this->option('wait')) {
|
||||
$this->info(' • Will WAIT for our jobs to complete (tracks pending + processing)');
|
||||
} else {
|
||||
$this->warn(' • WARNING: Jobs will queue up async (use --wait for production)');
|
||||
}
|
||||
|
||||
if ($this->option('no-queue')) {
|
||||
$this->info(' • Using SYNCHRONOUS import (slower but guaranteed)');
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
|
||||
$totalRecords = $this->getTotalRecordCount();
|
||||
$this->warn("Total records to re-index: {$totalRecords}");
|
||||
$this->newLine();
|
||||
|
||||
if (!$this->confirm('Do you want to rebuild all indexes?', false)) {
|
||||
$this->info('Operation cancelled.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
|
||||
$totalModels = count($this->searchableModels);
|
||||
$currentModel = 0;
|
||||
$startTime = now();
|
||||
|
||||
foreach ($this->searchableModels as $modelClass => $indexName) {
|
||||
$currentModel++;
|
||||
$modelName = class_basename($modelClass);
|
||||
|
||||
$this->newLine();
|
||||
$this->info("[{$currentModel}/{$totalModels}] Rebuilding {$modelName}...");
|
||||
$this->line("Index: {$indexName}");
|
||||
|
||||
if (!$this->rebuildIndex($modelClass, $indexName)) {
|
||||
$this->error("Failed to rebuild {$modelName}. Stopping.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$duration = now()->diffForHumans($startTime, true);
|
||||
$this->info('✓ All indexes rebuilt successfully!');
|
||||
$this->info("Total time: {$duration}");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
protected function rebuildSingleModel(string $modelName): int
|
||||
{
|
||||
$modelClass = null;
|
||||
foreach ($this->searchableModels as $class => $indexName) {
|
||||
if (class_basename($class) === $modelName) {
|
||||
$modelClass = $class;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$modelClass) {
|
||||
$this->error("Model '{$modelName}' not found.");
|
||||
$this->info('Available models: ' . implode(', ', array_map('class_basename', array_keys($this->searchableModels))));
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$indexName = $this->searchableModels[$modelClass];
|
||||
|
||||
try {
|
||||
$recordCount = $modelClass::count();
|
||||
} catch (\Exception $e) {
|
||||
$this->warn("Could not count records: " . $e->getMessage());
|
||||
$recordCount = 0;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Rebuilding {$modelName}");
|
||||
$this->line("Index: {$indexName}");
|
||||
$this->line("Records: {$recordCount}");
|
||||
$this->line("Chunk size: {$this->option('chunk')}");
|
||||
$this->line("Mode: " . ($this->option('no-queue') ? 'Synchronous' : 'Queued'));
|
||||
$this->newLine();
|
||||
|
||||
if (!$this->option('force') && !$this->option('dry-run')) {
|
||||
if (!$this->confirm('Continue with rebuild?', true)) {
|
||||
$this->info('Operation cancelled.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
$this->info('DRY RUN - Would rebuild:');
|
||||
$this->line(" 1. Drop index: {$indexName}");
|
||||
$this->line(" 2. Run elastic:migrate for {$modelName}");
|
||||
$this->line(" 3. Import {$recordCount} {$modelName} records");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$startTime = now();
|
||||
|
||||
if ($this->rebuildIndex($modelClass, $indexName)) {
|
||||
$duration = now()->diffForHumans($startTime, true);
|
||||
$this->newLine();
|
||||
$this->info("✓ {$modelName} rebuilt successfully in {$duration}!");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error("✗ Failed to rebuild {$modelName}");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
protected function performDryRun(): int
|
||||
{
|
||||
$this->info('DRY RUN - The following would be rebuilt:');
|
||||
$this->newLine();
|
||||
|
||||
foreach ($this->searchableModels as $modelClass => $indexName) {
|
||||
$modelName = class_basename($modelClass);
|
||||
|
||||
try {
|
||||
$recordCount = $modelClass::count();
|
||||
} catch (\Exception $e) {
|
||||
$recordCount = 0;
|
||||
}
|
||||
|
||||
$this->line("• {$modelName}");
|
||||
$this->line(" Index: {$indexName}");
|
||||
$this->line(" Records: {$recordCount}");
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
$this->info('No changes made (dry run mode)');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
protected function rebuildIndex(string $modelClass, string $indexName): bool
|
||||
{
|
||||
$modelName = class_basename($modelClass);
|
||||
$client = $this->getElasticsearchClient();
|
||||
|
||||
try {
|
||||
$this->line(" [1/3] Dropping index {$indexName}...");
|
||||
|
||||
try {
|
||||
$indexExists = $client->indices()->exists(['index' => $indexName]);
|
||||
|
||||
if ($indexExists) {
|
||||
try {
|
||||
$client->indices()->delete(['index' => $indexName]);
|
||||
$this->info(" ✓ Index dropped");
|
||||
} catch (\Exception $deleteException) {
|
||||
$this->warn(" ⚠ Failed to delete index: " . $deleteException->getMessage());
|
||||
$this->line(" - Continuing with migration...", 'comment');
|
||||
}
|
||||
} else {
|
||||
$this->line(" - Index does not exist (will be created)", 'comment');
|
||||
}
|
||||
} catch (\Exception $existsException) {
|
||||
$this->warn(" ⚠ Could not check index existence: " . $existsException->getMessage());
|
||||
$this->line(" - Continuing with migration...", 'comment');
|
||||
}
|
||||
|
||||
$this->line(" [2/3] Running elastic migration...");
|
||||
|
||||
try {
|
||||
Artisan::call('elastic:migrate', [], $this->getOutput());
|
||||
$this->info(" ✓ Migration completed");
|
||||
} catch (\Exception $migrateException) {
|
||||
$this->error(" ✗ Migration failed: " . $migrateException->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->line(" [3/3] Importing {$modelName} data...");
|
||||
|
||||
try {
|
||||
$recordCount = $modelClass::count();
|
||||
} catch (\Exception $countException) {
|
||||
$this->warn(" ⚠ Could not count records: " . $countException->getMessage());
|
||||
$recordCount = 0;
|
||||
}
|
||||
|
||||
if ($recordCount > 0) {
|
||||
try {
|
||||
if ($this->option('no-queue')) {
|
||||
$this->line(" - Using synchronous import (no queue)", 'comment');
|
||||
$this->importSynchronously($modelClass, $recordCount);
|
||||
} else {
|
||||
$this->importWithQueueTracking($modelClass, $recordCount);
|
||||
}
|
||||
$this->info(" ✓ Import completed for {$recordCount} records");
|
||||
} catch (\Exception $importException) {
|
||||
$this->error(" ✗ Import failed: " . $importException->getMessage());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$this->line(" - No records to import", 'comment');
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(" ✗ Unexpected error: " . $e->getMessage());
|
||||
$this->line(" Stack trace: " . $e->getTraceAsString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function importSynchronously(string $modelClass, int $totalRecords): void
|
||||
{
|
||||
$chunkSize = (int) $this->option('chunk');
|
||||
$chunks = ceil($totalRecords / $chunkSize);
|
||||
$processed = 0;
|
||||
|
||||
$this->line(" - Processing {$chunks} chunks of {$chunkSize} records each", 'comment');
|
||||
|
||||
$modelClass::chunk($chunkSize, function ($models) use (&$processed, $totalRecords) {
|
||||
$models->searchable();
|
||||
$processed += $models->count();
|
||||
$percentage = round(($processed / $totalRecords) * 100);
|
||||
$this->line(" - Indexed {$processed}/{$totalRecords} ({$percentage}%)", 'comment');
|
||||
});
|
||||
}
|
||||
|
||||
protected function importWithQueueTracking(string $modelClass, int $recordCount): void
|
||||
{
|
||||
$chunkSize = (int) $this->option('chunk');
|
||||
$expectedJobCount = ceil($recordCount / $chunkSize);
|
||||
|
||||
$queueName = config('scout.queue.queue', 'scout');
|
||||
$connection = config('scout.queue.connection', config('queue.default'));
|
||||
|
||||
try {
|
||||
$baselineJobCount = $this->getTotalActiveJobCount($connection, $queueName);
|
||||
} catch (\Exception $e) {
|
||||
$baselineJobCount = 0;
|
||||
$this->line(" - Cannot track queue baseline: " . $e->getMessage(), 'comment');
|
||||
}
|
||||
|
||||
$this->line(" - Baseline active jobs: {$baselineJobCount} (pending + processing)", 'comment');
|
||||
$this->line(" - Dispatching ~{$expectedJobCount} import jobs (chunks of {$chunkSize})", 'comment');
|
||||
|
||||
Artisan::call('scout:import', [
|
||||
'model' => $modelClass,
|
||||
'--chunk' => $chunkSize,
|
||||
], $this->getOutput());
|
||||
|
||||
$this->line(" - Jobs dispatched to queue", 'comment');
|
||||
|
||||
if ($this->option('wait')) {
|
||||
$this->waitForOurJobsToComplete($connection, $queueName, $baselineJobCount, $expectedJobCount);
|
||||
}
|
||||
}
|
||||
|
||||
protected function waitForOurJobsToComplete(
|
||||
string $connection,
|
||||
string $queueName,
|
||||
int $baselineJobCount,
|
||||
int $expectedJobCount
|
||||
): void {
|
||||
$this->newLine();
|
||||
$this->line(" Waiting for our {$expectedJobCount} jobs to complete...");
|
||||
$this->line(" (Tracking: pending + processing jobs)", 'comment');
|
||||
|
||||
$startTime = time();
|
||||
$lastReportedDelta = -1;
|
||||
$stableCount = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
$currentJobCount = $this->getTotalActiveJobCount($connection, $queueName);
|
||||
$delta = $currentJobCount - $baselineJobCount;
|
||||
|
||||
if ($currentJobCount <= $baselineJobCount) {
|
||||
$this->info(" ✓ Our jobs completed (active: {$currentJobCount}, baseline: {$baselineJobCount})");
|
||||
return;
|
||||
}
|
||||
|
||||
if ($delta !== $lastReportedDelta) {
|
||||
$this->line(" - Our jobs remaining: ~{$delta} (total active: {$currentJobCount})", 'comment');
|
||||
$lastReportedDelta = $delta;
|
||||
$stableCount = 0;
|
||||
} else {
|
||||
$stableCount++;
|
||||
}
|
||||
|
||||
if ($stableCount >= 15 && $delta <= $expectedJobCount) {
|
||||
$this->info(" ✓ Queue stabilized - assuming complete");
|
||||
return;
|
||||
}
|
||||
|
||||
sleep(2);
|
||||
} catch (\Exception $e) {
|
||||
$this->warn(" ⚠ Could not check queue status: " . $e->getMessage());
|
||||
$this->line(" - Waiting 10 seconds before continuing...", 'comment');
|
||||
sleep(10);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getTotalActiveJobCount(string $connection, string $queueName): int
|
||||
{
|
||||
$driver = config("queue.connections.{$connection}.driver");
|
||||
|
||||
switch ($driver) {
|
||||
case 'database':
|
||||
// Count both pending (reserved_at IS NULL) and processing (reserved_at IS NOT NULL)
|
||||
return DB::table(config("queue.connections.{$connection}.table", 'jobs'))
|
||||
->where('queue', $queueName)
|
||||
->count();
|
||||
|
||||
case 'redis':
|
||||
// Redis: pending jobs in list + reserved jobs in processing set
|
||||
$redis = Redis::connection('sentinel-default');
|
||||
$prefix = config('database.redis.options.prefix', '');
|
||||
|
||||
// Pending jobs in the queue list
|
||||
$pending = $redis->llen($prefix . 'queues:' . $queueName);
|
||||
|
||||
// Processing jobs in the reserved set
|
||||
$processing = $redis->zcard($prefix . 'queues:' . $queueName . ':reserved');
|
||||
|
||||
return $pending + $processing;
|
||||
|
||||
case 'sync':
|
||||
return 0;
|
||||
|
||||
default:
|
||||
throw new \Exception("Cannot check queue size for driver: {$driver}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function getTotalRecordCount(): int
|
||||
{
|
||||
$total = 0;
|
||||
foreach ($this->searchableModels as $modelClass => $indexName) {
|
||||
try {
|
||||
$total += $modelClass::count();
|
||||
} catch (\Exception $e) {
|
||||
// Skip if model fails to count
|
||||
}
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
protected function checkElasticsearchConnection(): bool
|
||||
{
|
||||
try {
|
||||
$client = $this->getElasticsearchClient();
|
||||
$client->ping();
|
||||
$this->info('✓ Elasticsearch connection successful');
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('✗ Elasticsearch connection failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getElasticsearchClient()
|
||||
{
|
||||
return ClientBuilder::fromConfig(config('elastic.client.connections.default'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper\Analytics;
|
||||
|
||||
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
|
||||
|
||||
class FeedbackCreated 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 = 'app.feedback';
|
||||
|
||||
/**
|
||||
* 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 = '';
|
||||
|
||||
/**
|
||||
* The exception string
|
||||
* set to 0.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $string_metric6 = '';
|
||||
|
||||
/**
|
||||
* 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($int_metric1, $string_metric5, $string_metric6, $string_metric7, $string_metric8)
|
||||
{
|
||||
$this->int_metric1 = $int_metric1;
|
||||
$this->string_metric5 = $string_metric5;
|
||||
$this->string_metric6 = $string_metric6;
|
||||
$this->string_metric7 = $string_metric7;
|
||||
$this->string_metric8 = $string_metric8;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper\Analytics;
|
||||
|
||||
use Turbo124\Beacon\ExampleMetric\GenericStructuredMetric;
|
||||
|
||||
class PeppolRegistration extends GenericStructuredMetric
|
||||
{
|
||||
/**
|
||||
* The type of Sample.
|
||||
*
|
||||
* Monotonically incrementing counter
|
||||
*
|
||||
* - counter
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type = 'structured_metric';
|
||||
|
||||
/**
|
||||
* The name of the counter.
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'peppol.registration';
|
||||
|
||||
/**
|
||||
* The datetime of the counter measurement.
|
||||
*
|
||||
* date("Y-m-d H:i:s")
|
||||
*
|
||||
*/
|
||||
public $datetime;
|
||||
|
||||
/**
|
||||
* HTML content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html = '';
|
||||
|
||||
/**
|
||||
* JSON data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $json = [];
|
||||
|
||||
/**
|
||||
* Initialize with either HTML or JSON content
|
||||
*
|
||||
* @param string|null $html HTML content
|
||||
* @param array|null $json JSON data
|
||||
*/
|
||||
public function __construct(?string $html = null, ?array $json = null)
|
||||
{
|
||||
if ($html !== null) {
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
if ($json !== null) {
|
||||
$this->json = $json;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper\Analytics;
|
||||
|
||||
use Turbo124\Beacon\ExampleMetric\GenericStructuredMetric;
|
||||
|
||||
class VerifactuLog extends GenericStructuredMetric
|
||||
{
|
||||
/**
|
||||
* The type of Sample.
|
||||
*
|
||||
* Monotonically incrementing counter
|
||||
*
|
||||
* - counter
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type = 'structured_metric';
|
||||
|
||||
/**
|
||||
* The name of the counter.
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'verifactu.log';
|
||||
|
||||
/**
|
||||
* The datetime of the counter measurement.
|
||||
*
|
||||
* date("Y-m-d H:i:s")
|
||||
*
|
||||
*/
|
||||
public $datetime;
|
||||
|
||||
/**
|
||||
* HTML content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html = '';
|
||||
|
||||
/**
|
||||
* JSON data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $json = [];
|
||||
|
||||
/**
|
||||
* Initialize with either HTML or JSON content
|
||||
*
|
||||
* @param string|null $html HTML content
|
||||
* @param array|null $json JSON data
|
||||
*/
|
||||
public function __construct(?string $html = null, ?array $json = null)
|
||||
{
|
||||
if ($html !== null) {
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
if ($json !== null) {
|
||||
$this->json = $json;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper\Billing;
|
||||
|
||||
use Turbo124\Beacon\ExampleMetric\GenericStructuredMetric;
|
||||
|
||||
class AppleLiapWebhook extends GenericStructuredMetric
|
||||
{
|
||||
/**
|
||||
* The type of Sample.
|
||||
*
|
||||
* Monotonically incrementing counter
|
||||
*
|
||||
* - counter
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type = 'structured_metric';
|
||||
|
||||
/**
|
||||
* The name of the counter.
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'apple.liap.webhook';
|
||||
|
||||
/**
|
||||
* The datetime of the counter measurement.
|
||||
*
|
||||
* date("Y-m-d H:i:s")
|
||||
*
|
||||
*/
|
||||
public $datetime;
|
||||
|
||||
/**
|
||||
* HTML content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html = '';
|
||||
|
||||
/**
|
||||
* JSON data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $json = [];
|
||||
|
||||
/**
|
||||
* Initialize with either HTML or JSON content
|
||||
*
|
||||
* @param string|null $html HTML content
|
||||
* @param array|null $json JSON data
|
||||
*/
|
||||
public function __construct(?string $html = null, ?array $json = null)
|
||||
{
|
||||
if ($html !== null) {
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
if ($json !== null) {
|
||||
$this->json = $json;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper\Billing;
|
||||
|
||||
use Turbo124\Beacon\ExampleMetric\GenericStructuredMetric;
|
||||
|
||||
class GoogleLiapWebhook extends GenericStructuredMetric
|
||||
{
|
||||
/**
|
||||
* The type of Sample.
|
||||
*
|
||||
* Monotonically incrementing counter
|
||||
*
|
||||
* - counter
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type = 'structured_metric';
|
||||
|
||||
/**
|
||||
* The name of the counter.
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'google.liap.webhook';
|
||||
|
||||
/**
|
||||
* The datetime of the counter measurement.
|
||||
*
|
||||
* date("Y-m-d H:i:s")
|
||||
*
|
||||
*/
|
||||
public $datetime;
|
||||
|
||||
/**
|
||||
* HTML content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html = '';
|
||||
|
||||
/**
|
||||
* JSON data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $json = [];
|
||||
|
||||
/**
|
||||
* Initialize with either HTML or JSON content
|
||||
*
|
||||
* @param string|null $html HTML content
|
||||
* @param array|null $json JSON data
|
||||
*/
|
||||
public function __construct(?string $html = null, ?array $json = null)
|
||||
{
|
||||
if ($html !== null) {
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
if ($json !== null) {
|
||||
$this->json = $json;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www/elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper;
|
||||
|
||||
/**
|
||||
* Cancellation value object for invoice backup data.
|
||||
*/
|
||||
class Cancellation
|
||||
{
|
||||
public function __construct(
|
||||
public float $adjustment = 0, // The cancellation adjustment amount
|
||||
public int $status_id = 0 //The status id of the invoice when it was cancelled
|
||||
) {}
|
||||
|
||||
public static function fromArray(array|object $data): self
|
||||
{
|
||||
if (is_object($data)) {
|
||||
$data = (array) $data;
|
||||
}
|
||||
|
||||
return new self(
|
||||
adjustment: $data['adjustment'] ?? 0,
|
||||
status_id: $data['status_id'] ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -479,7 +479,7 @@ class CompanySettings extends BaseSettings
|
|||
|
||||
public $sync_invoice_quote_columns = true;
|
||||
|
||||
public $e_invoice_type = 'EN16931';
|
||||
public $e_invoice_type = 'EN16931'; //verifactu
|
||||
|
||||
public $e_quote_type = 'OrderX_Comfort';
|
||||
|
||||
|
|
@ -963,6 +963,7 @@ class CompanySettings extends BaseSettings
|
|||
{
|
||||
$variables = [
|
||||
'client_details' => [
|
||||
'$client.location_name',
|
||||
'$client.name',
|
||||
'$client.number',
|
||||
'$client.vat_number',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www/elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper;
|
||||
|
||||
use App\Casts\InvoiceBackupCast;
|
||||
use App\DataMapper\Cancellation;
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* InvoiceBackup.
|
||||
*/
|
||||
class InvoiceBackup implements Castable
|
||||
{
|
||||
/**
|
||||
* @param string $guid - The E-INVOICE SENT GUID reference - or enum to advise the document has been successfully sent.
|
||||
* @param Cancellation $cancellation The cancellation data for the invoice.
|
||||
* @param string $parent_invoice_id The id of the invoice that was cancelled
|
||||
* @param string $parent_invoice_number The number of the invoice that was cancelled
|
||||
* @param string $document_type The type of document the invoice is - F1, R2, R1
|
||||
* @param Collection $child_invoice_ids The collection of child invoice IDs
|
||||
* @param string $redirect The redirect url for the invoice
|
||||
* @param float $adjustable_amount The adjustable amount for the invoice
|
||||
* @param string $notes The notes field - can be multi purpose, but general usage for Verifactu cancellation reason
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
public string $guid = '',
|
||||
public Cancellation $cancellation = new Cancellation(0,0),
|
||||
public ?string $parent_invoice_id = null,
|
||||
public ?string $parent_invoice_number = null,
|
||||
public ?string $document_type = null,
|
||||
public Collection $child_invoice_ids = new Collection(),
|
||||
public ?string $redirect = null,
|
||||
public float $adjustable_amount = 0,
|
||||
public ?string $notes = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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 InvoiceBackupCast::class;
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
guid: $data['guid'] ?? '',
|
||||
cancellation: Cancellation::fromArray($data['cancellation'] ?? []),
|
||||
parent_invoice_id: $data['parent_invoice_id'] ?? null,
|
||||
parent_invoice_number: $data['parent_invoice_number'] ?? null,
|
||||
document_type: $data['document_type'] ?? null,
|
||||
child_invoice_ids: isset($data['child_invoice_ids']) ? collect($data['child_invoice_ids']) : new Collection(),
|
||||
redirect: $data['redirect'] ?? null,
|
||||
adjustable_amount: $data['adjustable_amount'] ?? 0,
|
||||
notes: $data['notes'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a child invoice ID to the collection
|
||||
*/
|
||||
public function addChildInvoiceId(string $invoiceId): void
|
||||
{
|
||||
$this->child_invoice_ids->push($invoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a child invoice ID from the collection
|
||||
*/
|
||||
public function removeChildInvoiceId(string $invoiceId): void
|
||||
{
|
||||
$this->child_invoice_ids = $this->child_invoice_ids->reject($invoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a child invoice ID exists
|
||||
*/
|
||||
public function hasChildInvoiceId(string $invoiceId): bool
|
||||
{
|
||||
return $this->child_invoice_ids->contains($invoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child invoice IDs as an array
|
||||
*/
|
||||
public function getChildInvoiceIds(): array
|
||||
{
|
||||
return $this->child_invoice_ids->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,4 +34,5 @@ class EmailRecord
|
|||
* @var string
|
||||
*/
|
||||
public string $entity_id = '';
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,13 +23,14 @@ class TaxDetail
|
|||
public string $country_nexus; // Country nexus (e.g. "US", "UK", "CA")
|
||||
public float $taxable_amount; // net amount exclusive of taxes
|
||||
public float $tax_amount; // total tax amount
|
||||
public float $tax_amount_paid; // Amount actually paid (Based on the payment history)
|
||||
public float $tax_amount_remaining; // Amount still pending
|
||||
public string $tax_status; // "collected", "pending", "refundable", "partially_paid", "adjustment"
|
||||
|
||||
// Adjustment-specific fields (used when tax_status is "adjustment")
|
||||
public ?string $adjustment_reason; // "invoice_cancelled", "tax_rate_change", "exemption_applied", "correction"
|
||||
public ?string $postal_code; // "invoice_cancelled", "tax_rate_change", "exemption_applied", "correction"
|
||||
public float $line_total;
|
||||
public float $total_tax;
|
||||
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->tax_name = $attributes['tax_name'] ?? '';
|
||||
|
|
@ -38,13 +39,12 @@ class TaxDetail
|
|||
$this->country_nexus = $attributes['country_nexus'] ?? '';
|
||||
$this->taxable_amount = $attributes['taxable_amount'] ?? 0.0;
|
||||
$this->tax_amount = $attributes['tax_amount'] ?? 0.0;
|
||||
$this->tax_amount_paid = $attributes['tax_amount_paid'] ?? 0.0;
|
||||
$this->tax_amount_remaining = $attributes['tax_amount_remaining'] ?? 0.0;
|
||||
$this->tax_status = $attributes['tax_status'] ?? 'pending';
|
||||
|
||||
// Adjustment fields
|
||||
$this->adjustment_reason = $attributes['adjustment_reason'] ?? null;
|
||||
|
||||
$this->postal_code = $attributes['postal_code'] ?? null;
|
||||
|
||||
$this->line_total = $attributes['line_total'] ?? 0.0;
|
||||
$this->total_tax = $attributes['total_tax'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
|
|
@ -56,10 +56,10 @@ class TaxDetail
|
|||
'country_nexus' => $this->country_nexus,
|
||||
'taxable_amount' => $this->taxable_amount,
|
||||
'tax_amount' => $this->tax_amount,
|
||||
'tax_amount_paid' => $this->tax_amount_paid,
|
||||
'tax_amount_remaining' => $this->tax_amount_remaining,
|
||||
'tax_status' => $this->tax_status,
|
||||
'adjustment_reason' => $this->adjustment_reason,
|
||||
'postal_code' => $this->postal_code,
|
||||
'line_total' => $this->line_total,
|
||||
'total_tax' => $this->total_tax,
|
||||
];
|
||||
|
||||
return $data;
|
||||
|
|
|
|||
|
|
@ -14,28 +14,37 @@ namespace App\DataMapper\TaxReport;
|
|||
|
||||
/**
|
||||
* Tax summary with totals for different tax states
|
||||
*
|
||||
* Represents the taxable amount and tax amount for an invoice in a specific period.
|
||||
* The meaning of these values depends on the status:
|
||||
*
|
||||
* - 'updated': Full invoice tax liability (accrual) or paid tax (cash)
|
||||
* - 'delta': Differential tax change from invoice updates
|
||||
* - 'adjustment': Tax change from payment refunds/deletions
|
||||
* - 'cancelled': Proportional tax on refunded/cancelled amount
|
||||
* - 'deleted': Full tax reversal
|
||||
* - 'reversed': Full tax reversal of credit note
|
||||
*/
|
||||
class TaxSummary
|
||||
{
|
||||
public float $total_taxes; // Tax collected and confirmed (ie. Invoice Paid)
|
||||
public float $total_paid; // Tax pending collection (Outstanding tax of balance owing)
|
||||
public float $taxable_amount;
|
||||
public float $tax_amount;
|
||||
public string $status;
|
||||
public float $adjustment;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->total_taxes = $attributes['total_taxes'] ?? 0.0;
|
||||
$this->total_paid = $attributes['total_paid'] ?? 0.0;
|
||||
$this->taxable_amount = $attributes['taxable_amount'] ?? 0.0;
|
||||
// Support both old and new property names for backwards compatibility during migration
|
||||
$this->tax_amount = $attributes['tax_amount'] ?? $attributes['tax_adjustment'] ?? $attributes['total_taxes'] ?? 0.0;
|
||||
$this->status = $attributes['status'] ?? 'updated';
|
||||
$this->adjustment = $attributes['adjustment'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'total_taxes' => $this->total_taxes,
|
||||
'total_paid' => $this->total_paid,
|
||||
'taxable_amount' => $this->taxable_amount,
|
||||
'tax_amount' => $this->tax_amount,
|
||||
'status' => $this->status,
|
||||
'adjustment' => $this->adjustment,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Elastic\Index;
|
||||
|
||||
class EntityIndex
|
||||
{
|
||||
|
||||
public array $mapping = [
|
||||
'properties' => [
|
||||
'id' => [ 'type' => 'text' ],
|
||||
'name' => [ 'type' => 'text' ],
|
||||
'hashed_id' => [ 'type' => 'text' ],
|
||||
'number' => [ 'type' => 'text' ],
|
||||
'is_deleted' => [ 'type' => 'boolean' ],
|
||||
'amount' => [ 'type' => 'long' ],
|
||||
'balance' => [ 'type' => 'long' ],
|
||||
'due_date' => [ 'type' => 'date' ],
|
||||
'date' => [ 'type' => 'date' ],
|
||||
'custom_value1' => [ 'type' => 'text' ],
|
||||
'custom_value2' => [ 'type' => 'text' ],
|
||||
'custom_value3' => [ 'type' => 'text' ],
|
||||
'custom_value4' => [ 'type' => 'text' ],
|
||||
'company_key' => [ 'type' => 'text' ],
|
||||
'po_number' => [ 'type' => 'text' ],
|
||||
'line_items' => [
|
||||
'type' => 'nested',
|
||||
'properties' => [
|
||||
'product_key' => [ 'type' => 'text' ],
|
||||
'notes' => [ 'type' => 'text' ],
|
||||
'cost' => [ 'type' => 'long' ],
|
||||
'product_cost' => [ 'type' => 'long' ],
|
||||
'is_amount_discount' => [ 'type' => 'boolean' ],
|
||||
'line_total' => [ 'type' => 'long' ],
|
||||
'gross_line_total' => [ 'type' => 'long' ],
|
||||
'tax_amount' => [ 'type' => 'long' ],
|
||||
'quantity' => [ 'type' => 'float' ],
|
||||
'discount' => [ 'type' => 'float' ],
|
||||
'tax_name1' => [ 'type' => 'text' ],
|
||||
'tax_rate1' => [ 'type' => 'float' ],
|
||||
'tax_name2' => [ 'type' => 'text' ],
|
||||
'tax_rate2' => [ 'type' => 'float' ],
|
||||
'tax_name3' => [ 'type' => 'text' ],
|
||||
'tax_rate3' => [ 'type' => 'float' ],
|
||||
'custom_value1' => [ 'type' => 'text' ],
|
||||
'custom_value2' => [ 'type' => 'text' ],
|
||||
'custom_value3' => [ 'type' => 'text' ],
|
||||
'custom_value4' => [ 'type' => 'text' ],
|
||||
'type_id' => [ 'type' => 'text' ],
|
||||
'tax_id' => [ 'type' => 'text' ],
|
||||
'task_id' => [ 'type' => 'text' ],
|
||||
'expense_id' => [ 'type' => 'text' ],
|
||||
'unit_code' => [ 'type' => 'text' ],
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function create(string $index_name): void
|
||||
{
|
||||
\Elastic\Migrations\Facades\Index::createRaw($index_name, $this->mapping);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -81,6 +81,7 @@ class Handler extends ExceptionHandler
|
|||
ModelNotFoundException::class,
|
||||
NotFoundHttpException::class,
|
||||
RelationNotFoundException::class,
|
||||
StripeConnectFailure::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class ActivityExport extends BaseExport
|
|||
|
||||
}
|
||||
|
||||
private function init(): Builder
|
||||
public function init(): Builder
|
||||
{
|
||||
MultiDB::setDb($this->company->db);
|
||||
App::forgetInstance('translator');
|
||||
|
|
@ -115,6 +115,8 @@ class ActivityExport extends BaseExport
|
|||
|
||||
$query = $this->addDateRange($query, 'activities');
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
if ($this->input['activity_type_id'] ?? false) {
|
||||
$query->where('activity_type_id', $this->input['activity_type_id']);
|
||||
}
|
||||
|
|
@ -127,18 +129,16 @@ class ActivityExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
$this->csv->insertOne($this->buildHeader());
|
||||
|
||||
|
||||
$query->cursor()
|
||||
->each(function ($entity) {
|
||||
|
||||
/** @var \App\Models\Activity $entity */
|
||||
|
||||
$this->buildRow($entity);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@
|
|||
|
||||
namespace App\Export\CSV;
|
||||
|
||||
use App\Jobs\Credit\ZipCredits;
|
||||
use Str;
|
||||
use App\Models\Task;
|
||||
use App\Models\User;
|
||||
use App\Models\Quote;
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Design;
|
||||
use App\Models\Vendor;
|
||||
use App\Utils\Helpers;
|
||||
use App\Models\Company;
|
||||
|
|
@ -27,20 +28,21 @@ use App\Models\Payment;
|
|||
use App\Models\Product;
|
||||
use App\Models\Document;
|
||||
use League\Fractal\Manager;
|
||||
use App\Jobs\Quote\ZipQuotes;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\PurchaseOrder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Jobs\Credit\ZipCredits;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Jobs\Document\ZipDocuments;
|
||||
use App\Jobs\Invoice\ZipInvoices;
|
||||
use App\Jobs\PurchaseOrder\ZipPurchaseOrders;
|
||||
use App\Jobs\Quote\ZipQuotes;
|
||||
use App\Jobs\Document\ZipDocuments;
|
||||
use App\Transformers\TaskTransformer;
|
||||
use App\Transformers\PaymentTransformer;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Services\Template\TemplateService;
|
||||
use App\Jobs\PurchaseOrder\ZipPurchaseOrders;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use Str;
|
||||
|
||||
class BaseExport
|
||||
{
|
||||
|
|
@ -1291,10 +1293,13 @@ $products = str_getcsv($this->input['product_key'], ',', "'");
|
|||
$this->end_date = 'All available data';
|
||||
return $query;
|
||||
case 'last7':
|
||||
case 'last_7_days':
|
||||
case 'last7_days':
|
||||
$this->start_date = now()->subDays(7)->format('Y-m-d');
|
||||
$this->end_date = now()->format('Y-m-d');
|
||||
return $query->whereBetween($this->date_key, [now()->subDays(7), now()])->orderBy($this->date_key, 'ASC');
|
||||
case 'last30':
|
||||
case 'last_30_days':
|
||||
$this->start_date = now()->subDays(30)->format('Y-m-d');
|
||||
$this->end_date = now()->format('Y-m-d');
|
||||
return $query->whereBetween($this->date_key, [now()->subDays(30), now()])->orderBy($this->date_key, 'ASC');
|
||||
|
|
@ -1708,4 +1713,105 @@ $products = str_getcsv($this->input['product_key'], ',', "'");
|
|||
return $entity;
|
||||
|
||||
}
|
||||
|
||||
public function filterByUserPermissions(Builder $query): Builder
|
||||
{
|
||||
|
||||
$user = User::withTrashed()->where('id', $this->input['user_id'])->where('account_id', $this->company->account_id)->first();
|
||||
|
||||
if ($user->isAdmin() || $user->hasExactPermission('view_all') || $user->hasExactPermission('edit_all')) { // No State? Do we need to ensure -> isAdmin() binds to the correct company?
|
||||
return $query;
|
||||
}
|
||||
|
||||
if($user->hasExactPermission('create_all')){
|
||||
return $query->where('user_id', $user->id);
|
||||
}
|
||||
|
||||
return $this->resolveEntityFilters($user, $query);
|
||||
|
||||
}
|
||||
|
||||
public function exportTemplate(Builder $query, string $template_id)
|
||||
{
|
||||
$template = Design::withTrashed()->find($this->decodePrimaryKey($template_id));
|
||||
|
||||
$model_string = $this->getModelString($query);
|
||||
|
||||
$data = [
|
||||
"{$model_string}s" => $query->get(),
|
||||
"start_date" => $this->start_date,
|
||||
"end_date" => $this->end_date,
|
||||
];
|
||||
|
||||
$ts = new TemplateService($template);
|
||||
$ts->setCompany($this->company);
|
||||
$ts->addGlobal(['currency_code' => $this->company->currency()->code]);
|
||||
$ts->build($data);
|
||||
|
||||
return $ts->getPdf();
|
||||
|
||||
}
|
||||
|
||||
private function getModelString(Builder $query): ?string
|
||||
{
|
||||
|
||||
$model = get_class($query->getModel());
|
||||
|
||||
return match($model) {
|
||||
'App\Models\Client' => 'client',
|
||||
'App\Models\ClientContact' => 'client',
|
||||
'App\Models\Invoice' => 'invoice',
|
||||
'App\Models\Quote' => 'quote',
|
||||
'App\Models\Credit' => 'credit',
|
||||
'App\Models\PurchaseOrder' => 'purchase_order',
|
||||
'App\Models\RecurringInvoice' => 'recurring_invoice',
|
||||
'App\Models\RecurringExpense' => 'recurring_expense',
|
||||
'App\Models\Task' => 'task',
|
||||
'App\Models\Vendor' => 'vendor',
|
||||
'App\Models\VendorContact' => 'vendor_contact',
|
||||
'App\Models\Product' => 'product',
|
||||
'App\Models\Payment' => 'payment',
|
||||
'App\Models\Expense' => 'expense',
|
||||
'App\Models\Document' => 'document',
|
||||
'App\Models\Activity' => 'activity',
|
||||
'App\Models\Task' => 'task',
|
||||
'App\Models\Project' => 'project',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
private function resolveEntityFilters(User $user, Builder $query): Builder
|
||||
{
|
||||
|
||||
$model = get_class($query->getModel());
|
||||
$model_string = $this->getModelString($query);
|
||||
$column_listing = \Illuminate\Support\Facades\Schema::getColumnListing($query->getModel()->getTable());
|
||||
|
||||
/** If the User can view or edit the entity, then return the query unfiltered */
|
||||
if($user->hasIntersectPermissions(["view_{$model_string}", "edit_{$model_string}"])){
|
||||
return $query;
|
||||
}
|
||||
|
||||
//Handle Child Models Like ClientContact or VendorContact
|
||||
if(in_array($model, ['App\Models\ClientContact', 'App\Models\VendorContact'])){
|
||||
|
||||
$query->whereHas($model_string, function ($_q) use ($user){
|
||||
$_q->where('user_id', $user->id)->orWhere('assigned_user_id', $user->id);
|
||||
});
|
||||
|
||||
return $query;
|
||||
|
||||
}
|
||||
|
||||
return $query->where(function ($q) use ($user, $column_listing){
|
||||
|
||||
if(in_array('user_id', $column_listing)){
|
||||
$q->where('user_id', $user->id);
|
||||
}
|
||||
|
||||
if(in_array('assigned_user_id', $column_listing)){
|
||||
$q->orWhere('assigned_user_id', $user->id);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ class ClientExport extends BaseExport
|
|||
|
||||
$query = $this->addDateRange($query, ' clients');
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
}
|
||||
|
|
@ -149,7 +151,7 @@ class ClientExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
@ -227,7 +229,7 @@ class ClientExport extends BaseExport
|
|||
}
|
||||
|
||||
if (in_array('client.assigned_user', $this->input['report_keys'])) {
|
||||
$entity['client.assigned_user'] = $client->assigned_user ? $client->user->present()->name() : '';
|
||||
$entity['client.assigned_user'] = $client->assigned_user ? $client->assigned_user->present()->name() : '';
|
||||
}
|
||||
|
||||
if (in_array('client.classification', $this->input['report_keys']) && isset($client->classification)) {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class ContactExport extends BaseExport
|
|||
$this->decorator = new Decorator();
|
||||
}
|
||||
|
||||
private function init(): Builder
|
||||
public function init(): Builder
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
|
@ -65,6 +65,7 @@ class ContactExport extends BaseExport
|
|||
});
|
||||
|
||||
$query = $this->addDateRange($query, 'client_contacts');
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
return $query;
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ class ContactExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class CreditExport extends BaseExport
|
|||
return $clean_row;
|
||||
}
|
||||
|
||||
private function init(): Builder
|
||||
public function init(): Builder
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
|
@ -123,6 +123,8 @@ class CreditExport extends BaseExport
|
|||
$query = $this->addCreditStatusFilter($query, $this->input['status']);
|
||||
}
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
}
|
||||
|
|
@ -138,7 +140,7 @@ class CreditExport extends BaseExport
|
|||
{
|
||||
$query = $this->init();
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class DocumentExport extends BaseExport
|
|||
return array_merge(['columns' => $header], $report);
|
||||
}
|
||||
|
||||
private function init(): Builder
|
||||
public function init(): Builder
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
|
@ -81,6 +81,8 @@ class DocumentExport extends BaseExport
|
|||
|
||||
$query = $this->addDateRange($query, 'documents');
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
}
|
||||
|
|
@ -94,7 +96,7 @@ class DocumentExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class ExpenseExport extends BaseExport
|
|||
return array_merge(['columns' => $header], $report);
|
||||
}
|
||||
|
||||
private function init(): Builder
|
||||
public function init(): Builder
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
|
@ -114,6 +114,8 @@ class ExpenseExport extends BaseExport
|
|||
$query = $this->addCategoryFilter($query, $this->input['categories']);
|
||||
}
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
}
|
||||
|
|
@ -127,7 +129,7 @@ class ExpenseExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
@ -225,9 +227,9 @@ class ExpenseExport extends BaseExport
|
|||
|
||||
if (in_array('expense.vendor_id', $this->input['report_keys'])) {
|
||||
|
||||
// $entity['expense.vendor'] = $expense->vendor ? $expense->vendor->name : '';
|
||||
$entity['expense.vendor_id'] = $expense->vendor ? $expense->vendor->name : '';
|
||||
|
||||
$entity['expense.vendor_id'] = $expense->vendor ? $expense->vendor->id : '';
|
||||
// $entity['expense.vendor_id'] = $expense->vendor ? $expense->vendor->id : '';
|
||||
}
|
||||
|
||||
if (in_array('expense.payment_type_id', $this->input['report_keys'])) {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ class InvoiceExport extends BaseExport
|
|||
$query = $this->addInvoiceStatusFilter($query, $this->input['status']);
|
||||
}
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
}
|
||||
|
|
@ -120,7 +123,7 @@ class InvoiceExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
if ($tax_amount_position = array_search('invoice.total_taxes', $this->input['report_keys'])) {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,8 @@ class InvoiceItemExport extends BaseExport
|
|||
$query = $this->addInvoiceStatusFilter($query, $this->input['status']);
|
||||
}
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
$query = $this->applyProductFilters($query);
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
|
|
@ -137,7 +139,7 @@ class InvoiceItemExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class PaymentExport extends BaseExport
|
|||
$this->decorator = new Decorator();
|
||||
}
|
||||
|
||||
private function init(): Builder
|
||||
public function init(): Builder
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
|
@ -72,6 +72,7 @@ class PaymentExport extends BaseExport
|
|||
}
|
||||
|
||||
$query = $this->addPaymentStatusFilters($query, $this->input['status'] ?? '');
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
|
|
@ -107,7 +108,7 @@ class PaymentExport extends BaseExport
|
|||
{
|
||||
$query = $this->init();
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class ProductExport extends BaseExport
|
|||
return array_merge(['columns' => $header], $report);
|
||||
}
|
||||
|
||||
private function init(): Builder
|
||||
public function init(): Builder
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
|
@ -83,6 +83,7 @@ class ProductExport extends BaseExport
|
|||
}
|
||||
|
||||
$query = $this->addDateRange($query, 'products');
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
|
|
@ -98,7 +99,7 @@ class ProductExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class ProductSalesExport extends BaseExport
|
|||
$this->products = Product::query()->where('company_id', $this->company->id)->withTrashed()->get();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
if (count($this->input['report_keys']) == 0) {
|
||||
|
|
@ -128,6 +128,8 @@ class ProductSalesExport extends BaseExport
|
|||
|
||||
$query = $this->filterByClients($query);
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
$query = $this->filterByProducts($query);
|
||||
|
||||
$this->csv->insertOne($this->buildHeader());
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class PurchaseOrderExport extends BaseExport
|
|||
if ($clients) {
|
||||
$query = $this->addClientFilter($query, $clients);
|
||||
}
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
$query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ class PurchaseOrderExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ class PurchaseOrderItemExport extends BaseExport
|
|||
if ($clients) {
|
||||
$query = $this->addClientFilter($query, $clients);
|
||||
}
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
$query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');
|
||||
|
||||
|
|
@ -120,7 +121,7 @@ class PurchaseOrderItemExport extends BaseExport
|
|||
public function run()
|
||||
{
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
$query = $this->init();
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class QuoteExport extends BaseExport
|
|||
$this->decorator = new Decorator();
|
||||
}
|
||||
|
||||
private function init(): Builder
|
||||
public function init(): Builder
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
|
@ -78,6 +78,8 @@ class QuoteExport extends BaseExport
|
|||
|
||||
$query = $this->addQuoteStatusFilter($query, $this->input['status'] ?? '');
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
}
|
||||
|
|
@ -116,7 +118,7 @@ class QuoteExport extends BaseExport
|
|||
public function run()
|
||||
{
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
$query = $this->init();
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ class QuoteItemExport extends BaseExport
|
|||
if ($clients) {
|
||||
$query = $this->addClientFilter($query, $clients);
|
||||
}
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
$query = $this->addQuoteStatusFilter($query, $this->input['status'] ?? '');
|
||||
|
||||
|
|
@ -126,7 +127,7 @@ class QuoteItemExport extends BaseExport
|
|||
{
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
$query = $this->init();
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ class RecurringInvoiceExport extends BaseExport
|
|||
if ($clients) {
|
||||
$query = $this->addClientFilter($query, $clients);
|
||||
}
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
$query = $this->addRecurringInvoiceStatusFilter($query, $this->input['status'] ?? '');
|
||||
|
||||
|
|
@ -86,7 +87,7 @@ class RecurringInvoiceExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ class RecurringInvoiceItemExport extends BaseExport
|
|||
if ($this->input['status'] ?? false) {
|
||||
$query = $this->addRecurringInvoiceStatusFilter($query, $this->input['status']);
|
||||
}
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
$query = $this->applyProductFilters($query);
|
||||
|
||||
|
|
@ -138,7 +139,7 @@ class RecurringInvoiceItemExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
|
|||
|
|
@ -82,6 +82,12 @@ class TaskExport extends BaseExport
|
|||
$query = $this->addClientFilter($query, $clients);
|
||||
}
|
||||
|
||||
if($this->input['status'] ?? false){
|
||||
$query = $this->addTaskStatusFilter($query, $this->input['status']);
|
||||
}
|
||||
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
$document_attachments = &$this->input['document_email_attachment'];
|
||||
|
||||
if ($document_attachments) {
|
||||
|
|
@ -98,7 +104,7 @@ class TaskExport extends BaseExport
|
|||
$query = $this->init();
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
//insert the header
|
||||
|
|
@ -261,6 +267,7 @@ class TaskExport extends BaseExport
|
|||
*/
|
||||
protected function addTaskStatusFilter(Builder $query, string $status): Builder
|
||||
{
|
||||
|
||||
/** @var array $status_parameters */
|
||||
$status_parameters = explode(',', $status);
|
||||
|
||||
|
|
@ -276,6 +283,16 @@ class TaskExport extends BaseExport
|
|||
$query->whereNull('invoice_id');
|
||||
}
|
||||
|
||||
$keys = $this->transformKeys($status_parameters);
|
||||
|
||||
$keys = collect($keys)->filter(function ($key){
|
||||
return is_int($key);
|
||||
})->toArray();
|
||||
|
||||
if(count($keys) > 0){
|
||||
$query->whereIn('status_id', $keys);
|
||||
}
|
||||
|
||||
return $query;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class VendorExport extends BaseExport
|
|||
$t->replace(Ninja::transformTranslations($this->company->settings));
|
||||
|
||||
//load the CSV document from a string
|
||||
$this->csv = Writer::createFromString();
|
||||
$this->csv = Writer::fromString();
|
||||
\League\Csv\CharsetConverter::addTo($this->csv, 'UTF-8', 'UTF-8');
|
||||
|
||||
if (count($this->input['report_keys']) == 0) {
|
||||
|
|
@ -70,6 +70,7 @@ class VendorExport extends BaseExport
|
|||
}
|
||||
|
||||
$query = $this->addDateRange($query, 'vendors');
|
||||
$query = $this->filterByUserPermissions($query);
|
||||
|
||||
if ($this->input['document_email_attachment'] ?? false) {
|
||||
$this->queueDocuments($query);
|
||||
|
|
|
|||
|
|
@ -173,7 +173,9 @@ class ClientFilters extends QueryFilters
|
|||
$sort_col[0] = 'name';
|
||||
}
|
||||
|
||||
if (!is_array($sort_col) || count($sort_col) != 2 || !in_array($sort_col[0], \Illuminate\Support\Facades\Schema::getColumnListing($this->builder->getModel()->getTable()))) {
|
||||
if(is_array($sort_col) && $sort_col[0] == 'contacts'){
|
||||
}
|
||||
elseif (!is_array($sort_col) || count($sort_col) != 2 || !in_array($sort_col[0], \Illuminate\Support\Facades\Schema::getColumnListing($this->builder->getModel()->getTable()))) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
|
|
@ -184,18 +186,52 @@ class ClientFilters extends QueryFilters
|
|||
}
|
||||
|
||||
if ($sort_col[0] == 'name') {
|
||||
return $this->builder
|
||||
->select('clients.*')
|
||||
->selectSub(function ($query) {
|
||||
$query->from('client_contacts')
|
||||
->whereColumn('client_contacts.client_id', 'clients.id')
|
||||
->whereNull('client_contacts.deleted_at')
|
||||
->select(\DB::raw('COALESCE(NULLIF(first_name, ""), email) as contact_info'))
|
||||
->limit(1);
|
||||
}, 'first_contact_name')
|
||||
->orderByRaw("COALESCE(NULLIF(clients.name, ''), first_contact_name) " . $dir);
|
||||
// Use a raw subquery in the ORDER BY instead of adding it to SELECT
|
||||
// This avoids conflicts with the Excludable trait
|
||||
return $this->builder->orderByRaw("
|
||||
COALESCE(
|
||||
NULLIF(clients.name, ''),
|
||||
(
|
||||
SELECT COALESCE(NULLIF(first_name, ''), email)
|
||||
FROM client_contacts
|
||||
WHERE client_contacts.client_id = clients.id
|
||||
AND client_contacts.deleted_at IS NULL
|
||||
LIMIT 1
|
||||
)
|
||||
) " . $dir
|
||||
);
|
||||
}
|
||||
|
||||
if($sort_col[0] == 'contacts'){
|
||||
return $this->builder->orderByRaw("
|
||||
(
|
||||
SELECT
|
||||
CASE
|
||||
WHEN first_name IS NOT NULL AND first_name != '' AND last_name IS NOT NULL AND last_name != ''
|
||||
THEN CONCAT(first_name, ' ', last_name)
|
||||
WHEN first_name IS NOT NULL AND first_name != ''
|
||||
THEN first_name
|
||||
WHEN last_name IS NOT NULL AND last_name != ''
|
||||
THEN last_name
|
||||
ELSE email
|
||||
END
|
||||
FROM client_contacts
|
||||
WHERE client_contacts.client_id = clients.id
|
||||
AND client_contacts.deleted_at IS NULL
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN first_name IS NOT NULL AND first_name != '' AND last_name IS NOT NULL AND last_name != '' THEN 1
|
||||
WHEN first_name IS NOT NULL AND first_name != '' THEN 2
|
||||
WHEN last_name IS NOT NULL AND last_name != '' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
first_name ASC,
|
||||
last_name ASC,
|
||||
email ASC
|
||||
LIMIT 1
|
||||
) " . $dir
|
||||
);
|
||||
}
|
||||
return $this->builder->orderBy($sort_col[0], $dir);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -150,7 +150,8 @@ class CreditFilters extends QueryFilters
|
|||
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||
|
||||
if ($sort_col[0] == 'client_id') {
|
||||
return $this->builder->orderBy(\App\Models\Client::select('name')
|
||||
return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
|
||||
->orderBy(\App\Models\Client::select('name')
|
||||
->whereColumn('clients.id', 'credits.client_id'), $dir);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -287,11 +287,20 @@ class InvoiceFilters extends QueryFilters
|
|||
|
||||
if ($sort_col[0] == 'client_id') {
|
||||
|
||||
return $this->builder->orderBy(\App\Models\Client::select('name')
|
||||
return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
|
||||
->orderBy(\App\Models\Client::select('name')
|
||||
->whereColumn('clients.id', 'invoices.client_id'), $dir);
|
||||
|
||||
}
|
||||
|
||||
if ($sort_col[0] == 'project_id') {
|
||||
|
||||
return $this->builder->orderByRaw('ISNULL(project_id), project_id '. $dir)
|
||||
->orderBy(\App\Models\Project::select('name')
|
||||
->whereColumn('projects.id', 'invoices.project_id'), $dir);
|
||||
|
||||
}
|
||||
|
||||
if ($sort_col[0] == 'number') {
|
||||
return $this->builder->orderByRaw("REGEXP_REPLACE(invoices.number,'[^0-9]+','')+0 " . $dir);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,7 +172,8 @@ class PaymentFilters extends QueryFilters
|
|||
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||
|
||||
if ($sort_col[0] == 'client_id') {
|
||||
return $this->builder->orderBy(\App\Models\Client::select('name')
|
||||
return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
|
||||
->orderBy(\App\Models\Client::select('name')
|
||||
->whereColumn('clients.id', 'payments.client_id'), $dir);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ class ProjectFilters extends QueryFilters
|
|||
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||
|
||||
if ($sort_col[0] == 'client_id') {
|
||||
return $this->builder->orderBy(\App\Models\Client::select('name')
|
||||
return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
|
||||
->orderBy(\App\Models\Client::select('name')
|
||||
->whereColumn('clients.id', 'projects.client_id'), $dir);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,12 +147,12 @@ abstract class QueryFilters
|
|||
|
||||
return $this->builder->where(function ($query) use ($filters) {
|
||||
if (in_array(self::STATUS_ACTIVE, $filters)) {
|
||||
$query = $query->orWhereNull('deleted_at');
|
||||
$query = $query->orWhereNull($this->builder->getModel()->getTable() . '.deleted_at');
|
||||
}
|
||||
|
||||
if (in_array(self::STATUS_ARCHIVED, $filters)) {
|
||||
$query = $query->orWhere(function ($q) {
|
||||
$q->whereNotNull('deleted_at')->where('is_deleted', 0);
|
||||
$q->whereNotNull($this->builder->getModel()->getTable() . '.deleted_at')->where('is_deleted', 0);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -173,7 +173,8 @@ class QuoteFilters extends QueryFilters
|
|||
}
|
||||
|
||||
if ($sort_col[0] == 'client_id') {
|
||||
return $this->builder->orderBy(\App\Models\Client::select('name')
|
||||
return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
|
||||
->orderBy(\App\Models\Client::select('name')
|
||||
->whereColumn('clients.id', 'quotes.client_id'), $dir);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@ class RecurringInvoiceFilters extends QueryFilters
|
|||
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||
|
||||
if ($sort_col[0] == 'client_id') {
|
||||
return $this->builder->orderBy(\App\Models\Client::select('name')
|
||||
return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
|
||||
->orderBy(\App\Models\Client::select('name')
|
||||
->whereColumn('clients.id', 'recurring_invoices.client_id'), $dir);
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +205,13 @@ class RecurringInvoiceFilters extends QueryFilters
|
|||
$parts = explode('|', $range);
|
||||
|
||||
if (!isset($parts[0]) || !isset($parts[1])) {
|
||||
return $this->builder;
|
||||
|
||||
$parts = explode(',', $range);
|
||||
|
||||
if (!isset($parts[0]) || !isset($parts[1])){
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (is_numeric($parts[0])) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class TaskFilters extends QueryFilters
|
|||
|
||||
return $this->builder->where(function ($query) use ($filter) {
|
||||
$query->where('description', 'like', '%'.$filter.'%')
|
||||
->orWhere("number", 'like', '%'.$filter.'%')
|
||||
->orWhere('time_log', 'like', '%'.$filter.'%')
|
||||
->orWhere('custom_value1', 'like', '%'.$filter.'%')
|
||||
->orWhere('custom_value2', 'like', '%'.$filter.'%')
|
||||
|
|
|
|||
|
|
@ -89,21 +89,44 @@ class NordigenClient
|
|||
$allRequisitions = collect();
|
||||
$offset = null;
|
||||
$limit = 100;
|
||||
$maxIterations = 1000; // Safety limit to prevent infinite loops
|
||||
$iteration = 0;
|
||||
|
||||
do {
|
||||
$iteration++;
|
||||
|
||||
// Safety check to prevent infinite loops
|
||||
if ($iteration > $maxIterations) {
|
||||
nlog("getAllRequisitions: Maximum iterations reached ({$maxIterations}), breaking to prevent infinite loop");
|
||||
break;
|
||||
}
|
||||
|
||||
$requisitions = $this->getRequisitions($limit, $offset);
|
||||
|
||||
nlog($requisitions);
|
||||
if ($requisitions->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$allRequisitions = $allRequisitions->merge($requisitions);
|
||||
|
||||
// Check if we got fewer results than requested (end of data)
|
||||
if ($requisitions->count() < $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Use the last requisition's ID as the offset for cursor-based pagination
|
||||
$lastRequisition = $requisitions->last();
|
||||
$offset = $lastRequisition['id'] ?? null;
|
||||
$newOffset = $lastRequisition['id'] ?? null;
|
||||
|
||||
// Check if we're making progress (offset is changing)
|
||||
if ($newOffset === $offset) {
|
||||
nlog("getAllRequisitions: Offset not changing, likely stuck in loop. Breaking.");
|
||||
break;
|
||||
}
|
||||
|
||||
$offset = $newOffset;
|
||||
|
||||
} while ($requisitions->count() === $limit && $offset);
|
||||
} while ($offset);
|
||||
|
||||
return $allRequisitions;
|
||||
}
|
||||
|
|
@ -120,7 +143,7 @@ class NordigenClient
|
|||
$params['offset'] = $offset;
|
||||
}
|
||||
|
||||
$response = $this->httpClient->get("{$this->baseUrl}/agreements/", $params);
|
||||
$response = $this->httpClient->get("{$this->baseUrl}/agreements/enduser", $params);
|
||||
|
||||
return $this->handlePaginatedResponse($response);
|
||||
}
|
||||
|
|
@ -130,7 +153,7 @@ class NordigenClient
|
|||
*/
|
||||
public function getAgreement(string $agreementId): ?array
|
||||
{
|
||||
$response = $this->httpClient->get("{$this->baseUrl}/agreements/{$agreementId}/");
|
||||
$response = $this->httpClient->get("{$this->baseUrl}/agreements/enduser{$agreementId}/");
|
||||
|
||||
return $this->handleResponse($response);
|
||||
}
|
||||
|
|
@ -140,7 +163,7 @@ class NordigenClient
|
|||
*/
|
||||
public function createAgreement(array $data): ?array
|
||||
{
|
||||
$response = $this->httpClient->post("{$this->baseUrl}/agreements/", $data);
|
||||
$response = $this->httpClient->post("{$this->baseUrl}/agreements/enduser", $data);
|
||||
|
||||
return $this->handleResponse($response);
|
||||
}
|
||||
|
|
@ -150,7 +173,7 @@ class NordigenClient
|
|||
*/
|
||||
public function updateAgreement(string $agreementId, array $data): ?array
|
||||
{
|
||||
$response = $this->httpClient->put("{$this->baseUrl}/agreements/{$agreementId}/", $data);
|
||||
$response = $this->httpClient->put("{$this->baseUrl}/agreements/enduser/{$agreementId}/", $data);
|
||||
|
||||
return $this->handleResponse($response);
|
||||
}
|
||||
|
|
@ -160,7 +183,7 @@ class NordigenClient
|
|||
*/
|
||||
public function deleteAgreement(string $agreementId): bool
|
||||
{
|
||||
$response = $this->httpClient->delete("{$this->baseUrl}/agreements/{$agreementId}/");
|
||||
$response = $this->httpClient->delete("{$this->baseUrl}/agreements/enduser/{$agreementId}/");
|
||||
|
||||
return $response->successful();
|
||||
}
|
||||
|
|
@ -850,6 +873,8 @@ class NordigenClient
|
|||
return collect($data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handle single response
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -177,18 +177,27 @@ class Nordigen
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* validAgreement
|
||||
* @param string $institution_id
|
||||
* @param array $_accounts
|
||||
* @return array|null
|
||||
* @todo - very expensive!
|
||||
*/
|
||||
public function validAgreement($institution_id, $_accounts)
|
||||
{
|
||||
|
||||
$nc = new \App\Helpers\Bank\Nordigen\Http\NordigenClient($this->client->getAccessToken());
|
||||
$requisitions = $nc->getAllRequisitions();
|
||||
|
||||
$requisitions->filter(function($requisition) use ($institution_id, $_accounts){
|
||||
$requisition = $requisitions->filter(function($requisition) use ($institution_id, $_accounts){
|
||||
if($requisition['institution_id'] == $institution_id && !empty(array_intersect($requisition['accounts'], $_accounts))){
|
||||
return $requisition;
|
||||
}
|
||||
});
|
||||
|
||||
return $requisition->first()->toArray() ?? null;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -214,11 +223,11 @@ class Nordigen
|
|||
$out->metadata = $this->client->account($account_id)->getAccountMetaData();
|
||||
$out->institution = $this->client->institution->getInstitution($out->metadata['institution_id']);
|
||||
|
||||
if($out->metadata['status'] == 'READY'){
|
||||
$out->data = $this->client->account($account_id)->getAccountDetails()['account'];
|
||||
$out->balances = $this->client->account($account_id)->getAccountBalances()['balances'];
|
||||
}
|
||||
else{
|
||||
// if($out->metadata['status'] == 'READY'){
|
||||
// $out->data = $this->client->account($account_id)->getAccountDetails()['account'];
|
||||
// $out->balances = $this->client->account($account_id)->getAccountBalances()['balances'];
|
||||
// }
|
||||
// else{
|
||||
|
||||
$out->data = [
|
||||
'iban' => $out->metadata['iban'],
|
||||
|
|
@ -233,7 +242,7 @@ class Nordigen
|
|||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
// }
|
||||
|
||||
$it = new AccountTransformer();
|
||||
return $it->transform($out);
|
||||
|
|
|
|||
|
|
@ -67,10 +67,16 @@ class EpcQrGenerator
|
|||
public function encodeMessage()
|
||||
{
|
||||
|
||||
$name = $this->company->present()->name();
|
||||
|
||||
if (isset($this->company->e_invoice->Invoice->PaymentMeans) && ($pm = $this->company->e_invoice->Invoice->PaymentMeans[0] ?? false) && in_array($pm->PaymentMeansCode->value, ['30', '58'])) {
|
||||
|
||||
$iban = $pm->PayeeFinancialAccount->ID->value;
|
||||
$bic = $pm->PayeeFinancialAccount->FinancialInstitutionBranch->FinancialInstitution->ID->value ?? '';
|
||||
|
||||
if(isset($pm->PayeeFinancialAccount->Name) && strlen($pm->PayeeFinancialAccount->Name ?? '') > 0) {
|
||||
$name = $pm->PayeeFinancialAccount->Name;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
|
|
@ -85,7 +91,7 @@ class EpcQrGenerator
|
|||
'1', // Encoding: 1 = UTF-8
|
||||
'SCT', // Service Tag: SEPA Credit Transfer
|
||||
$bic, // BIC
|
||||
$this->company->present()->name(), // Name of the beneficiary
|
||||
$name, // Recipient Name - Account Name
|
||||
$iban, // IBAN
|
||||
$this->formatMoney($this->amount), // Amount with EUR prefix
|
||||
'', // Reference
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ class InvoiceItemSum
|
|||
|
||||
private function push(): self
|
||||
{
|
||||
|
||||
$this->sub_total += round($this->getLineTotal(), $this->currency->precision);
|
||||
|
||||
$this->gross_sub_total += $this->getGrossLineTotal();
|
||||
|
|
@ -295,6 +296,7 @@ class InvoiceItemSum
|
|||
*/
|
||||
private function calcTaxes()
|
||||
{
|
||||
|
||||
if ($this->calc_tax) {
|
||||
$this->calcTaxesAutomatically();
|
||||
}
|
||||
|
|
@ -368,24 +370,28 @@ class InvoiceItemSum
|
|||
|
||||
$tax_component = 0;
|
||||
|
||||
$amount = 0;
|
||||
|
||||
if ($this->invoice->custom_surcharge1) {
|
||||
$tax_component += round($this->invoice->custom_surcharge1 * ($tax['percentage'] / 100), 2);
|
||||
$amount += $this->invoice->custom_surcharge1;
|
||||
}
|
||||
|
||||
if ($this->invoice->custom_surcharge2) {
|
||||
$tax_component += round($this->invoice->custom_surcharge2 * ($tax['percentage'] / 100), 2);
|
||||
$amount += $this->invoice->custom_surcharge2;
|
||||
}
|
||||
|
||||
if ($this->invoice->custom_surcharge3) {
|
||||
$tax_component += round($this->invoice->custom_surcharge3 * ($tax['percentage'] / 100), 2);
|
||||
$amount += $this->invoice->custom_surcharge3;
|
||||
}
|
||||
|
||||
if ($this->invoice->custom_surcharge4) {
|
||||
$tax_component += round($this->invoice->custom_surcharge4 * ($tax['percentage'] / 100), 2);
|
||||
$amount += $this->invoice->custom_surcharge4;
|
||||
}
|
||||
|
||||
$amount = $this->invoice->custom_surcharge4 + $this->invoice->custom_surcharge3 + $this->invoice->custom_surcharge2 + $this->invoice->custom_surcharge1;
|
||||
|
||||
if ($tax_component > 0) {
|
||||
$this->groupTax($tax['name'], $tax['percentage'], $tax_component, $amount, $tax['tax_id']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ class Office365MailTransport extends AbstractTransport
|
|||
{
|
||||
$symfony_message = MessageConverter::toEmail($message->getOriginalMessage()); //@phpstan-ignore-line
|
||||
|
||||
|
||||
$graph = new Graph();
|
||||
|
||||
/** @phpstan-ignore-next-line **/
|
||||
$token = $symfony_message->getHeaders()->get('gmailtoken')->getValue();
|
||||
$symfony_message->getHeaders()->remove('gmailtoken');
|
||||
// $symfony_message->getHeaders()->remove('gmailtoken');
|
||||
$message->getOriginalMessage()->getHeaders()->remove('gmailtoken');
|
||||
|
||||
$graph->setAccessToken($token);
|
||||
|
||||
|
|
|
|||
|
|
@ -86,8 +86,8 @@ class AccountController extends BaseController
|
|||
|
||||
}
|
||||
|
||||
if ($request->has('hash') && config('ninja.cloudflare.turnstile.secret')) { //@todo once all platforms are implemented, we disable access to the rest of this route without a success response.
|
||||
|
||||
if ($request->has('hash') && config('ninja.cloudflare.turnstile.secret')) {
|
||||
|
||||
if (Secure::decrypt($request->input('hash')) !== $request->input('email')) {
|
||||
return response()->json(['message' => 'Invalid Signup Payload'], 400);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,13 +135,13 @@ class LoginController extends BaseController
|
|||
|
||||
if (strlen($request->input('one_time_password')) == 0 || !$google2fa->verifyKey(decrypt($user->google_2fa_secret), $request->input('one_time_password'))) {
|
||||
return response()
|
||||
->json(['message' => ctrans('texts.invalid_one_time_password')], 401)
|
||||
->json(['message' => ctrans('texts.invalid_one_time_password')], 422)
|
||||
->header('X-App-Version', config('ninja.app_version'))
|
||||
->header('X-Api-Version', config('ninja.minimum_client_version'));
|
||||
}
|
||||
} elseif (strlen($user->google_2fa_secret ?? '') > 2 && !$request->has('one_time_password')) {
|
||||
return response()
|
||||
->json(['message' => ctrans('texts.invalid_one_time_password')], 401)
|
||||
->json(['message' => ctrans('texts.invalid_one_time_password')], 422)
|
||||
->header('X-App-Version', config('ninja.app_version'))
|
||||
->header('X-Api-Version', config('ninja.minimum_client_version'));
|
||||
}
|
||||
|
|
@ -378,8 +378,8 @@ class LoginController extends BaseController
|
|||
$account_user = $account->default_company->owner();
|
||||
Auth::login($account_user, false);
|
||||
|
||||
$account_user->email_verified_at = now();
|
||||
$account_user->save();
|
||||
// $account_user->email_verified_at = now();
|
||||
// $account_user->save();
|
||||
|
||||
/** @var \App\Models\CompanyUser $cu */
|
||||
$cu = $this->hydrateCompanyUser($account_user);
|
||||
|
|
@ -630,8 +630,8 @@ class LoginController extends BaseController
|
|||
}
|
||||
|
||||
$user = $account->default_company->owner();
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
// $user->email_verified_at = now();
|
||||
// $user->save();
|
||||
|
||||
Auth::login($user, false);
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,24 @@ class ResetPasswordController extends Controller
|
|||
*/
|
||||
public function reset(Request $request)
|
||||
{
|
||||
// Safely decode URL-encoded token and email before validation
|
||||
if ($request->has('token')) {
|
||||
$token = $request->input('token');
|
||||
// Only decode if it contains URL encoding characters
|
||||
if (strpos($token, '%') !== false) {
|
||||
$request->merge(['token' => urldecode($token)]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('email')) {
|
||||
$email = $request->input('email');
|
||||
// Only decode if it contains URL encoding characters
|
||||
if (strpos($email, '%') !== false) {
|
||||
$request->merge(['email' => urldecode($email)]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$request->validate($this->rules(), $this->validationErrorMessages());
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
|
|
|
|||
|
|
@ -82,15 +82,10 @@ class NordigenController extends BaseController
|
|||
|
||||
$agreement = $nordigen->createAgreement($institution, $institution['max_access_valid_for_days'], $txDays);//@2025-07-01: this is the correct way to get the access days
|
||||
|
||||
// $agreement = $nordigen->createAgreement($institution, $data['access_days'] ?? 9999, $txDays);
|
||||
|
||||
//this does not work in a multi tenant environment, it simply grabs the first agreement, without differentiating between companies. we may need to store the current requistion...
|
||||
// $agreement = $nordigen->firstValidAgreement($institution['id'], $data['access_days'] ?? 0, $txDays)
|
||||
// ?? $nordigen->createAgreement($institution, $data['max_access_valid_for_days'] ?? 90, $txDays);
|
||||
} catch (\Exception $e) {
|
||||
$debug = "{$e->getMessage()} ({$e->getCode()})";
|
||||
|
||||
nlog("Nordigen: Could not create an agreement with ${institution['name']}: {$debug}");
|
||||
nlog("Nordigen: Could not create an agreement with {$institution['name']}: {$debug}");
|
||||
|
||||
return $this->failed('eua-failure', $context, $company);
|
||||
}
|
||||
|
|
@ -224,7 +219,7 @@ class NordigenController extends BaseController
|
|||
->where('integration_type', BankIntegration::INTEGRATION_TYPE_NORDIGEN)
|
||||
->where('auto_sync', true)
|
||||
->each(function ($bank_integration) {
|
||||
ProcessBankTransactionsNordigen::dispatch($bank_integration);
|
||||
ProcessBankTransactionsNordigen::dispatch($bank_integration)->delay(now()->addHour());
|
||||
});
|
||||
|
||||
// prevent rerun of this method with same ref
|
||||
|
|
|
|||
|
|
@ -77,7 +77,13 @@ class YodleeController extends BaseController
|
|||
$accounts = $yodlee->getAccounts();
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
if (!BankIntegration::where('bank_account_id', $account['id'])->where('company_id', $company->id)->exists()) {
|
||||
if ($bi = BankIntegration::where('bank_account_id', $account['id'])->where('company_id', $company->id)->first()) {
|
||||
$bi->disabled_upstream = false;
|
||||
$bi->balance = $account['current_balance'];
|
||||
$bi->currency = $account['account_currency'];
|
||||
$bi->integration_type = BankIntegration::INTEGRATION_TYPE_YODLEE;
|
||||
$bi->save();
|
||||
} else {
|
||||
$bank_integration = new BankIntegration();
|
||||
$bank_integration->company_id = $company->id;
|
||||
$bank_integration->account_id = $company->account_id;
|
||||
|
|
|
|||
|
|
@ -653,10 +653,7 @@ class BaseController extends Controller
|
|||
$resource = new Collection($query, $transformer, $this->entity_type);
|
||||
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
|
||||
}
|
||||
// else {
|
||||
// $resource = new Collection($query, $transformer, $this->entity_type);
|
||||
// }
|
||||
|
||||
|
||||
return $this->response($this->manager->createData($resource)->toArray());
|
||||
}
|
||||
|
||||
|
|
@ -671,7 +668,22 @@ class BaseController extends Controller
|
|||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user->getCompany()->is_large) {
|
||||
/** React does not require bloated login response. */
|
||||
if(request()->hasHeader('X-React')){
|
||||
$this->manager->parseIncludes(
|
||||
[
|
||||
'account',
|
||||
'user.company_user',
|
||||
'token',
|
||||
'company',
|
||||
]);
|
||||
|
||||
// Set created_at to current time to filter out all existing related records
|
||||
// (designs, documents, groups, etc.) for a minimal response payload
|
||||
request()->merge(['created_at' => time()]);
|
||||
return $this->miniLoadResponse($query);
|
||||
}
|
||||
elseif ($user->getCompany()->is_large) {
|
||||
$this->manager->parseIncludes($this->mini_load);
|
||||
|
||||
return $this->miniLoadResponse($query);
|
||||
|
|
@ -1089,7 +1101,7 @@ class BaseController extends Controller
|
|||
$data = $this->first_load;
|
||||
}
|
||||
} else {
|
||||
$included = request()->input('include', '');
|
||||
$included = request()->input('include') ?? '';
|
||||
$included = explode(',', $included);
|
||||
|
||||
foreach ($included as $include) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class CreditController extends Controller
|
|||
|
||||
$data = [
|
||||
'credit' => $credit,
|
||||
'key' => $invitation ? $invitation->key : false,
|
||||
'_key' => $invitation ? $invitation->key : false,
|
||||
'invitation' => $invitation
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,16 @@ class EmailPreferencesController extends Controller
|
|||
{
|
||||
public function index(string $entity, string $invitation_key, Request $request): \Illuminate\View\View
|
||||
{
|
||||
|
||||
request()->session()->invalidate();
|
||||
request()->session()->regenerate(true);
|
||||
request()->session()->regenerateToken();
|
||||
|
||||
$class = "\\App\\Models\\".ucfirst(Str::camel($entity)).'Invitation';
|
||||
$invitation = $class::where('key', $invitation_key)->firstOrFail();
|
||||
|
||||
auth()->guard('contact')->loginUsingId($invitation->contact->id, true);
|
||||
|
||||
$data['receive_emails'] = $invitation->contact->is_locked ? false : true;
|
||||
$data['company'] = $invitation->company;
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class InvoiceController extends Controller
|
|||
$data = [
|
||||
'invoice' => $invoice->service()->removeUnpaidGatewayFees()->save(),
|
||||
'invitation' => $invitation ?: $invoice->invitations->first(),
|
||||
'key' => $invitation ? $invitation->key : false,
|
||||
'_key' => $invitation ? $invitation->key : false,
|
||||
'hash' => $hash,
|
||||
'variables' => $variables,
|
||||
'invoices' => [$invoice->hashed_id],
|
||||
|
|
@ -99,7 +99,7 @@ class InvoiceController extends Controller
|
|||
{
|
||||
$data = Cache::get($hash);
|
||||
|
||||
for ($x = 0; $x < 3; $x++) {
|
||||
for ($x = 0; $x < 18; $x++) {
|
||||
|
||||
$data = Cache::get($hash);
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ class InvoiceController extends Controller
|
|||
break;
|
||||
}
|
||||
|
||||
usleep(300000);
|
||||
usleep(100000);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ class InvoiceController extends Controller
|
|||
|
||||
$file = (new \App\Jobs\Entity\CreateRawPdf($invitation))->handle();
|
||||
|
||||
$headers = ['Content-Type' => 'application/pdf'];
|
||||
$headers = ['Content-Type' => 'application/pdf', 'Content-Disposition' => 'inline'];
|
||||
return response()->make($file, 200, $headers);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class QuoteController extends Controller
|
|||
|
||||
$data = [
|
||||
'quote' => $quote,
|
||||
'key' => $invitation ? $invitation->key : false,
|
||||
'_key' => $invitation ? $invitation->key : false,
|
||||
'invitation' => $invitation,
|
||||
'variables' => $variables,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -552,7 +552,23 @@ class CompanyController extends BaseController
|
|||
});
|
||||
|
||||
try {
|
||||
Storage::disk(config('filesystems.default'))->deleteDirectory($company->company_key);
|
||||
|
||||
if(Ninja::isHosted()){
|
||||
try{
|
||||
Storage::disk('s3')->deleteDirectory($company->company_key);
|
||||
}
|
||||
catch(\Throwable $th){}
|
||||
|
||||
try{
|
||||
Storage::disk('backup')->deleteDirectory($company->company_key);
|
||||
}
|
||||
catch(\Throwable $th){}
|
||||
|
||||
}
|
||||
else {
|
||||
Storage::disk(config('filesystems.default'))->deleteDirectory($company->company_key);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class ConnectedAccountController extends BaseController
|
|||
nlog("microsoft");
|
||||
nlog($email);
|
||||
|
||||
if (auth()->user()->email != $email && MultiDB::checkUserEmailExists($email)) {
|
||||
if (strtolower(auth()->user()->email) != strtolower($email) && MultiDB::checkUserEmailExists(strtolower($email))) {
|
||||
return response()->json(['message' => ctrans('texts.email_already_register')], 400);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -212,24 +212,38 @@ class CreditController extends BaseController
|
|||
->triggeredActions($request)
|
||||
->save();
|
||||
|
||||
/** 2025-09-24
|
||||
*
|
||||
* Handling invoice reversals needs stricter boundary checks:
|
||||
*
|
||||
* On the reversal of a paid invoice creates a credit note. However if this credit note is deleted the original payment becomes a dangling record.
|
||||
*
|
||||
* In order to avoid this, we link the payment to the credit note. This allows us to preserve the relation of the payment to the subsequent credit note.
|
||||
*
|
||||
* Now on Credit or Payment deletion we can correctly maintain the relation of the payment to the credit note.
|
||||
*/
|
||||
if ($credit->invoice_id) {
|
||||
$credit = $credit->service()->markSent()->save();
|
||||
// $credit->client->service()->updatePaidToDate(-1 * $credit->balance)->save(); // If we mutate the paid to date, we need to reverse the status of the invoice, this will allow the credit note that has been created to be used and double paid to dates prevented.
|
||||
$credit->client->service()->updateBalanceAndPaidToDate(-1 * ($credit->invoice->balance ?? 0), -1 * $credit->balance)->save();
|
||||
// $invoice = $credit->invoice;
|
||||
|
||||
|
||||
$invoice = \App\Models\Invoice::withTrashed()->find($credit->invoice_id);
|
||||
if ($invoice) {
|
||||
$invoice->status_id = Invoice::STATUS_REVERSED;
|
||||
$invoice->save();
|
||||
|
||||
//2025-08-25 after convert to a credit note, we need to delete the payments associated with the invoice.
|
||||
$invoice->payments()->each(function ($p) {
|
||||
$p->pivot->forceDelete();
|
||||
$p->invoices()->each(function ($i) {
|
||||
$i->pivot->forceDelete();
|
||||
});
|
||||
});
|
||||
//2025-09-25 this logic is flawed as unlinking the invoice then prevents a valid refund from taking place.
|
||||
// $invoice->payments()->each(function ($p) use ($credit) {
|
||||
// // $p->pivot->forceDelete();
|
||||
// $p->invoices()->each(function ($i) use ($credit) {
|
||||
// // $i->pivot->forceDelete();
|
||||
// $pivot = $i->pivot;
|
||||
// $pivot->paymentable_id = $credit->id;
|
||||
// $pivot->paymentable_type = Credit::class;
|
||||
// $pivot->save();
|
||||
|
||||
// });
|
||||
// });
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ class EInvoiceController extends BaseController
|
|||
*/
|
||||
public function validateEntity(ValidateEInvoiceRequest $request)
|
||||
{
|
||||
$el = new EntityLevel();
|
||||
$el = $request->getValidatorClass();
|
||||
|
||||
$data = [];
|
||||
|
||||
|
||||
match ($request->entity) {
|
||||
'invoices' => $data = $el->checkInvoice($request->getEntity()),
|
||||
'clients' => $data = $el->checkClient($request->getEntity()),
|
||||
|
|
@ -132,9 +132,8 @@ class EInvoiceController extends BaseController
|
|||
public function quota(ShowQuotaRequest $request): JsonResponse
|
||||
{
|
||||
nlog(["quota" => $request->all()]);
|
||||
/**
|
||||
* @var \App\Models\Company
|
||||
*/
|
||||
|
||||
/** @var \App\Models\Company $company */
|
||||
$company = auth()->user()->company();
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::baseUrl(config('ninja.hosted_ninja_url'))
|
||||
|
|
|
|||
|
|
@ -12,17 +12,18 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Http\Requests\EInvoice\Peppol\StoreEntityRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Services\EDocument\Jobs\SendEDocument;
|
||||
use App\Http\Requests\EInvoice\Peppol\RetrySendRequest;
|
||||
use App\Services\EDocument\Gateway\Storecove\Storecove;
|
||||
use App\Http\Requests\EInvoice\Peppol\DisconnectRequest;
|
||||
use App\Http\Requests\EInvoice\Peppol\ShowEntityRequest;
|
||||
use App\Http\Requests\EInvoice\Peppol\StoreEntityRequest;
|
||||
use App\Http\Requests\EInvoice\Peppol\UpdateEntityRequest;
|
||||
use App\Services\EDocument\Standards\Verifactu\SendToAeat;
|
||||
use App\Http\Requests\EInvoice\Peppol\AddTaxIdentifierRequest;
|
||||
use App\Http\Requests\EInvoice\Peppol\RemoveTaxIdentifierRequest;
|
||||
use App\Http\Requests\EInvoice\Peppol\RetrySendRequest;
|
||||
use App\Http\Requests\EInvoice\Peppol\ShowEntityRequest;
|
||||
use App\Http\Requests\EInvoice\Peppol\UpdateEntityRequest;
|
||||
use App\Services\EDocument\Jobs\SendEDocument;
|
||||
|
||||
class EInvoicePeppolController extends BaseController
|
||||
{
|
||||
|
|
@ -266,8 +267,12 @@ class EInvoicePeppolController extends BaseController
|
|||
|
||||
public function retrySend(RetrySendRequest $request)
|
||||
{
|
||||
|
||||
SendEDocument::dispatch($request->entity, $request->entity_id, auth()->user()->company()->db);
|
||||
if(auth()->user()->company()->verifactuEnabled()) {
|
||||
SendToAeat::dispatch($request->entity_id, auth()->user()->company(), 'create');
|
||||
}
|
||||
else {
|
||||
SendEDocument::dispatch($request->entity, $request->entity_id, auth()->user()->company()->db);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'trying....'], 200);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,13 @@ class EmailController extends BaseController
|
|||
$user = auth()->user();
|
||||
$company = $entity_obj->company;
|
||||
|
||||
/** Force AEAT Submission */
|
||||
if($company->verifactuEnabled() && ($entity_obj instanceof Invoice) && $entity_obj->backup->guid == "") {
|
||||
$entity_obj->invitations()->update(['email_error' => 'primed']); // Flag the invitations as primed for AEAT submission
|
||||
$entity_obj->service()->markSent()->sendVerifactu();
|
||||
return $this->itemResponse($entity_obj->fresh());
|
||||
}
|
||||
|
||||
if ($request->cc_email && (Ninja::isSelfHost() || $user->account->isPremium())) {
|
||||
|
||||
foreach ($request->cc_email as $email) {
|
||||
|
|
@ -80,19 +87,27 @@ class EmailController extends BaseController
|
|||
|
||||
}
|
||||
|
||||
$entity_obj->invitations->each(function ($invitation) use ($entity_obj, $mo, $template) {
|
||||
if (! $invitation->contact->trashed() && $invitation->contact->email && !$invitation->contact->is_locked) {
|
||||
$entity_obj->invitations()
|
||||
->whereHas('contact', function($query) {
|
||||
$query->where(function ($sq){
|
||||
$sq->whereNotNull('email')
|
||||
->orWhere('email', '!=', '');
|
||||
})->where('is_locked', false)
|
||||
->withoutTrashed();
|
||||
})
|
||||
->each(function ($invitation) use ($entity_obj, $mo, $template) {
|
||||
|
||||
$entity_obj->service()->markSent()->save();
|
||||
|
||||
$mo->invitation_id = $invitation->id;
|
||||
$mo->client_id = $invitation->contact->client_id ?? null;
|
||||
$mo->vendor_id = $invitation->contact->vendor_id ?? null;
|
||||
$mo->invitation_id = $invitation->id;
|
||||
$mo->client_id = $invitation->contact->client_id ?? null;
|
||||
$mo->vendor_id = $invitation->contact->vendor_id ?? null;
|
||||
|
||||
Email::dispatch($mo, $invitation->company);
|
||||
$entity_obj->entityEmailEvent($invitation, $template, $template);
|
||||
Email::dispatch($mo, $invitation->company);
|
||||
|
||||
$entity_obj->entityEmailEvent($invitation, $template, $template);
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$entity_obj = $entity_obj->fresh();
|
||||
$entity_obj->last_sent_date = now();
|
||||
|
|
|
|||
|
|
@ -12,30 +12,31 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\Expense\ExpenseWasCreated;
|
||||
use App\Events\Expense\ExpenseWasUpdated;
|
||||
use App\Factory\ExpenseFactory;
|
||||
use App\Filters\ExpenseFilters;
|
||||
use App\Http\Requests\Expense\BulkExpenseRequest;
|
||||
use App\Http\Requests\Expense\CreateExpenseRequest;
|
||||
use App\Http\Requests\Expense\DestroyExpenseRequest;
|
||||
use App\Http\Requests\Expense\EditExpenseRequest;
|
||||
use App\Http\Requests\Expense\EDocumentRequest;
|
||||
use App\Http\Requests\Expense\ShowExpenseRequest;
|
||||
use App\Http\Requests\Expense\StoreExpenseRequest;
|
||||
use App\Http\Requests\Expense\UpdateExpenseRequest;
|
||||
use App\Http\Requests\Expense\UploadExpenseRequest;
|
||||
use App\Jobs\EDocument\ImportEDocument;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Account;
|
||||
use App\Models\Expense;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Factory\ExpenseFactory;
|
||||
use App\Filters\ExpenseFilters;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Utils\Traits\Uploadable;
|
||||
use App\Utils\Traits\BulkOptions;
|
||||
use App\Utils\Traits\SavesDocuments;
|
||||
use App\Jobs\EDocument\ImportEDocument;
|
||||
use App\Repositories\ExpenseRepository;
|
||||
use App\Transformers\ExpenseTransformer;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Traits\BulkOptions;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Utils\Traits\SavesDocuments;
|
||||
use App\Utils\Traits\Uploadable;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Events\Expense\ExpenseWasCreated;
|
||||
use App\Events\Expense\ExpenseWasUpdated;
|
||||
use App\Services\Template\TemplateAction;
|
||||
use App\Http\Requests\Expense\EDocumentRequest;
|
||||
use App\Http\Requests\Expense\BulkExpenseRequest;
|
||||
use App\Http\Requests\Expense\EditExpenseRequest;
|
||||
use App\Http\Requests\Expense\ShowExpenseRequest;
|
||||
use App\Http\Requests\Expense\StoreExpenseRequest;
|
||||
use App\Http\Requests\Expense\CreateExpenseRequest;
|
||||
use App\Http\Requests\Expense\UpdateExpenseRequest;
|
||||
use App\Http\Requests\Expense\UploadExpenseRequest;
|
||||
use App\Http\Requests\Expense\DestroyExpenseRequest;
|
||||
|
||||
/**
|
||||
* Class ExpenseController.
|
||||
|
|
@ -329,6 +330,7 @@ class ExpenseController extends BaseController
|
|||
$user = auth()->user();
|
||||
|
||||
$expense = ExpenseFactory::create($user->company()->id, $user->id);
|
||||
$expense->date = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
|
||||
|
||||
return $this->itemResponse($expense);
|
||||
}
|
||||
|
|
@ -498,6 +500,25 @@ class ExpenseController extends BaseController
|
|||
|
||||
$expenses = Expense::withTrashed()->find($request->ids);
|
||||
|
||||
if ($request->action == 'template' && $user->can('view', $expenses->first())) {
|
||||
|
||||
$hash_or_response = $request->boolean('send_email') ? 'email sent' : \Illuminate\Support\Str::uuid();
|
||||
|
||||
TemplateAction::dispatch(
|
||||
$expenses->pluck('hashed_id')->toArray(),
|
||||
$request->template_id,
|
||||
Expense::class,
|
||||
$user->id,
|
||||
$user->company(),
|
||||
$user->company()->db,
|
||||
$hash_or_response,
|
||||
$request->boolean('send_email')
|
||||
);
|
||||
|
||||
return response()->json(['message' => $hash_or_response], 200);
|
||||
}
|
||||
|
||||
|
||||
if ($request->action == 'bulk_update' && $user->can('edit', $expenses->first())) {
|
||||
|
||||
$expenses = Expense::withTrashed()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use Illuminate\Http\Request;
|
||||
use Turbo124\Beacon\Facades\LightLogs;
|
||||
use App\DataMapper\Analytics\FeedbackCreated;
|
||||
|
||||
class FeedbackController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
if(Ninja::isHosted()){
|
||||
|
||||
$user = auth()->user();
|
||||
$company = $user->company();
|
||||
|
||||
$rating = $request->input('rating', 0);
|
||||
$notes = $request->input('notes', '');
|
||||
|
||||
LightLogs::create(new FeedbackCreated($rating, $notes, $company->company_key, $company->account->key, $user->present()->name()))->batch();
|
||||
}
|
||||
|
||||
return response()->noContent();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Gateways;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request; // Import the Request class
|
||||
use Illuminate\Support\Facades\Http; // Import the Http facade
|
||||
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
|
||||
class BlockonomicsController extends Controller
|
||||
{
|
||||
public function getBTCPrice(Request $request)
|
||||
{
|
||||
$currency = $request->query('currency');
|
||||
$response = Http::get("https://www.blockonomics.co/api/price?currency={$currency}");
|
||||
|
||||
if ($response->successful()) {
|
||||
return response()->json(['price' => $response->json('price')]);
|
||||
}
|
||||
|
||||
return response()->json(['error' => 'Unable to fetch BTC price'], 500);
|
||||
}
|
||||
|
||||
public function getQRCode(Request $request)
|
||||
{
|
||||
$qr_string = $request->query('qr_string');
|
||||
$svg = $this->getPaymentQrCodeRaw($qr_string);
|
||||
return response($svg)->header('Content-Type', 'image/svg+xml');
|
||||
}
|
||||
|
||||
private function getPaymentQrCodeRaw($qr_string)
|
||||
{
|
||||
|
||||
$renderer = new ImageRenderer(
|
||||
new RendererStyle(150, margin: 0),
|
||||
new SvgImageBackEnd()
|
||||
);
|
||||
$writer = new Writer($renderer);
|
||||
|
||||
$qr = $writer->writeString($qr_string, 'utf-8');
|
||||
|
||||
return $qr;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -443,11 +443,10 @@ class ImportController extends Controller
|
|||
|
||||
private function getCsvData($csvfile)
|
||||
{
|
||||
if (! ini_get('auto_detect_line_endings')) {
|
||||
ini_set('auto_detect_line_endings', '1');
|
||||
}
|
||||
|
||||
$csv = Reader::createFromString($csvfile);
|
||||
$csv = Reader::fromString($csvfile);
|
||||
|
||||
// $csv = Reader::createFromString($csvfile);
|
||||
$csvdelimiter = self::detectDelimiter($csvfile);
|
||||
$csv->setDelimiter($csvdelimiter);
|
||||
$stmt = new Statement();
|
||||
|
|
|
|||
|
|
@ -241,6 +241,8 @@ class InvoiceController extends BaseController
|
|||
|
||||
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars($user ? $user->id : null)));
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
|
||||
return $this->itemResponse($invoice);
|
||||
}
|
||||
|
||||
|
|
@ -495,24 +497,29 @@ class InvoiceController extends BaseController
|
|||
$ids = $request->input('ids');
|
||||
|
||||
if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !$user->company()->account->account_sms_verified) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
return response(['message' => 'Please verify your account to send emails.'], 400);
|
||||
}
|
||||
|
||||
if (Ninja::isHosted() && $user->account->emailQuotaExceeded()) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
return response(['message' => ctrans('texts.email_quota_exceeded_subject')], 400);
|
||||
}
|
||||
|
||||
if ($user->hasExactPermission('disable_emails') && (stripos($action, 'email') !== false)) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
return response(['message' => ctrans('texts.disable_emails_error')], 400);
|
||||
}
|
||||
|
||||
if (in_array($request->action, ['auto_bill', 'mark_paid']) && $user->cannot('create', \App\Models\Payment::class)) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
return response(['message' => ctrans('texts.not_authorized'), 'errors' => ['ids' => [ctrans('texts.not_authorized')]]], 422);
|
||||
}
|
||||
|
||||
$invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
|
||||
|
||||
if ($invoices->count() == 0) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
return response()->json(['message' => 'No Invoices Found']);
|
||||
}
|
||||
|
||||
|
|
@ -521,13 +528,15 @@ class InvoiceController extends BaseController
|
|||
*/
|
||||
|
||||
if ($action == 'bulk_download' && $invoices->count() > 1) {
|
||||
$invoices->each(function ($invoice) use ($user) {
|
||||
$invoices->each(function ($invoice) use ($user, $request) {
|
||||
if ($user->cannot('view', $invoice)) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
return response()->json(['message' => ctrans('text.access_denied')]);
|
||||
}
|
||||
});
|
||||
|
||||
ZipInvoices::dispatch($invoices->pluck('id'), $invoices->first()->company, auth()->user());
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
|
||||
return response()->json(['message' => ctrans('texts.sent_message')], 200);
|
||||
}
|
||||
|
|
@ -536,6 +545,8 @@ class InvoiceController extends BaseController
|
|||
|
||||
$filename = $invoices->first()->getFileName();
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
|
||||
return response()->streamDownload(function () use ($invoices) {
|
||||
echo $invoices->first()->service()->getInvoicePdf();
|
||||
}, $filename, ['Content-Type' => 'application/pdf']);
|
||||
|
|
@ -563,6 +574,7 @@ class InvoiceController extends BaseController
|
|||
})->toArray();
|
||||
|
||||
$mergedPdf = (new PdfMerge($paths))->run();
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
|
||||
return response()->streamDownload(function () use ($mergedPdf) {
|
||||
echo $mergedPdf;
|
||||
|
|
@ -588,6 +600,8 @@ class InvoiceController extends BaseController
|
|||
$request->boolean('send_email')
|
||||
);
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
|
||||
return response()->json(['message' => $hash_or_response], 200);
|
||||
}
|
||||
|
||||
|
|
@ -599,19 +613,21 @@ class InvoiceController extends BaseController
|
|||
}
|
||||
});
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
|
||||
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
}
|
||||
|
||||
if (in_array($action, ['email','send_email'])) {
|
||||
|
||||
$invoice = $invoices->first();
|
||||
|
||||
$invoices->filter(function ($invoice) use ($user) {
|
||||
return $user->can('edit', $invoice);
|
||||
})->each(function ($invoice) use ($user, $request) {
|
||||
$invoice->service()->sendEmail(email_type: $request->input('email_type', $invoice->calculateTemplate('invoice')));
|
||||
$invoice->service()->markSent()->sendEmail(email_type: $request->input('email_type', $invoice->calculateTemplate('invoice')));
|
||||
});
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
|
||||
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
|
||||
}
|
||||
|
|
@ -625,6 +641,7 @@ class InvoiceController extends BaseController
|
|||
});
|
||||
|
||||
/* Need to understand which permission are required for the given bulk action ie. view / edit */
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
|
||||
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
}
|
||||
|
|
@ -779,7 +796,7 @@ class InvoiceController extends BaseController
|
|||
}
|
||||
break;
|
||||
case 'cancel':
|
||||
$invoice = $invoice->service()->handleCancellation()->save();
|
||||
$invoice = $invoice->service()->handleCancellation(request()->input('reason'))->save();
|
||||
if (! $bulk) {
|
||||
$this->itemResponse($invoice);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,8 @@ class PaymentController extends BaseController
|
|||
|
||||
event('eloquent.created: App\Models\Payment', $payment);
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
|
||||
return $this->itemResponse($payment);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -561,7 +561,8 @@ class QuoteController extends BaseController
|
|||
|
||||
$quotes->each(function ($quote, $key) use ($user) {
|
||||
if ($user->can('edit', $quote) && $quote->service()->isConvertable()) {
|
||||
$quote->service()->convertToInvoice();
|
||||
// $quote->service()->convertToInvoice();
|
||||
$quote->service()->convert()->save();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,20 +34,21 @@ class ReportExportController extends BaseController
|
|||
return response()->json(['message' => 'Still working.....'], 409);
|
||||
}
|
||||
|
||||
// $report = base64_decode($report);
|
||||
$report = base64_decode($report);
|
||||
Cache::forget($hash);
|
||||
|
||||
// Cache::forget($hash);
|
||||
if($this->isXlsxData($report)){
|
||||
nlog("isXlsxData");
|
||||
return response($report, 200, [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Content-Disposition' => 'inline; filename="report.xlsx"',
|
||||
'Content-Length' => strlen($report)
|
||||
]);
|
||||
|
||||
// if($this->isXlsxData($report)){
|
||||
}
|
||||
|
||||
nlog(" IS NOT");
|
||||
|
||||
// return response($report, 200, [
|
||||
// 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
// 'Content-Disposition' => 'inline; filename="report.xlsx"',
|
||||
// 'Content-Length' => strlen($report)
|
||||
// ]);
|
||||
|
||||
// }
|
||||
|
||||
// Check if the content starts with PDF signature (%PDF-)
|
||||
$isPdf = str_starts_with(trim($report), '%PDF-');
|
||||
|
||||
|
|
@ -72,7 +73,7 @@ class ReportExportController extends BaseController
|
|||
// private function isXlsxData($fileData)
|
||||
// {
|
||||
// // Check minimum size (XLSX files are typically > 1KB)
|
||||
// if (strlen($fileData) < 1024) {
|
||||
// if (strlen($fileData ?? '') < 1024) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
|
|
@ -86,4 +87,13 @@ class ReportExportController extends BaseController
|
|||
// return strpos($fileData, '[Content_Types].xml') !== false;
|
||||
// }
|
||||
|
||||
|
||||
function isXlsxData(string $blob): bool {
|
||||
|
||||
// nlog(bin2hex(substr($blob, 0, 4)));
|
||||
nlog("504b0304" === bin2hex(substr($blob, 0, 4)));
|
||||
return "504b0304" === bin2hex(substr($blob, 0, 4));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,6 @@ class ReportPreviewController extends BaseController
|
|||
{
|
||||
|
||||
$report = Cache::get($hash);
|
||||
|
||||
nlog($report);
|
||||
|
||||
if (!$report) {
|
||||
return response()->json(['message' => 'Still working.....'], 409);
|
||||
|
|
|
|||
|
|
@ -302,8 +302,6 @@ class SNSController extends BaseController
|
|||
OPENSSL_ALGO_SHA1
|
||||
);
|
||||
|
||||
openssl_free_key($publicKey);
|
||||
|
||||
if ($verificationResult === 1) {
|
||||
nlog('SNS: Signature verification successful');
|
||||
return true;
|
||||
|
|
@ -401,13 +399,19 @@ class SNSController extends BaseController
|
|||
*/
|
||||
private function handleSubscriptionConfirmation(array $snsData)
|
||||
{
|
||||
$subscribeUrl = $snsData['SubscribeURL'] ?? null;
|
||||
// Verify the subscription confirmation payload
|
||||
$verificationResult = $this->verifySubscriptionConfirmationPayload($snsData);
|
||||
|
||||
if (!$subscribeUrl) {
|
||||
nlog('SNS Subscription confirmation: Missing SubscribeURL');
|
||||
return response()->json(['error' => 'Missing SubscribeURL'], 400);
|
||||
if (!$verificationResult['valid']) {
|
||||
nlog('SNS Subscription confirmation: Payload verification failed', [
|
||||
'errors' => $verificationResult['errors'],
|
||||
'payload' => $snsData
|
||||
]);
|
||||
return response()->json(['error' => 'Invalid subscription confirmation payload', 'details' => $verificationResult['errors']], 400);
|
||||
}
|
||||
|
||||
$subscribeUrl = $snsData['SubscribeURL'];
|
||||
|
||||
nlog('SNS Subscription confirmation received', [
|
||||
'topic_arn' => $snsData['TopicArn'] ?? 'unknown',
|
||||
'subscribe_url' => $subscribeUrl
|
||||
|
|
@ -416,7 +420,7 @@ class SNSController extends BaseController
|
|||
// You can optionally make an HTTP request to confirm the subscription
|
||||
// This is required by AWS to complete the SNS subscription setup
|
||||
try {
|
||||
$response = file_get_contents($subscribeUrl);
|
||||
$response = \Illuminate\Support\Facades\Http::timeout(10)->get($subscribeUrl);
|
||||
nlog('SNS Subscription confirmed', ['response' => $response]);
|
||||
} catch (\Exception $e) {
|
||||
nlog('SNS Subscription confirmation failed', ['error' => $e->getMessage()]);
|
||||
|
|
@ -425,6 +429,133 @@ class SNSController extends BaseController
|
|||
return response()->json(['status' => 'subscription_confirmed']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verify SNS subscription confirmation payload structure and content
|
||||
*
|
||||
* @param array $snsData
|
||||
* @return array ['valid' => bool, 'errors' => array]
|
||||
*/
|
||||
private function verifySubscriptionConfirmationPayload(array $snsData): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Required fields for subscription confirmation
|
||||
$requiredFields = [
|
||||
'Type' => 'SubscriptionConfirmation',
|
||||
'MessageId' => 'string',
|
||||
'TopicArn' => 'string',
|
||||
'SubscribeURL' => 'string',
|
||||
'Timestamp' => 'string',
|
||||
'Token' => 'string'
|
||||
];
|
||||
|
||||
// Validate required fields exist and have correct types
|
||||
foreach ($requiredFields as $field => $expectedType) {
|
||||
if (!isset($snsData[$field])) {
|
||||
$errors[] = "Missing required field: {$field}";
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $snsData[$field];
|
||||
|
||||
// Type-specific validation
|
||||
if ($expectedType === 'string' && !is_string($value)) {
|
||||
$errors[] = "Field '{$field}' must be a string";
|
||||
} elseif ($expectedType === 'SubscriptionConfirmation' && $value !== 'SubscriptionConfirmation') {
|
||||
$errors[] = "Field '{$field}' must be 'SubscriptionConfirmation'";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate specific field formats
|
||||
if (isset($snsData['MessageId']) && !$this->isValidMessageId($snsData['MessageId'])) {
|
||||
$errors[] = 'Invalid MessageId format';
|
||||
}
|
||||
|
||||
if (isset($snsData['TopicArn']) && !$this->isValidTopicArn($snsData['TopicArn'])) {
|
||||
$errors[] = 'Invalid TopicArn format';
|
||||
}
|
||||
|
||||
if (isset($snsData['SubscribeURL']) && !$this->isValidSubscribeUrl($snsData['SubscribeURL'])) {
|
||||
$errors[] = 'Invalid SubscribeURL format or domain';
|
||||
}
|
||||
|
||||
if (isset($snsData['Timestamp']) && !$this->isValidISOTimestamp($snsData['Timestamp'])) {
|
||||
$errors[] = 'Invalid Timestamp format (must be ISO 8601)';
|
||||
}
|
||||
|
||||
if (isset($snsData['Token']) && !$this->isValidSubscriptionToken($snsData['Token'])) {
|
||||
$errors[] = 'Invalid Token format';
|
||||
}
|
||||
|
||||
// Validate TopicArn matches expected if configured
|
||||
if (isset($snsData['TopicArn']) && !empty($this->expectedTopicArn)) {
|
||||
if ($snsData['TopicArn'] !== $this->expectedTopicArn) {
|
||||
$errors[] = 'TopicArn does not match expected value';
|
||||
}
|
||||
}
|
||||
|
||||
// Check for replay attacks (messages older than 15 minutes)
|
||||
if (isset($snsData['Timestamp'])) {
|
||||
$messageTimestamp = strtotime($snsData['Timestamp']);
|
||||
$currentTimestamp = time();
|
||||
if (($currentTimestamp - $messageTimestamp) > 900) { // 15 minutes
|
||||
$errors[] = 'Message timestamp is too old (potential replay attack)';
|
||||
}
|
||||
}
|
||||
|
||||
// Check for suspicious content patterns
|
||||
if ($this->containsSuspiciousContent($snsData)) {
|
||||
$errors[] = 'Payload contains suspicious content patterns';
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate MessageId format (should be a UUID-like string)
|
||||
*
|
||||
* @param string $messageId
|
||||
* @return bool
|
||||
*/
|
||||
private function isValidMessageId(string $messageId): bool
|
||||
{
|
||||
// AWS SNS MessageId is typically a UUID format
|
||||
return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $messageId) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate TopicArn format
|
||||
*
|
||||
* @param string $topicArn
|
||||
* @return bool
|
||||
*/
|
||||
private function isValidTopicArn(string $topicArn): bool
|
||||
{
|
||||
// AWS SNS Topic ARN format: arn:aws:sns:region:account-id:topic-name
|
||||
return preg_match('/^arn:aws:sns:[a-z0-9-]+:[0-9]{12}:[a-zA-Z0-9_-]+$/', $topicArn) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate subscription token format
|
||||
*
|
||||
* @param string $token
|
||||
* @return bool
|
||||
*/
|
||||
private function isValidSubscriptionToken(string $token): bool
|
||||
{
|
||||
// AWS SNS subscription tokens are typically long alphanumeric strings
|
||||
if (strlen($token) < 20 || strlen($token) > 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Should contain only alphanumeric characters and common symbols
|
||||
return preg_match('/^[a-zA-Z0-9+\/=\-_]+$/', $token) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SES notification from SNS
|
||||
*
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ class SearchController extends Controller
|
|||
|
||||
$params = [
|
||||
// 'index' => 'clients,invoices,client_contacts',
|
||||
// 'index' => 'clients,invoices,client_contacts,quotes,expenses,credits,recurring_invoices,vendors,vendor_contacts,purchase_orders,projects',
|
||||
'index' => 'clients_v2,invoices_v2,client_contacts_v2,quotes_v2,expenses_v2,credits_v2,recurring_invoices_v2,vendors_v2,vendor_contacts_v2,purchase_orders_v2,projects_v2,tasks_v2',
|
||||
'index' => 'clients,invoices,client_contacts,quotes,expenses_v2,credits,recurring_invoices,vendors,vendor_contacts,purchase_orders,projects',
|
||||
// 'index' => 'clients_v2,invoices_v2,client_contacts_v2,quotes_v2,expenses_v2,credits_v2,recurring_invoices_v2,vendors_v2,vendor_contacts_v2,purchase_orders_v2,projects_v2,tasks_v2',
|
||||
'body' => [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
|
|
@ -139,7 +139,7 @@ class SearchController extends Controller
|
|||
|
||||
$results = $elastic->search($params);
|
||||
|
||||
nlog($results['hits']);
|
||||
// nlog($results['hits']);
|
||||
|
||||
$this->mapResults($results['hits']['hits'] ?? []);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class SelfUpdateController extends BaseController
|
|||
set_time_limit(0);
|
||||
define('STDIN', fopen('php://stdin', 'r'));
|
||||
|
||||
if (Ninja::isHosted()) {
|
||||
if (Ninja::isHosted() || config('ninja.disable_auto_update')) {
|
||||
return response()->json(['message' => ctrans('texts.self_update_not_available')], 403);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -276,12 +276,32 @@ class TaskController extends BaseController
|
|||
|
||||
$old_task_status_order = $task->status_order;
|
||||
|
||||
$request_data = $request->all();
|
||||
|
||||
//2025-07-31 - if the start or stop query parameter is not present, then we need to save the task
|
||||
if (!($request->query('start', false) || $request->query('stop', false))) {
|
||||
$task = $this->task_repo->save($request->all(), $task);
|
||||
$task = $this->task_repo->save($request_data, $task);
|
||||
}
|
||||
else {
|
||||
|
||||
$task = $this->task_repo->triggeredActions($request, $task);
|
||||
|
||||
/*
|
||||
*
|
||||
* 2025-10-30 - if the start or stop query parameter is present,
|
||||
* then we need to trigger the actions and save the task
|
||||
* but we need to remove the time_log from the request data
|
||||
* because it will be updated by the triggeredActions method.
|
||||
*
|
||||
* Handles the scenario where the description is updated and then start/stop is pressed
|
||||
*/
|
||||
|
||||
$task = $this->task_repo->triggeredActions($request, $task);
|
||||
if(isset($request_data['time_log'])) {
|
||||
unset($request_data['time_log']);
|
||||
}
|
||||
|
||||
$task = $this->task_repo->save($request_data, $task);
|
||||
}
|
||||
|
||||
if (is_null($task->status_order) || $task->status_order != $old_task_status_order) {
|
||||
$this->task_repo->sortStatuses($task);
|
||||
|
|
@ -512,7 +532,19 @@ class TaskController extends BaseController
|
|||
|
||||
$ids = $request->input('ids');
|
||||
|
||||
$tasks = Task::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
|
||||
$tasks = Task::withTrashed()->whereIn('id', $this->transformKeys($ids))->company();
|
||||
|
||||
$_tasks = (clone $tasks);
|
||||
|
||||
if ($request->action == 'bulk_update' && $user->can('edit', $_tasks->first())) {
|
||||
|
||||
$this->task_repo->bulkUpdate($tasks, $request->column, $request->new_value);
|
||||
|
||||
return $this->listResponse(Task::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
|
||||
}
|
||||
|
||||
$tasks = $tasks->get();
|
||||
|
||||
if ($action == 'template' && $user->can('view', $tasks->first())) {
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class PurchaseOrderController extends Controller
|
|||
|
||||
$data = [
|
||||
'purchase_order' => $purchase_order,
|
||||
'key' => $invitation ? $invitation->key : false,
|
||||
'_key' => $invitation ? $invitation->key : false,
|
||||
'settings' => $purchase_order->company->settings,
|
||||
'sidebar' => $this->sidebarMenu(),
|
||||
'company' => $purchase_order->company,
|
||||
|
|
@ -124,7 +124,8 @@ class PurchaseOrderController extends Controller
|
|||
|
||||
$file = $invitation->purchase_order->service()->getPurchaseOrderPdf();
|
||||
|
||||
$headers = ['Content-Type' => 'application/pdf'];
|
||||
// $headers = ['Content-Type' => 'application/pdf'];
|
||||
$headers = ['Content-Type' => 'application/pdf', 'Content-Disposition' => 'inline'];
|
||||
|
||||
return response()->make($file, 200, $headers);
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ class QueryLogging
|
|||
}
|
||||
|
||||
LightLogs::create(new DbQuery($request->method(), substr(urldecode($request->url()), 0, 180), $count, $time, $ip, $client_version, $platform))
|
||||
->probe($request, $ip, true)
|
||||
->batch();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ class ValidJson
|
|||
is_null(json_decode($request->getContent())) &&
|
||||
json_last_error() !== JSON_ERROR_NONE
|
||||
) {
|
||||
|
||||
// nlog("Malformed JSON payload.");
|
||||
// nlog($request->all());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Malformed JSON payload.',
|
||||
'error' => 'Invalid JSON data provided',
|
||||
|
|
|
|||
|
|
@ -20,6 +20,167 @@ use App\Utils\Ninja;
|
|||
|
||||
class CreateAccountRequest extends Request
|
||||
{
|
||||
|
||||
private array $fake_domains = [
|
||||
'generator.email',
|
||||
'emailfake.com',
|
||||
'email-fake.com',
|
||||
'10minutemail.com',
|
||||
'10minutemail.net',
|
||||
'10minmail.com',
|
||||
'tempmail.com',
|
||||
'temp-mail.org',
|
||||
'tempmail.net',
|
||||
'tempmailo.com',
|
||||
'tempmail.ws',
|
||||
'tempmail.ninja',
|
||||
'tempmail.plus',
|
||||
'guerrillamail.com',
|
||||
'guerrillamail.net',
|
||||
'guerrillamail.org',
|
||||
'guerrillamail.de',
|
||||
'mailinator.com',
|
||||
'yopmail.com',
|
||||
'yopmail.fr',
|
||||
'yopmail.net',
|
||||
'maildrop.cc',
|
||||
'mailnesia.com',
|
||||
'throwawaymail.com',
|
||||
'fakemailgenerator.com',
|
||||
'trashmail.com',
|
||||
'trashmail.de',
|
||||
'spamgourmet.com',
|
||||
'mailondeck.com',
|
||||
'getnada.com',
|
||||
'nada.ltd',
|
||||
'dispostable.com',
|
||||
'mohmal.com',
|
||||
'33mail.com',
|
||||
'anonaddy.com',
|
||||
'simplelogin.io',
|
||||
'burnermail.io',
|
||||
'burnermail.com',
|
||||
'mytempmail.org',
|
||||
'fakeinbox.com',
|
||||
'mail-temp.com',
|
||||
'20minutemail.com',
|
||||
'30minutemail.com',
|
||||
'60minutemail.com',
|
||||
'0wnd.net',
|
||||
'0clickemail.com',
|
||||
'0-mail.com',
|
||||
'mailcatch.com',
|
||||
'mailcatch.org',
|
||||
'mailforspam.com',
|
||||
'getairmail.com',
|
||||
'mailtothis.com',
|
||||
'spam4.me',
|
||||
'spamdecoy.net',
|
||||
'mailnull.com',
|
||||
'inboxkitten.com',
|
||||
'mailimate.com',
|
||||
'instantemailaddress.com',
|
||||
'mailfreeonline.com',
|
||||
'mailsac.com',
|
||||
'fakebox.org',
|
||||
'nowmymail.com',
|
||||
'mail-temporaire.fr',
|
||||
'trashmail.ws',
|
||||
'spamspot.com',
|
||||
'mail-temporaire.com',
|
||||
'tempinbox.com',
|
||||
'mailcatch.io',
|
||||
'inboxalias.com',
|
||||
'get-mail.org',
|
||||
'mailpoof.com',
|
||||
'temporary-mail.net',
|
||||
'mytrashmail.com',
|
||||
'spambog.com',
|
||||
'spambog.de',
|
||||
'spambox.us',
|
||||
'spamcorptastic.com',
|
||||
'temporarymail.com',
|
||||
'mail-tester.com',
|
||||
'temporarymail.org',
|
||||
'emailondeck.com',
|
||||
'easytrashmail.com',
|
||||
'mailsucker.net',
|
||||
'fakeemailgenerator.com',
|
||||
'mail7.io',
|
||||
'sharklasers.com',
|
||||
'spamgourmet.net',
|
||||
'fakermail.com',
|
||||
'dodsi.com',
|
||||
'spamstack.net',
|
||||
'byom.de',
|
||||
'temporarymailaddress.com',
|
||||
'mail-temp.org',
|
||||
'spambox.info',
|
||||
'luxusmail.org',
|
||||
'e-mail.com',
|
||||
'trash-me.com',
|
||||
'fexbox.org',
|
||||
'getonemail.com',
|
||||
'mailhub.pro',
|
||||
'cryptogmail.com',
|
||||
'mailjunkie.com',
|
||||
'fake-mail.io',
|
||||
'disposablemail.com',
|
||||
'disposableinbox.com',
|
||||
'mailswipe.net',
|
||||
'instantmailaddress.com',
|
||||
'dropmail.me',
|
||||
'trashmails.com',
|
||||
'spambog.ru',
|
||||
'fakeinbox.org',
|
||||
'meltmail.com',
|
||||
'mail-temporaire.info',
|
||||
'mailnesia.net',
|
||||
'mailscreen.com',
|
||||
'spambooger.com',
|
||||
'tempr.email',
|
||||
'emailondeck.net',
|
||||
'throwawayaddress.com',
|
||||
'mail-temp.info',
|
||||
'mailinator.net',
|
||||
'emailtemporal.org',
|
||||
'tempmailgen.com',
|
||||
'temporaryemail.net',
|
||||
'temporaryinbox.com',
|
||||
'tempmailer.com',
|
||||
'tempmailer.de',
|
||||
'mymail-in.net',
|
||||
'trashmail.net',
|
||||
'mailexpire.com',
|
||||
'mailhazard.com',
|
||||
'guerrillamailblock.com',
|
||||
'temporary-mail.org',
|
||||
'mailcatch.io',
|
||||
'emailtemporal.com',
|
||||
'dropmail.ga',
|
||||
'tempail.com',
|
||||
'spambox.org',
|
||||
'mytemp.email',
|
||||
'mailspeed.ru',
|
||||
'mailimate.org',
|
||||
'getairmail.com',
|
||||
'dispostable.org',
|
||||
'emailfake.org',
|
||||
'fakemail.net',
|
||||
'tempmailaddress.com',
|
||||
'tempmailbox.com',
|
||||
'mailbox92.biz',
|
||||
'tempmailgen.org',
|
||||
'mail-temporaire.net',
|
||||
'tempmail24.com',
|
||||
'inboxbear.com',
|
||||
'maildrop.cf',
|
||||
'maildrop.ga',
|
||||
'maildrop.gq',
|
||||
'maildrop.ml',
|
||||
'maildrop.tk',
|
||||
];
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
|
|
@ -59,10 +220,33 @@ class CreateAccountRequest extends Request
|
|||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
|
||||
$validator->after(function ($validator) {
|
||||
|
||||
|
||||
try {
|
||||
$domain = explode("@", $this->input('email'))[1] ?? "";
|
||||
$dns = dns_get_record($domain, DNS_MX);
|
||||
$server = $dns[0]["target"] ?? null;
|
||||
|
||||
if($server && in_array($server, $this->fake_domains)){
|
||||
$validator->errors()->add('email', 'Account Already Exists.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
nlog($e->getMessage());
|
||||
nlog("I could not check the email address => ".$this->input('email'));
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
|
||||
nlog(array_merge(['signup' => 'true', 'ipaddy' => request()->ip()], $this->all()));
|
||||
nlog(array_merge(['signup' => 'true', 'ipaddy' => request()->ip(), 'headers' => request()->headers->all()], $this->all()));
|
||||
|
||||
$input = $this->all();
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class BulkClientRequest extends Request
|
|||
'action' => 'required|string|in:archive,restore,delete,template,assign_group,bulk_update',
|
||||
'ids' => ['required','bail','array',Rule::exists('clients', 'id')->where('company_id', $user->company()->id)],
|
||||
'template' => 'sometimes|string',
|
||||
'template_id' => 'sometimes|string',
|
||||
'template_id' => 'sometimes|string|required_if:action,template',
|
||||
'group_settings_id' => ['required_if:action,assign_group',Rule::exists('group_settings', 'id')->where('company_id', $user->company()->id)],
|
||||
'send_email' => 'sometimes|bool',
|
||||
'column' => ['required_if:action,bulk_update', 'string', Rule::in(\App\Models\Client::$bulk_update_columns)],
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class StoreClientRequest extends Request
|
|||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$rules['name'] = 'bail|sometimes|nullable|string';
|
||||
$rules['file'] = 'bail|sometimes|array';
|
||||
$rules['file.*'] = $this->fileValidation();
|
||||
$rules['documents'] = 'bail|sometimes|array';
|
||||
|
|
@ -99,6 +100,20 @@ class StoreClientRequest extends Request
|
|||
return $rules;
|
||||
}
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
|
||||
$user = auth()->user();
|
||||
$company = $user->company();
|
||||
|
||||
if(isset($this->settings['lock_invoices']) && $company->verifactuEnabled() && $this->settings['lock_invoices'] != 'when_sent'){
|
||||
$validator->errors()->add('settings.lock_invoices', 'Locked Invoices Cannot Be Disabled');
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
|
@ -185,7 +200,7 @@ class StoreClientRequest extends Request
|
|||
}
|
||||
|
||||
// prevent xss injection
|
||||
if (array_key_exists('name', $input)) {
|
||||
if (array_key_exists('name', $input) && is_string($input['name'])) {
|
||||
$input['name'] = strip_tags($input['name']);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,20 @@ class UpdateClientRequest extends Request
|
|||
return $rules;
|
||||
}
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
|
||||
$user = auth()->user();
|
||||
$company = $user->company();
|
||||
|
||||
if(isset($this->settings['lock_invoices']) && $company->verifactuEnabled() && $this->settings['lock_invoices'] != 'when_sent'){
|
||||
$validator->errors()->add('settings.lock_invoices', 'Locked Invoices Cannot Be Disabled');
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -213,6 +213,15 @@ class UpdateCompanyRequest extends Request
|
|||
$settings[$protected_var] = str_replace("script", "", $settings[$protected_var]);
|
||||
}
|
||||
}
|
||||
|
||||
if($this->company->getSetting('e_invoice_type') == 'VERIFACTU') {
|
||||
$settings['e_invoice_type'] = 'VERIFACTU';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(isset($settings['e_invoice_type']) && $settings['e_invoice_type'] == 'VERIFACTU' && $this->company->verifactuEnabled()) {
|
||||
$settings['lock_invoices'] = 'when_sent';
|
||||
}
|
||||
|
||||
if (isset($settings['email_style_custom'])) {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,12 @@ class UpdateCreditRequest extends Request
|
|||
return $rules;
|
||||
}
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ class StoreDesignRequest extends Request
|
|||
'credit',
|
||||
'purchase_order',
|
||||
'project',
|
||||
'task'
|
||||
'task',
|
||||
'expense'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -89,7 +90,7 @@ class StoreDesignRequest extends Request
|
|||
$input['design']['body'] = '';
|
||||
}
|
||||
|
||||
if (array_key_exists('entities', $input)) {
|
||||
if (array_key_exists('entities', $input) && is_string($input['entities'])) {
|
||||
$user_entities = explode(",", $input['entities']);
|
||||
|
||||
$e = [];
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ class UpdateDesignRequest extends Request
|
|||
'credit',
|
||||
'purchase_order',
|
||||
'project',
|
||||
'task'
|
||||
'task',
|
||||
'expense'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -80,7 +81,7 @@ class UpdateDesignRequest extends Request
|
|||
$input['design']['body'] = '';
|
||||
}
|
||||
|
||||
if (array_key_exists('entities', $input)) {
|
||||
if (array_key_exists('entities', $input) && is_string($input['entities'])) {
|
||||
$user_entities = explode(",", $input['entities']);
|
||||
|
||||
$e = [];
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class RetrySendRequest extends Request
|
|||
return true;
|
||||
}
|
||||
|
||||
return $user->account->isPaid() && $user->isAdmin() && $user->company()->legal_entity_id != null;
|
||||
return $user->account->isPaid() && $user->isAdmin() && ($user->company()->legal_entity_id != null || $user->company()->verifactuEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -53,14 +53,4 @@ class UpdateEntityRequest extends FormRequest
|
|||
$this->replace($input);
|
||||
}
|
||||
|
||||
// public function after(): array
|
||||
// {
|
||||
// return [
|
||||
// function (Validator $validator) {
|
||||
// if ($this->input('acts_as_sender') === false && $this->input('acts_as_receiver') === false) {
|
||||
// $validator->errors()->add('acts_as_receiver', ctrans('texts.acts_as_must_be_true'));
|
||||
// }
|
||||
// }
|
||||
// ];
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use App\Models\Client;
|
|||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ValidateEInvoiceRequest extends Request
|
||||
|
|
@ -76,7 +77,6 @@ class ValidateEInvoiceRequest extends Request
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
$class = Invoice::class;
|
||||
|
||||
match ($this->entity) {
|
||||
|
|
@ -93,4 +93,25 @@ class ValidateEInvoiceRequest extends Request
|
|||
return $class::withTrashed()->find(is_string($this->entity_id) ? $this->decodePrimaryKey($this->entity_id) : $this->entity_id);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* getValidatorClass
|
||||
*
|
||||
* Return the validator class based on the EInvoicing Standard
|
||||
*
|
||||
* @return \App\Services\EDocument\Standards\Validation\EntityLevelInterface
|
||||
*/
|
||||
public function getValidatorClass()
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if($user->company()->settings->e_invoice_type == 'VERIFACTU') {
|
||||
return new \App\Services\EDocument\Standards\Validation\Verifactu\EntityLevel();
|
||||
}
|
||||
|
||||
// if($user->company()->settings->e_invoice_type == 'PEPPOL') {
|
||||
return new \App\Services\EDocument\Standards\Validation\Peppol\EntityLevel();
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,11 +38,14 @@ class BulkExpenseRequest extends Request
|
|||
$user = auth()->user();
|
||||
|
||||
return [
|
||||
'action' => 'required|string|in:archive,restore,delete,bulk_update,bulk_categorize',
|
||||
'action' => 'required|string|in:archive,restore,delete,bulk_update,bulk_categorize,template',
|
||||
'ids' => ['required','bail','array', Rule::exists('expenses', 'id')->where('company_id', $user->company()->id)],
|
||||
'category_id' => ['sometimes', 'bail', Rule::exists('expense_categories', 'id')->where('company_id', $user->company()->id)],
|
||||
'column' => ['required_if:action,bulk_update', 'string', Rule::in(\App\Models\Expense::$bulk_update_columns)],
|
||||
'new_value' => ['required_if:action,bulk_update|string'],
|
||||
'template' => 'sometimes|string',
|
||||
'template_id' => 'sometimes|string|required_if:action,template',
|
||||
'send_email' => 'sometimes|bool',
|
||||
];
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class UpdateExpenseRequest extends Request
|
|||
unset($input['documents']);
|
||||
}
|
||||
|
||||
if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) {
|
||||
if (! array_key_exists('currency_id', $input) || strlen($input['currency_id'] ?? '') == 0) {
|
||||
$input['currency_id'] = (string) $user->company()->settings->currency_id;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,20 @@ class StoreGroupSettingRequest extends Request
|
|||
return $rules;
|
||||
}
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
|
||||
$user = auth()->user();
|
||||
$company = $user->company();
|
||||
|
||||
if(isset($this->settings['lock_invoices']) && $company->verifactuEnabled() && $this->settings['lock_invoices'] != 'when_sent'){
|
||||
$validator->errors()->add('settings.lock_invoices', 'Locked Invoices Cannot Be Disabled');
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
|
|
|||
|
|
@ -41,6 +41,22 @@ class UpdateGroupSettingRequest extends Request
|
|||
|
||||
}
|
||||
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
|
||||
$user = auth()->user();
|
||||
$company = $user->company();
|
||||
|
||||
if(isset($this->settings['lock_invoices']) && $company->verifactuEnabled() && $this->settings['lock_invoices'] != 'when_sent'){
|
||||
$validator->errors()->add('settings.lock_invoices', 'Locked Invoices Cannot Be Disabled');
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class ActionInvoiceRequest extends Request
|
|||
*
|
||||
* @return bool
|
||||
*/
|
||||
private $error_msg;
|
||||
// private $error_msg;
|
||||
|
||||
// private $invoice;
|
||||
|
||||
|
|
@ -38,36 +38,37 @@ class ActionInvoiceRequest extends Request
|
|||
public function rules()
|
||||
{
|
||||
return [
|
||||
'action' => 'required',
|
||||
'action' => ['required'],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
|
||||
$validator->after(function ($validator) {
|
||||
|
||||
if ($this->action == 'delete' && ! $this->invoiceDeletable($this->invoice)) {
|
||||
$validator->errors()->add('action', 'This invoice cannot be deleted');
|
||||
}elseif ($this->action == 'cancel' && ! $this->invoiceCancellable($this->invoice)) {
|
||||
$validator->errors()->add('action', 'This invoice cannot be cancelled');
|
||||
}elseif ($this->action == 'reverse' && ! $this->invoiceReversable($this->invoice)) {
|
||||
$validator->errors()->add('action', 'This invoice cannot be reversed');
|
||||
}elseif($this->action == 'restore' && ! $this->invoiceRestorable($this->invoice)) {
|
||||
$validator->errors()->add('action', 'This invoice cannot be restored');
|
||||
}elseif($this->action == 'mark_paid' && ! $this->invoicePayable($this->invoice)) {
|
||||
$validator->errors()->add('action', 'This invoice cannot be marked as paid');
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
||||
if ($this->action) {
|
||||
$input['action'] = $this->action;
|
||||
} elseif (! array_key_exists('action', $input)) {
|
||||
$this->error_msg = 'Action is a required field';
|
||||
} elseif (! $this->invoiceDeletable($this->invoice)) {
|
||||
unset($input['action']);
|
||||
$this->error_msg = 'This invoice cannot be deleted';
|
||||
} elseif (! $this->invoiceCancellable($this->invoice)) {
|
||||
unset($input['action']);
|
||||
$this->error_msg = 'This invoice cannot be cancelled';
|
||||
} elseif (! $this->invoiceReversable($this->invoice)) {
|
||||
unset($input['action']);
|
||||
$this->error_msg = 'This invoice cannot be reversed';
|
||||
}
|
||||
|
||||
$input['action'] = $this->route('action');
|
||||
|
||||
$this->replace($input);
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'action' => $this->error_msg,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue