|
|
@@ -13,6 +13,7 @@ use App\DTO\AgingBucketDTO;
|
|
13
|
13
|
use App\DTO\CashFlowOverviewDTO;
|
|
14
|
14
|
use App\DTO\EntityBalanceDTO;
|
|
15
|
15
|
use App\DTO\EntityReportDTO;
|
|
|
16
|
+use App\DTO\PaymentMetricsDTO;
|
|
16
|
17
|
use App\DTO\ReportDTO;
|
|
17
|
18
|
use App\Enums\Accounting\AccountCategory;
|
|
18
|
19
|
use App\Enums\Accounting\AccountType;
|
|
|
@@ -30,6 +31,7 @@ use App\Utilities\Currency\CurrencyConverter;
|
|
30
|
31
|
use App\ValueObjects\Money;
|
|
31
|
32
|
use Illuminate\Database\Eloquent\Builder;
|
|
32
|
33
|
use Illuminate\Support\Carbon;
|
|
|
34
|
+use Illuminate\Support\Number;
|
|
33
|
35
|
|
|
34
|
36
|
class ReportService
|
|
35
|
37
|
{
|
|
|
@@ -844,7 +846,6 @@ class ReportService
|
|
844
|
846
|
): ReportDTO {
|
|
845
|
847
|
$asOfDateCarbon = Carbon::parse($asOfDate);
|
|
846
|
848
|
|
|
847
|
|
- /** @var DocumentCollection<int,DocumentCollection<int,Invoice|Bill>> $documents */
|
|
848
|
849
|
$documents = $entityType === DocumentEntityType::Client
|
|
849
|
850
|
? $this->accountService->getUnpaidClientInvoices($asOfDate)->with(['client:id,name'])->get()->groupBy('client_id')
|
|
850
|
851
|
: $this->accountService->getUnpaidVendorBills($asOfDate)->with(['vendor:id,name'])->get()->groupBy('vendor_id');
|
|
|
@@ -859,6 +860,7 @@ class ReportService
|
|
859
|
860
|
$totalAging['over_periods'] = 0;
|
|
860
|
861
|
$totalAging['total'] = 0;
|
|
861
|
862
|
|
|
|
863
|
+ /** @var DocumentCollection<int,Invoice|Bill> $entityDocuments */
|
|
862
|
864
|
foreach ($documents as $entityId => $entityDocuments) {
|
|
863
|
865
|
$aging = [
|
|
864
|
866
|
'current' => $entityDocuments
|
|
|
@@ -909,8 +911,8 @@ class ReportService
|
|
909
|
911
|
|
|
910
|
912
|
public function buildEntityBalanceSummaryReport(string $startDate, string $endDate, DocumentEntityType $entityType, array $columns = []): ReportDTO
|
|
911
|
913
|
{
|
|
912
|
|
- $documents = $entityType === DocumentEntityType::Client
|
|
913
|
|
- ? Invoice::query()
|
|
|
914
|
+ $documents = match ($entityType) {
|
|
|
915
|
+ DocumentEntityType::Client => Invoice::query()
|
|
914
|
916
|
->whereBetween('date', [$startDate, $endDate])
|
|
915
|
917
|
->whereNotIn('status', [
|
|
916
|
918
|
InvoiceStatus::Draft,
|
|
|
@@ -919,15 +921,14 @@ class ReportService
|
|
919
|
921
|
->whereNotNull('approved_at')
|
|
920
|
922
|
->with(['client:id,name'])
|
|
921
|
923
|
->get()
|
|
922
|
|
- ->groupBy('client_id')
|
|
923
|
|
- : Bill::query()
|
|
|
924
|
+ ->groupBy('client_id'),
|
|
|
925
|
+ DocumentEntityType::Vendor => Bill::query()
|
|
924
|
926
|
->whereBetween('date', [$startDate, $endDate])
|
|
925
|
|
- ->whereNotIn('status', [
|
|
926
|
|
- BillStatus::Void,
|
|
927
|
|
- ])
|
|
|
927
|
+ ->whereNot('status', BillStatus::Void)
|
|
928
|
928
|
->with(['vendor:id,name'])
|
|
929
|
929
|
->get()
|
|
930
|
|
- ->groupBy('vendor_id');
|
|
|
930
|
+ ->groupBy('vendor_id'),
|
|
|
931
|
+ };
|
|
931
|
932
|
|
|
932
|
933
|
$entities = [];
|
|
933
|
934
|
$totalBalance = 0;
|
|
|
@@ -980,4 +981,111 @@ class ReportService
|
|
980
|
981
|
endDate: Carbon::parse($endDate),
|
|
981
|
982
|
);
|
|
982
|
983
|
}
|
|
|
984
|
+
|
|
|
985
|
+ public function buildEntityPaymentPerformanceReport(
|
|
|
986
|
+ string $startDate,
|
|
|
987
|
+ string $endDate,
|
|
|
988
|
+ DocumentEntityType $entityType,
|
|
|
989
|
+ array $columns = []
|
|
|
990
|
+ ): ReportDTO {
|
|
|
991
|
+ $documents = match ($entityType) {
|
|
|
992
|
+ DocumentEntityType::Client => Invoice::query()
|
|
|
993
|
+ ->whereBetween('date', [$startDate, $endDate])
|
|
|
994
|
+ ->whereNotIn('status', [InvoiceStatus::Draft, InvoiceStatus::Void])
|
|
|
995
|
+ ->whereNotNull('approved_at')
|
|
|
996
|
+ ->whereNotNull('paid_at')
|
|
|
997
|
+ ->with(['client:id,name'])
|
|
|
998
|
+ ->get()
|
|
|
999
|
+ ->groupBy('client_id'),
|
|
|
1000
|
+ DocumentEntityType::Vendor => Bill::query()
|
|
|
1001
|
+ ->whereBetween('date', [$startDate, $endDate])
|
|
|
1002
|
+ ->whereNotIn('status', [BillStatus::Void])
|
|
|
1003
|
+ ->whereNotNull('paid_at')
|
|
|
1004
|
+ ->with(['vendor:id,name'])
|
|
|
1005
|
+ ->get()
|
|
|
1006
|
+ ->groupBy('vendor_id'),
|
|
|
1007
|
+ };
|
|
|
1008
|
+
|
|
|
1009
|
+ $categories = [];
|
|
|
1010
|
+ $totalDocs = 0;
|
|
|
1011
|
+ $totalOnTime = 0;
|
|
|
1012
|
+ $totalLate = 0;
|
|
|
1013
|
+ $allPaymentDays = [];
|
|
|
1014
|
+ $allLateDays = [];
|
|
|
1015
|
+
|
|
|
1016
|
+ /** @var DocumentCollection<int,Invoice|Bill> $entityDocuments */
|
|
|
1017
|
+ foreach ($documents as $entityId => $entityDocuments) {
|
|
|
1018
|
+ $entity = $entityDocuments->first()->{$entityType->value};
|
|
|
1019
|
+
|
|
|
1020
|
+ $onTimeDocs = $entityDocuments->filter(fn (Invoice | Bill $doc) => $doc->paid_at->lte($doc->due_date));
|
|
|
1021
|
+ $onTimeCount = $onTimeDocs->count();
|
|
|
1022
|
+
|
|
|
1023
|
+ $lateDocs = $entityDocuments->filter(fn (Invoice | Bill $doc) => $doc->paid_at->gt($doc->due_date));
|
|
|
1024
|
+ $lateCount = $lateDocs->count();
|
|
|
1025
|
+
|
|
|
1026
|
+ $avgDaysToPay = $entityDocuments->avg(
|
|
|
1027
|
+ fn (Invoice | Bill $doc) => $doc instanceof Invoice
|
|
|
1028
|
+ ? $doc->approved_at->diffInDays($doc->paid_at)
|
|
|
1029
|
+ : $doc->date->diffInDays($doc->paid_at)
|
|
|
1030
|
+ ) ?? 0;
|
|
|
1031
|
+
|
|
|
1032
|
+ $avgDaysLate = $lateDocs->avg(fn (Invoice | Bill $doc) => $doc->due_date->diffInDays($doc->paid_at)) ?? 0;
|
|
|
1033
|
+
|
|
|
1034
|
+ $onTimeRate = $entityDocuments->isNotEmpty()
|
|
|
1035
|
+ ? ($onTimeCount / $entityDocuments->count() * 100)
|
|
|
1036
|
+ : 0;
|
|
|
1037
|
+
|
|
|
1038
|
+ $totalDocs += $entityDocuments->count();
|
|
|
1039
|
+ $totalOnTime += $onTimeCount;
|
|
|
1040
|
+ $totalLate += $lateCount;
|
|
|
1041
|
+
|
|
|
1042
|
+ $entityDocuments->each(function (Invoice | Bill $doc) use (&$allPaymentDays, &$allLateDays) {
|
|
|
1043
|
+ $allPaymentDays[] = $doc instanceof Invoice
|
|
|
1044
|
+ ? $doc->approved_at->diffInDays($doc->paid_at)
|
|
|
1045
|
+ : $doc->date->diffInDays($doc->paid_at);
|
|
|
1046
|
+
|
|
|
1047
|
+ if ($doc->paid_at->gt($doc->due_date)) {
|
|
|
1048
|
+ $allLateDays[] = $doc->due_date->diffInDays($doc->paid_at);
|
|
|
1049
|
+ }
|
|
|
1050
|
+ });
|
|
|
1051
|
+
|
|
|
1052
|
+ $categories[] = new EntityReportDTO(
|
|
|
1053
|
+ name: $entity->name,
|
|
|
1054
|
+ id: $entityId,
|
|
|
1055
|
+ paymentMetrics: new PaymentMetricsDTO(
|
|
|
1056
|
+ totalDocuments: $entityDocuments->count(),
|
|
|
1057
|
+ onTimeCount: $onTimeCount ?: null,
|
|
|
1058
|
+ lateCount: $lateCount ?: null,
|
|
|
1059
|
+ avgDaysToPay: $avgDaysToPay ? round($avgDaysToPay) : null,
|
|
|
1060
|
+ avgDaysLate: $avgDaysLate ? round($avgDaysLate) : null,
|
|
|
1061
|
+ onTimePaymentRate: Number::percentage($onTimeRate, maxPrecision: 2),
|
|
|
1062
|
+ ),
|
|
|
1063
|
+ );
|
|
|
1064
|
+ }
|
|
|
1065
|
+
|
|
|
1066
|
+ $categories = collect($categories)
|
|
|
1067
|
+ ->sortByDesc(static fn (EntityReportDTO $category) => $category->paymentMetrics->onTimePaymentRate, SORT_NATURAL)
|
|
|
1068
|
+ ->values()
|
|
|
1069
|
+ ->all();
|
|
|
1070
|
+
|
|
|
1071
|
+ $overallMetrics = new PaymentMetricsDTO(
|
|
|
1072
|
+ totalDocuments: $totalDocs,
|
|
|
1073
|
+ onTimeCount: $totalOnTime,
|
|
|
1074
|
+ lateCount: $totalLate,
|
|
|
1075
|
+ avgDaysToPay: round(collect($allPaymentDays)->avg() ?? 0),
|
|
|
1076
|
+ avgDaysLate: round(collect($allLateDays)->avg() ?? 0),
|
|
|
1077
|
+ onTimePaymentRate: Number::percentage(
|
|
|
1078
|
+ $totalDocs > 0 ? ($totalOnTime / $totalDocs * 100) : 0,
|
|
|
1079
|
+ maxPrecision: 2
|
|
|
1080
|
+ ),
|
|
|
1081
|
+ );
|
|
|
1082
|
+
|
|
|
1083
|
+ return new ReportDTO(
|
|
|
1084
|
+ categories: ['Entities' => $categories],
|
|
|
1085
|
+ overallPaymentMetrics: $overallMetrics,
|
|
|
1086
|
+ fields: $columns,
|
|
|
1087
|
+ startDate: Carbon::parse($startDate),
|
|
|
1088
|
+ endDate: Carbon::parse($endDate),
|
|
|
1089
|
+ );
|
|
|
1090
|
+ }
|
|
983
|
1091
|
}
|