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('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(); });