with('client:id,name,customer_id') ->whereNull('approved_at') ->latest('id') ->get([ 'id', 'client_id', 'pending_sql_acc_code', 'pending_client_name', 'invoice_no', 'start_date', 'end_date', 'payment_no', 'management_fee', 'management_fee_amount', 'management_fee_tax', 'media_fee', 'media_fee_amount', 'media_fee_tax', 'nett_amount', 'total_spending', 'created_at', ]); return response()->json([ 'count' => $invoices->count(), 'invoices' => $invoices->map(function (ClientInvoice $invoice) { $previousPayments = $this->previousPaymentsForInvoice($invoice); $invoiceBillingTotals = $this->invoiceBillingTotalsFromPayments($previousPayments); return [ ...$invoice->toArray(), 'requires_client' => $invoice->client_id === null, 'previous_payments' => $previousPayments, 'invoice_billing_totals' => $invoiceBillingTotals, ]; }), ]); } public function store(Request $request): JsonResponse { $validated = $request->validate([ 'client_id' => ['nullable', 'exists:clients,id'], 'sql_acc_code' => ['required_without:client_id', 'nullable', 'string'], 'client_name' => ['nullable', 'string'], 'invoice_no' => ['required', 'string'], 'linked_invoice_id' => ['nullable', 'integer'], 'is_credit_card' => ['nullable', 'boolean'], 'is_paid' => ['nullable', 'boolean'], 'payment_no' => ['nullable', 'string'], 'start_date' => ['nullable', 'date'], 'end_date' => ['nullable', 'date', 'after_or_equal:start_date'], 'management_fee' => ['required', 'numeric', 'min:0'], 'management_fee_amount' => ['nullable', 'numeric', 'min:0'], 'management_fee_tax' => ['nullable', 'numeric', 'min:0'], 'media_fee' => ['required', 'numeric', 'min:0'], 'media_fee_amount' => ['nullable', 'numeric', 'min:0'], 'media_fee_tax' => ['nullable', 'numeric', 'min:0'], 'total_spending' => ['nullable', 'numeric', 'min:0'], ]); $mediaFee = $validated['media_fee']; $taxPercent = (float) ($validated['tax_percent'] ?? 0); $nettAmount = $mediaFee / (1 + ($taxPercent / 100)); $sqlAccCode = $this->clientLookupService->normalizeSqlAccCode($validated['sql_acc_code'] ?? null); $client = ! empty($validated['client_id']) ? \App\Models\Client::find($validated['client_id']) : $this->clientLookupService->findBySqlAccCode($sqlAccCode); if (! empty($validated['linked_invoice_id'])) { $linkedInvoiceExists = $client !== null && ClientInvoice::query() ->where('id', $validated['linked_invoice_id']) ->where('client_id', $client->id) ->exists(); if (! $linkedInvoiceExists) { throw ValidationException::withMessages([ 'linked_invoice_id' => 'The linked invoice must belong to the resolved client.', ]); } } $invoice = ClientInvoice::create([ 'client_id' => $client?->id, 'pending_sql_acc_code' => $client === null ? $sqlAccCode : null, 'pending_client_name' => $client === null ? ($validated['client_name'] ?? null) : null, 'invoice_no' => $validated['invoice_no'], 'linked_invoice_id' => $validated['linked_invoice_id'] ?? null, 'is_credit_card' => (bool) ($validated['is_credit_card'] ?? false), 'is_paid' => (bool) ($validated['is_paid'] ?? false), 'approved_at' => null, 'payment_no' => $validated['payment_no'] ?? null, 'start_date' => $validated['start_date'] ?? null, 'end_date' => $validated['end_date'] ?? null, 'management_fee' => $validated['management_fee'], 'management_fee_amount' => $validated['management_fee_amount'] ?? null, 'management_fee_tax' => $validated['management_fee_tax'] ?? null, 'media_fee' => $validated['media_fee'], 'media_fee_amount' => $validated['media_fee_amount'] ?? null, 'media_fee_tax' => $validated['media_fee_tax'] ?? null, 'tax_percent' => null, 'nett_amount' => $nettAmount, 'total_spending' => $validated['total_spending'] ?? null, ]); $this->approvalService->requireApproval($invoice); return response()->json([ 'message' => 'Invoice created and marked for approval.', 'invoice' => $invoice->fresh(), ], 201); } public function approve(ClientInvoice $invoice): JsonResponse { if ($invoice->client_id === null) { return response()->json([ 'message' => 'Create and link the client before approving this invoice.', ], 409); } $invoice = $this->approvalService->approve($invoice); return response()->json([ 'message' => 'Invoice approved successfully.', 'invoice' => $invoice, ]); } private function previousPaymentsForInvoice(ClientInvoice $invoice): array { if (empty($invoice->invoice_no)) { return []; } try { $response = Http::acceptJson() ->withHeaders([ 'X-Secret' => config('app.billing_key'), 'Accept' => 'application/json', ]) ->get(config('app.billing_url').'/customer/invoices/getInvoicePaymentDetailsByInvoiceGoogle', [ 'invoice_number' => $invoice->invoice_no, ]); if (! $response->successful()) { Log::warning('Unable to fetch invoice payment details.', [ 'invoice_no' => $invoice->invoice_no, 'status' => $response->status(), ]); return []; } $records = $this->normalizePaymentRecords($response->json('data')); return collect($records) ->filter(fn (array $payment) => ($payment['payment_number'] ?? null) !== $invoice->payment_no) ->map(fn (array $payment) => $this->formatPreviousPayment($payment)) ->values() ->all(); } catch (\Throwable $e) { Log::warning('Unable to fetch invoice payment details.', [ 'invoice_no' => $invoice->invoice_no, 'message' => $e->getMessage(), ]); return []; } } private function normalizePaymentRecords(mixed $data): array { if (! is_array($data)) { return []; } if (array_is_list($data)) { return $data; } return [$data]; } private function formatPreviousPayment(array $payment): array { $items = collect($payment['items'] ?? []) ->filter(fn (mixed $paymentItem) => is_array($paymentItem)) ->values(); $paymentAmount = (float) ($payment['amount'] ?? 0); $estimatedItemsTotal = $items->sum( fn (array $paymentItem) => $this->paymentItemEstimatedTotal($paymentItem) ); $itemAmounts = $items ->map(fn (array $paymentItem) => (float) ($paymentItem['amount'] ?? 0)) ->filter(fn (float $amount) => $amount > 0) ->unique() ->values(); $usesRepeatedPaymentAmount = $items->count() > 1 && $paymentAmount > 0 && $estimatedItemsTotal > 0 && $itemAmounts->count() === 1 && abs($itemAmounts->first() - $paymentAmount) < 0.01; $totals = $items->reduce(function (array $totals, array $paymentItem) use ($usesRepeatedPaymentAmount, $paymentAmount, $estimatedItemsTotal) { $sqlAccCode = $this->paymentItemSqlAccCode($paymentItem); $estimatedTotal = $this->paymentItemEstimatedTotal($paymentItem); $exact_tax = $this->paymentItemTax($paymentItem); $taxPercent = $this->paymentTaxPercent($paymentItem) / 100 + 1; $amount = $usesRepeatedPaymentAmount ? $paymentAmount * ($estimatedTotal / $estimatedItemsTotal) : (float) ($paymentItem['amount'] ?? 0); if ($sqlAccCode === 'G03') { $totals['media_fee'] += $amount; $totals['invoice_media_fee'] += $estimatedTotal; } if ($sqlAccCode === 'GOOGLE') { $totals['management_fee'] += $amount; $totals['invoice_management_fee'] += $estimatedTotal; } return $totals; }, [ 'media_fee' => 0.0, 'management_fee' => 0.0, 'invoice_media_fee' => 0.0, 'invoice_management_fee' => 0.0, ]); return [ 'payment_number' => $payment['payment_number'] ?? null, 'pending_client_name' => $payment['company_name'] ?? null, 'status' => $payment['status'] ?? null, 'sql_created_at' => $payment['sql_created_at'] ?? null, 'amount' => $payment['amount'] ?? null, 'media_fee' => $totals['media_fee'] / 1.08, 'management_fee' => $totals['management_fee'] / 1.08, 'invoice_media_fee' => $totals['invoice_media_fee'] / 1.08, 'invoice_management_fee' => $totals['invoice_management_fee'] / 1.08, 'invoice_number' => data_get($payment, 'invoice.invoice_number'), ]; } private function paymentItemSqlAccCode(array $paymentItem): ?string { $sqlAccCode = data_get($paymentItem, 'item.item.sql_acc_code') ?? data_get($paymentItem, 'item.sql_acc_code') ?? data_get($paymentItem, 'sql_acc_code'); return is_string($sqlAccCode) ? strtoupper(trim($sqlAccCode)) : null; } private function paymentItemEstimatedTotal(array $paymentItem): float { return (float) ( data_get($paymentItem, 'item.estimated_total') ?? data_get($paymentItem, 'estimated_total') ?? 0 ); } private function paymentItemTax(array $paymentItem): float { return (float) ( data_get($paymentItem, 'exact_tax') ?? 0 ); } private function paymentTaxPercent(array $paymentItem): float { return (float) ( data_get($paymentItem, 'item.sql_acc_tax_percent') ?? 0 ); } private function invoiceBillingTotalsFromPayments(array $payments): array { $payment = $payments[0] ?? null; return [ 'media_fee' => $payment['invoice_media_fee'] ?? 0, 'management_fee' => $payment['invoice_management_fee'] ?? 0, ]; } }