inspiren-sem-tool/app/Http/Controllers/Api/ClientInvoiceController.php

317 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\ClientInvoice;
use App\Services\ClientLookupService;
use App\Services\ClientInvoiceApprovalService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class ClientInvoiceController extends Controller
{
public function __construct(
private ClientInvoiceApprovalService $approvalService,
private ClientLookupService $clientLookupService,
) {
}
public function pending(): JsonResponse
{
$invoices = ClientInvoice::query()
->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,
];
}
}