370 lines
14 KiB
PHP
370 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Client;
|
|
use App\Models\ClientCustomer;
|
|
use App\Models\ClientInvoice;
|
|
use App\Models\ClientUserAssignation;
|
|
use App\Services\ClientInvoiceApprovalService;
|
|
use App\Services\ClientLookupService;
|
|
use App\Services\UserHierarchyService;
|
|
use Illuminate\Http\Request;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
class ClientInvoiceController extends Controller
|
|
{
|
|
public function __construct(
|
|
private ClientInvoiceApprovalService $approvalService,
|
|
private UserHierarchyService $hierarchyService,
|
|
private ClientLookupService $clientLookupService,
|
|
) {
|
|
}
|
|
|
|
public function create(Request $request): Response
|
|
{
|
|
$clientId = $request->query('client');
|
|
$customerId = $request->query('customer_id');
|
|
|
|
if (! $clientId || ! $customerId) {
|
|
abort(404);
|
|
}
|
|
|
|
$client = Client::findOrFail($clientId);
|
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
|
|
|
|
$availableInvoices = ClientInvoice::query()
|
|
->where('client_id', $clientId)
|
|
->orderBy('invoice_no')
|
|
->get(['id', 'invoice_no', 'linked_invoice_id']);
|
|
|
|
return Inertia::render('client-invoices/create', [
|
|
'clientId' => $clientId,
|
|
'customerId' => $customerId,
|
|
'availableInvoices' => $availableInvoices,
|
|
]);
|
|
}
|
|
|
|
public function edit(ClientInvoice $invoice): Response
|
|
{
|
|
abort_if($invoice->client === null, 409, 'Create the client before editing this invoice.');
|
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
|
|
|
$availableInvoices = ClientInvoice::query()
|
|
->where('client_id', $invoice->client_id)
|
|
->where('id', '!=', $invoice->id)
|
|
->orderBy('invoice_no')
|
|
->get(['id', 'invoice_no', 'linked_invoice_id']);
|
|
|
|
return Inertia::render('client-invoices/edit', [
|
|
'invoice' => $invoice->load('client'),
|
|
'availableInvoices' => $availableInvoices,
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'client_id' => ['required', 'exists:clients,id'],
|
|
'customer_id' => ['required', 'string'],
|
|
'invoice_no' => ['required', 'string'],
|
|
'linked_invoice_id' => [
|
|
'nullable',
|
|
'integer',
|
|
Rule::exists('client_invoices', 'id')->where(function ($query) use ($request) {
|
|
return $query->where('client_id', $request->integer('client_id'));
|
|
}),
|
|
],
|
|
'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'],
|
|
'media_fee' => ['required', 'numeric', 'min:0'],
|
|
'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
|
'total_spending' => ['nullable', 'numeric', 'min:0'],
|
|
]);
|
|
|
|
$client = Client::findOrFail($validated['client_id']);
|
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
|
|
|
|
$mediaFee = $validated['media_fee'];
|
|
$taxPercent = $validated['tax_percent'] ?? 0;
|
|
$nettAmount = $mediaFee - ($mediaFee * ($taxPercent / 100));
|
|
|
|
$invoice = ClientInvoice::create([
|
|
'client_id' => $validated['client_id'],
|
|
'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'],
|
|
'media_fee' => $validated['media_fee'],
|
|
'tax_percent' => $taxPercent,
|
|
'nett_amount' => $nettAmount,
|
|
'total_spending' => $validated['total_spending'] ?? null,
|
|
]);
|
|
|
|
$this->approvalService->approve($invoice);
|
|
|
|
return redirect()
|
|
->route('google-ads.accounts.show', ['id' => $validated['customer_id']])
|
|
->with('message-info', 'Invoice created successfully.');
|
|
}
|
|
|
|
public function update(Request $request, ClientInvoice $invoice)
|
|
{
|
|
abort_if($invoice->client === null, 409, 'Create the client before updating this invoice.');
|
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
|
|
|
$validated = $request->validate([
|
|
'invoice_no' => ['required', 'string'],
|
|
'linked_invoice_id' => [
|
|
'nullable',
|
|
'integer',
|
|
Rule::exists('client_invoices', 'id')->where(function ($query) use ($invoice) {
|
|
return $query
|
|
->where('client_id', $invoice->client_id)
|
|
->where('id', '!=', $invoice->id);
|
|
}),
|
|
],
|
|
'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'],
|
|
'amount' => ['required', 'numeric', 'min:0'],
|
|
'management_fee' => ['required', 'numeric', 'min:0'],
|
|
'media_fee' => ['required', 'numeric', 'min:0'],
|
|
'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
|
'total_spending' => ['nullable', 'numeric', 'min:0'],
|
|
]);
|
|
|
|
$managementFee = $validated['management_fee'];
|
|
$mediaFee = $validated['media_fee'];
|
|
$taxPercent = $validated['tax_percent'] ?? 0;
|
|
$nettAmount = $mediaFee - ($mediaFee * ($taxPercent / 100));
|
|
|
|
$invoice->update([
|
|
'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),
|
|
'payment_no' => $validated['payment_no'] ?? null,
|
|
'start_date' => $validated['start_date'] ?? null,
|
|
'end_date' => $validated['end_date'] ?? null,
|
|
'amount' => $validated['amount'],
|
|
'management_fee' => $managementFee,
|
|
'media_fee' => $validated['media_fee'],
|
|
'tax_percent' => $taxPercent,
|
|
'nett_amount' => $nettAmount,
|
|
'total_spending' => $validated['total_spending'] ?? null,
|
|
]);
|
|
|
|
return redirect()
|
|
->route('google-ads.accounts.show', ['id' => $invoice->client->customer_id])
|
|
->with('message-info', 'Invoice updated successfully.');
|
|
}
|
|
|
|
public function approve(ClientInvoice $invoice)
|
|
{
|
|
abort_if($invoice->client === null, 409, 'Create the client before approving this invoice.');
|
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
|
|
|
$this->approvalService->approve($invoice);
|
|
|
|
return redirect()
|
|
->back()
|
|
->with('message-info', 'Invoice approved successfully.');
|
|
}
|
|
|
|
public function destroy(ClientInvoice $invoice)
|
|
{
|
|
if ($invoice->client !== null) {
|
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
|
}
|
|
|
|
$invoice->delete();
|
|
|
|
return redirect()
|
|
->back()
|
|
->with('message-info', 'Invoice deleted successfully.');
|
|
}
|
|
|
|
public function getPdfInvoice($id)
|
|
{
|
|
$invoice = ClientInvoice::where('id', $id)->first();
|
|
if (! empty($invoice) && ! empty($invoice->invoice_no)) {
|
|
if ($invoice->client !== null) {
|
|
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $invoice->client), 403);
|
|
}
|
|
|
|
$payload = [
|
|
'audience' => 'SEM',
|
|
'invoice_numbers' => [$invoice->invoice_no],
|
|
];
|
|
$invoiceResponse = Http::acceptJson()
|
|
->withHeaders([
|
|
'X-Secret' => config('app.billing_key'),
|
|
'Accept' => 'application/json',
|
|
])
|
|
->get(config('app.billing_url').'/customer/invoices/', $payload);
|
|
|
|
if ($invoiceResponse->successful()) {
|
|
$invoiceDetails = json_decode($invoiceResponse->body());
|
|
$response = Http::withHeaders([
|
|
'X-Secret' => config('app.billing_key'),
|
|
'Accept' => 'application/json',
|
|
])->get($invoiceDetails->data[0]->pdf_url);
|
|
if ($response->successful()) {
|
|
return response()->stream(
|
|
function () use ($response) {
|
|
echo $response->body();
|
|
},
|
|
200,
|
|
[
|
|
'Content-Type' => 'application/pdf',
|
|
]
|
|
);
|
|
} else {
|
|
$response->throw();
|
|
}
|
|
} else {
|
|
$invoiceResponse->throw();
|
|
}
|
|
}
|
|
}
|
|
|
|
public function createClient(ClientInvoice $invoice): Response|\Illuminate\Http\RedirectResponse
|
|
{
|
|
$existingClient = $invoice->client
|
|
?? $this->clientLookupService->findBySqlAccCode($invoice->pending_sql_acc_code);
|
|
|
|
if ($existingClient !== null) {
|
|
$invoice->update([
|
|
'client_id' => $existingClient->id,
|
|
'pending_sql_acc_code' => null,
|
|
'pending_client_name' => null,
|
|
]);
|
|
|
|
return redirect()
|
|
->route('client-invoices.edit', $invoice)
|
|
->with('message-info', 'Invoice '.$invoice->invoice_no.' has been linked to '.$existingClient->name.'.');
|
|
}
|
|
|
|
$unlinkedClients = Client::query()
|
|
->where(function ($query) {
|
|
$query->whereNull('sql_acc_code')
|
|
->orWhere('sql_acc_code', '');
|
|
})
|
|
->whereDoesntHave('customers', function ($query) {
|
|
$query->whereNotNull('sql_acc_code')
|
|
->where('sql_acc_code', '!=', '');
|
|
})
|
|
->orderBy('name')
|
|
->get(['id', 'name', 'customer_id', 'status', 'time_zone'])
|
|
->map(fn (Client $client) => [
|
|
'value' => (string) $client->id,
|
|
'label' => trim($client->name.' ('.$client->customer_id.')'),
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
return Inertia::render('client-invoices/create-client', [
|
|
'invoice' => $invoice->load('client'),
|
|
'existingClient' => null,
|
|
'unlinkedClients' => $unlinkedClients,
|
|
]);
|
|
}
|
|
|
|
public function storeClient(Request $request, ClientInvoice $invoice)
|
|
{
|
|
$validated = $request->validate([
|
|
'client_id' => [
|
|
'required',
|
|
Rule::exists('clients', 'id')->where(function ($query) {
|
|
$query
|
|
->where(function ($query) {
|
|
$query->whereNull('sql_acc_code')
|
|
->orWhere('sql_acc_code', '');
|
|
});
|
|
}),
|
|
],
|
|
'sql_acc_code' => ['required', 'string'],
|
|
]);
|
|
|
|
$sqlAccCode = $this->clientLookupService->normalizeSqlAccCode($validated['sql_acc_code']);
|
|
$existingClient = $this->clientLookupService->findBySqlAccCode($sqlAccCode);
|
|
|
|
if ($existingClient !== null && $existingClient->id !== (int) $validated['client_id']) {
|
|
return redirect()
|
|
->back()
|
|
->withInput()
|
|
->withErrors(['sql_acc_code' => 'This SQL account code is already linked to another client.']);
|
|
}
|
|
|
|
$selectedClientIsLinked = Client::query()
|
|
->where('id', $validated['client_id'])
|
|
->whereHas('customers', function ($query) {
|
|
$query->whereNotNull('sql_acc_code')
|
|
->where('sql_acc_code', '!=', '');
|
|
})
|
|
->exists();
|
|
|
|
if ($selectedClientIsLinked) {
|
|
return redirect()
|
|
->back()
|
|
->withInput()
|
|
->withErrors(['client_id' => 'Select a client that does not already have an SQL account code.']);
|
|
}
|
|
|
|
$client = DB::transaction(function () use ($validated, $sqlAccCode, $invoice) {
|
|
$client = Client::findOrFail($validated['client_id']);
|
|
|
|
$client->update([
|
|
'sql_acc_code' => $sqlAccCode,
|
|
]);
|
|
|
|
ClientCustomer::updateOrCreate(
|
|
[
|
|
'client_id' => $client->id,
|
|
'sql_acc_code' => $sqlAccCode,
|
|
],
|
|
[]
|
|
);
|
|
|
|
ClientUserAssignation::updateOrCreate(
|
|
[
|
|
'client_id' => $client->id,
|
|
'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON,
|
|
],
|
|
[
|
|
'user_id' => Auth::id(),
|
|
]
|
|
);
|
|
|
|
$invoice->update([
|
|
'client_id' => $client->id,
|
|
'pending_sql_acc_code' => null,
|
|
'pending_client_name' => null,
|
|
]);
|
|
|
|
return $client;
|
|
});
|
|
|
|
return redirect()
|
|
->route('client-invoices.edit', $invoice)
|
|
->with('message-info', 'Client '.$client->name.' has been linked to invoice '.$invoice->invoice_no.'.');
|
|
}
|
|
}
|