hasMany(GoogleCampaign::class); } public function assignations() { return $this->hasMany(ClientUserAssignation::class); } public function invoices(): HasMany { return $this->hasMany(ClientInvoice::class); } public function invoiceAdjustments(): HasMany { return $this->hasMany(ClientInvoiceAdjustment::class); } public function getLatestRemainingAmountAttribute(): string { return $this->latestRemainingAmount(); } public function getLatestStartDateAttribute(): ?string { return $this->latestPaymentDateRange()['start_date']; } public function getLatestEndDateAttribute(): ?string { return $this->latestPaymentDateRange()['end_date']; } public function getLatestRemainingDaysAttribute(): ?int { return $this->latestRemainingDays(); } public function latestRemainingAmount(): string { $invoices = $this->relationLoaded('invoices') ? $this->invoices : $this->invoices()->with('payments.items.billingItemType')->get(); $invoices->loadMissing('payments.items.billingItemType', 'linkedInvoice.payments.items.billingItemType'); $adjustments = $this->relationLoaded('invoiceAdjustments') ? $this->invoiceAdjustments : $this->invoiceAdjustments()->get(['client_id', 'entry_type', 'amount']); $invoiceItems = $invoices->flatMap(fn (ClientInvoice $invoice) => $invoice->payments ->flatMap(fn ($payment) => $payment->items ->map(fn (ClientInvoicePaymentItem $item) => [ 'invoice' => $invoice, 'item' => $item, ]))); $nettAmount = $invoiceItems ->filter(fn (array $row) => ! $row['item']->is_creditcard && ($row['item']->billingItemType?->nett_contribution ?? false)) ->sum(fn (array $row) => (float) ($row['item']->final_net_amount ?? $row['item']->net_amount ?? 0)); $billableSpending = $invoiceItems ->reject(fn (array $row) => $this->isCreditCardSpend($row['invoice'], $row['item'])) ->sum(fn (array $row) => (float) ($row['item']->spending ?? 0)); $adjustmentNet = $adjustments->sum(function (ClientInvoiceAdjustment $adjustment) { $amount = (float) ($adjustment->amount ?? 0); return $adjustment->entry_type === ClientInvoiceAdjustment::TYPE_CREDIT ? $amount : -$amount; }); return number_format($nettAmount + $adjustmentNet - $billableSpending, 2, '.', ''); } public function latestPaymentDateRange(): array { if ($this->latestPaymentDateRange !== null) { return $this->latestPaymentDateRange; } $row = $this->effectiveInvoiceItems() ->filter(fn (array $row) => $row['item']->start_date !== null || $row['item']->end_date !== null) ->sortByDesc(function (array $row) { $item = $row['item']; return sprintf( '%s|%s|%010d', $this->dateSortValue($item->start_date), $this->dateSortValue($item->end_date), $item->id ?? 0, ); }) ->first(); if ($row === null) { return $this->latestPaymentDateRange = [ 'start_date' => null, 'end_date' => null, ]; } return $this->latestPaymentDateRange = [ 'start_date' => $row['item']->start_date?->toDateString(), 'end_date' => $row['item']->end_date?->toDateString(), ]; } public function latestRemainingDays(): ?int { $range = $this->latestPaymentDateRange(); if ($range['start_date'] === null || $range['end_date'] === null) { return null; } $startDate = Carbon::parse($range['start_date'])->startOfDay(); $endDate = Carbon::parse($range['end_date'])->startOfDay(); if ($endDate->lessThan($startDate)) { return null; } $today = today()->startOfDay(); if ($today->greaterThan($endDate)) { return 0; } $fromDate = $today->lessThan($startDate) ? $startDate : $today; return (int) $fromDate->diffInDays($endDate); } public function activitiesList(): HasMany { return $this->hasMany(ClientProjectActivities::class); } private function isCreditCardSpend(ClientInvoice $invoice, ClientInvoicePaymentItem $item): bool { if ($item->is_creditcard) { return true; } return $invoice->is_credit_card && $item->billingItemType?->name === ClientInvoicePaymentSyncService::MANAGEMENT_SEARCH_NAME && (float) ($item->spending ?? 0) > 0; } private function effectiveInvoiceItems(): Collection { $invoices = $this->relationLoaded('invoices') ? $this->invoices : $this->invoices()->with('payments.items.billingItemType', 'linkedInvoice.payments.items.billingItemType')->get(); $invoices->loadMissing('payments.items.billingItemType', 'linkedInvoice.payments.items.billingItemType'); return $invoices->flatMap(function (ClientInvoice $invoice) { return collect([$invoice, $invoice->linkedInvoice]) ->filter() ->flatMap(fn (ClientInvoice $effectiveInvoice) => $effectiveInvoice->payments ->flatMap(fn ($payment) => $payment->items ->map(fn (ClientInvoicePaymentItem $item) => [ 'invoice' => $effectiveInvoice, 'item' => $item, ]))); }); } private function dateSortValue(?CarbonInterface $date): string { return $date?->toDateString() ?? '0000-00-00'; } public function customers() { return $this->hasMany(ClientCustomer::class); } }