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'], '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'], '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 = (float) ($validated['tax_percent'] ?? 0); $nettAmount = $mediaFee / (1 + ($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'], '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' => $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'], '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'], 'tax_percent' => ['nullable', 'numeric', 'min:0', 'max:100'], 'nett_amount' => ['nullable', 'numeric', 'min:0'], 'total_spending' => ['nullable', 'numeric', 'min:0'], ]); $managementFee = $validated['management_fee']; $mediaFee = $validated['media_fee']; $taxPercent = (float) ($validated['tax_percent'] ?? 0); $nettAmount = $validated['nett_amount'] ?? ($mediaFee / (1 + ($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, ]); if(empty($invoice->approved_at)) { $this->approvalService->approve($invoice); } 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']) ->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.'.'); } }