forceCreate([ 'client_id' => $client->id, 'invoice_no' => fake()->unique()->numerify('INV-####'), ]); $payment = ClientInvoicePayment::query()->create([ 'client_invoice_id' => $invoice->id, ]); return ClientInvoicePaymentItem::query()->create([ 'client_invoice_payment_id' => $payment->id, 'billing_item_types_id' => $billingItemTypeId, 'start_date' => $startDate, 'end_date' => $endDate, 'spending' => $spending, ]); } test('it updates only the latest eligible invoice payment item for each client', function () { Carbon::setTestNow('2026-06-25 10:00:00'); BillingItemType::query()->insert([ ['id' => 1, 'name' => 'Media'], ['id' => 2, 'name' => 'Management'], ]); $firstClient = Client::factory()->create([ 'customer_id' => '1111111111', 'status' => 'ENABLED', 'time_zone' => 'Asia/Kuala_Lumpur', ]); $secondClient = Client::factory()->create([ 'customer_id' => '2222222222', 'status' => 'ENABLED', 'time_zone' => 'Asia/Kuala_Lumpur', ]); $olderItem = createInvoiceItem($firstClient, 1, '2026-05-01', '2026-05-31', 10); $latestItem = createInvoiceItem($firstClient, 1, '2026-06-01', null); $otherTypeItem = createInvoiceItem($firstClient, 2, '2026-06-01', null, 20); $secondClientItem = createInvoiceItem($secondClient, 1, '2026-06-10', '2026-06-20'); $adsService = Mockery::mock(GoogleAdsService::class); $adsService->shouldReceive('listCampaigns') ->once() ->with('1111111111') ->andReturn([['id' => 101], ['id' => 102]]); $adsService->shouldReceive('listCampaignsMetricsById') ->once() ->with('1111111111', '101', '2026-06-01', '2026-06-25') ->andReturn([['actual_spend' => 12.25]]); $adsService->shouldReceive('listCampaignsMetricsById') ->once() ->with('1111111111', '102', '2026-06-01', '2026-06-25') ->andReturn([['actual_spend' => 7.75]]); $adsService->shouldReceive('listCampaigns') ->once() ->with('2222222222') ->andReturn([['id' => 201]]); $adsService->shouldReceive('listCampaignsMetricsById') ->once() ->with('2222222222', '201', '2026-06-10', '2026-06-20') ->andReturn([['actual_spend' => 5.5]]); app()->instance(GoogleAdsService::class, $adsService); $this->artisan('customer:update-latest-invoice-item-spending') ->expectsOutputToContain('Done. 2 calculated, 0 skipped, 0 failed.') ->assertSuccessful(); expect((float) $olderItem->fresh()->spending)->toBe(10.0) ->and((float) $latestItem->fresh()->spending)->toBe(20.0) ->and((float) $otherTypeItem->fresh()->spending)->toBe(20.0) ->and((float) $secondClientItem->fresh()->spending)->toBe(5.5); }); test('dry run calculates spending without updating the item', function () { BillingItemType::query()->insert([ 'id' => 1, 'name' => 'Media', ]); $client = Client::factory()->create([ 'customer_id' => '3333333333', 'status' => 'ENABLED', 'time_zone' => 'Asia/Kuala_Lumpur', ]); $item = createInvoiceItem($client, 1, '2026-06-01', '2026-06-15', 8); $adsService = Mockery::mock(GoogleAdsService::class); $adsService->shouldReceive('listCampaigns') ->once() ->with('3333333333') ->andReturn([]); app()->instance(GoogleAdsService::class, $adsService); $this->artisan('customer:update-latest-invoice-item-spending', ['--dry-run' => true]) ->assertSuccessful(); expect((float) $item->fresh()->spending)->toBe(8.0); });