inspiren-sem-tool/tests/Feature/ClientInvoiceApprovalTest.php

327 lines
10 KiB
PHP

<?php
use App\Models\Client;
use App\Models\ClientCustomer;
use App\Models\ClientInvoice;
use App\Models\User;
use Illuminate\Support\Facades\Http;
it('auto approves invoices created through the web form', function () {
$user = User::factory()->create();
$client = Client::create([
'name' => 'Acme',
'customer_id' => '1234567890',
'status' => 'ENABLED',
'time_zone' => 'Asia/Kuala_Lumpur',
'industry' => 'Marketing',
'sql_acc_code' => 'SEM-001',
]);
$this->actingAs($user)
->post(route('client-invoices.store'), [
'client_id' => $client->id,
'customer_id' => $client->customer_id,
'invoice_no' => 'INV-1001',
'management_fee' => 120,
'media_fee' => 880,
])
->assertRedirect();
$invoice = ClientInvoice::where('invoice_no', 'INV-1001')->first();
expect($invoice)->not->toBeNull();
expect($invoice?->approved_at)->not->toBeNull();
});
it('keeps invoices created through the api pending approval', function () {
$client = Client::create([
'name' => 'Acme',
'customer_id' => '1234567891',
'status' => 'ENABLED',
'time_zone' => 'Asia/Kuala_Lumpur',
'industry' => 'Marketing',
'sql_acc_code' => 'SEM-002',
]);
$this->postJson(route('api.customer-invoices.store'), [
'client_id' => $client->id,
'invoice_no' => 'INV-2001',
'management_fee' => 120,
'media_fee' => 880,
])
->assertCreated()
->assertJsonPath('invoice.approved_at', null);
$invoice = ClientInvoice::where('invoice_no', 'INV-2001')->first();
expect($invoice)->not->toBeNull();
expect($invoice?->approved_at)->toBeNull();
});
it('links api invoices to existing clients by sql account code', function () {
$client = Client::create([
'name' => 'Acme',
'customer_id' => '1234567894',
'status' => 'ENABLED',
'time_zone' => 'Asia/Kuala_Lumpur',
'industry' => 'Marketing',
'sql_acc_code' => null,
]);
ClientCustomer::create([
'client_id' => $client->id,
'sql_acc_code' => 'SEM-005',
]);
$this->postJson(route('api.customer-invoices.store'), [
'sql_acc_code' => 'SEM-005',
'invoice_no' => 'INV-2002',
'management_fee' => 120,
'media_fee' => 880,
])
->assertCreated()
->assertJsonPath('invoice.client_id', $client->id)
->assertJsonPath('invoice.approved_at', null);
});
it('keeps api invoices pending without a client when sql account code is unknown', function () {
$this->postJson(route('api.customer-invoices.store'), [
'sql_acc_code' => 'SEM-006',
'client_name' => 'New Client',
'invoice_no' => 'INV-2003',
'management_fee' => 120,
'media_fee' => 880,
])
->assertCreated()
->assertJsonPath('invoice.client_id', null)
->assertJsonPath('invoice.pending_sql_acc_code', 'SEM-006')
->assertJsonPath('invoice.pending_client_name', 'New Client');
});
it('opens the edit form for pending invoices that still need a client', function () {
$user = User::factory()->create();
$invoice = ClientInvoice::create([
'client_id' => null,
'pending_sql_acc_code' => 'SEM-008',
'pending_client_name' => 'Pending Client',
'invoice_no' => 'INV-NEEDS-CLIENT',
'approved_at' => null,
'management_fee' => 120,
'media_fee' => 880,
]);
$this->actingAs($user)
->get(route('client-invoices.edit', $invoice))
->assertOk()
->assertInertia(fn ($page) => $page
->component('client-invoices/edit')
->where('invoice.id', $invoice->id)
->where('invoice.pending_sql_acc_code', 'SEM-008')
->where('unlinkedClients', [])
);
});
it('links a pending invoice client while saving the edit form', function () {
$user = User::factory()->create();
$client = Client::create([
'name' => 'Acme',
'customer_id' => '1234567896',
'status' => 'ENABLED',
'time_zone' => 'Asia/Kuala_Lumpur',
'industry' => 'Marketing',
'sql_acc_code' => null,
]);
$invoice = ClientInvoice::create([
'client_id' => null,
'pending_sql_acc_code' => 'SEM-009',
'pending_client_name' => 'Acme',
'invoice_no' => 'INV-LINK-EDIT',
'approved_at' => null,
'management_fee' => 120,
'media_fee' => 880,
]);
$this->actingAs($user)
->put(route('client-invoices.update', $invoice), [
'client_id' => $client->id,
'invoice_no' => 'INV-LINK-EDIT',
'management_fee' => 120,
'media_fee' => 880,
])
->assertRedirect();
$invoice->refresh();
expect($invoice->client_id)->toBe($client->id);
expect($invoice->pending_sql_acc_code)->toBeNull();
expect($invoice->approved_at)->not->toBeNull();
expect($client->refresh()->sql_acc_code)->toBeNull();
});
it('changes the linked google client from the edit form', function () {
$user = User::factory()->create();
$originalClient = Client::create([
'name' => 'Original',
'customer_id' => '1234567897',
'status' => 'ENABLED',
'time_zone' => 'Asia/Kuala_Lumpur',
'industry' => 'Marketing',
'sql_acc_code' => 'SEM-010',
]);
$newClient = Client::create([
'name' => 'Replacement',
'customer_id' => '1234567898',
'status' => 'ENABLED',
'time_zone' => 'Asia/Kuala_Lumpur',
'industry' => 'Marketing',
'sql_acc_code' => 'SEM-011',
]);
$invoice = ClientInvoice::create([
'client_id' => $originalClient->id,
'invoice_no' => 'INV-CHANGE-CLIENT',
'approved_at' => null,
'management_fee' => 120,
'media_fee' => 880,
]);
$this->actingAs($user)
->put(route('client-invoices.update', $invoice), [
'client_id' => $newClient->id,
'invoice_no' => 'INV-CHANGE-CLIENT',
'management_fee' => 120,
'media_fee' => 880,
])
->assertRedirect();
expect($invoice->refresh()->client_id)->toBe($newClient->id);
});
it('lists client invoices pending approval through the api', function () {
$client = Client::create([
'name' => 'Acme',
'customer_id' => '1234567893',
'status' => 'ENABLED',
'time_zone' => 'Asia/Kuala_Lumpur',
'industry' => 'Marketing',
'sql_acc_code' => 'SEM-004',
]);
ClientInvoice::create([
'client_id' => $client->id,
'invoice_no' => 'INV-PENDING',
'approved_at' => null,
'management_fee' => 120,
'media_fee' => 880,
]);
ClientInvoice::create([
'client_id' => $client->id,
'invoice_no' => 'INV-APPROVED',
'approved_at' => now(),
'management_fee' => 120,
'media_fee' => 880,
]);
$this->getJson(route('api.customer-invoices.pending'))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('invoices.0.invoice_no', 'INV-PENDING')
->assertJsonMissing(['invoice_no' => 'INV-APPROVED']);
});
it('splits repeated previous payment item amounts by invoice item totals', function () {
config([
'app.billing_url' => 'https://billing.test',
'app.billing_key' => 'secret',
]);
$client = Client::create([
'name' => 'Acme',
'customer_id' => '1234567895',
'status' => 'ENABLED',
'time_zone' => 'Asia/Kuala_Lumpur',
'industry' => 'Marketing',
'sql_acc_code' => 'SEM-007',
]);
ClientInvoice::create([
'client_id' => $client->id,
'invoice_no' => 'INV-REPEATED-AMOUNTS',
'payment_no' => 'PAY-CURRENT',
'approved_at' => null,
'management_fee' => 120,
'media_fee' => 880,
]);
Http::fake([
'https://billing.test/customer/invoices/getInvoicePaymentDetailsByInvoiceGoogle*' => Http::response([
'data' => [
[
'payment_number' => 'PAY-PREVIOUS',
'company_name' => 'Acme',
'status' => 'Processed',
'sql_created_at' => '2026-05-18 10:00:00',
'amount' => 1000,
'invoice' => [
'invoice_number' => 'INV-REPEATED-AMOUNTS',
],
'items' => [
[
'amount' => 1000,
'item' => [
'estimated_total' => 880,
'item' => [
'sql_acc_code' => 'G03',
],
],
],
[
'amount' => 1000,
'item' => [
'estimated_total' => 120,
'item' => [
'sql_acc_code' => 'GOOGLE',
],
],
],
],
],
],
]),
]);
$this->getJson(route('api.customer-invoices.pending'))
->assertOk()
->assertJsonPath('invoices.0.previous_payments.0.media_fee', 880.0)
->assertJsonPath('invoices.0.previous_payments.0.management_fee', 120.0)
->assertJsonPath('invoices.0.previous_payments.0.invoice_media_fee', 880.0)
->assertJsonPath('invoices.0.previous_payments.0.invoice_management_fee', 120.0);
});
it('approves a pending invoice from the web approval action', function () {
$user = User::factory()->create();
$client = Client::create([
'name' => 'Acme',
'customer_id' => '1234567892',
'status' => 'ENABLED',
'time_zone' => 'Asia/Kuala_Lumpur',
'industry' => 'Marketing',
'sql_acc_code' => 'SEM-003',
]);
$invoice = ClientInvoice::create([
'client_id' => $client->id,
'invoice_no' => 'INV-3001',
'approved_at' => null,
'management_fee' => 120,
'media_fee' => 880,
]);
$this->actingAs($user)
->patch(route('client-invoices.approve', $invoice))
->assertRedirect();
expect($invoice->refresh()->approved_at)->not->toBeNull();
});