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 eligible invoice payment items ending today or later through today', function () { Carbon::setTestNow('2026-06-25 10:00:00'); BillingItemType::query()->insert([ ['id' => 1, 'name' => 'Media'], ['id' => 2, 'name' => 'Management'], ]); $client = Client::factory()->create([ 'customer_id' => '1111111111', 'status' => 'ENABLED', 'time_zone' => 'Asia/Kuala_Lumpur', ]); $expiredItem = createCurrentSpendingInvoiceItem($client, 1, '2026-06-01', '2026-06-24', 10); $todayItem = createCurrentSpendingInvoiceItem($client, 1, '2026-06-01', '2026-06-25'); $futureEndItem = createCurrentSpendingInvoiceItem($client, 1, '2026-06-10', '2026-06-30'); $futureStartItem = createCurrentSpendingInvoiceItem($client, 1, '2026-06-26', '2026-06-30', 30); $openEndedItem = createCurrentSpendingInvoiceItem($client, 1, '2026-06-01', null, 40); $otherTypeItem = createCurrentSpendingInvoiceItem($client, 2, '2026-06-01', '2026-06-30', 50); $adsService = Mockery::mock(GoogleAdsService::class); $adsService->shouldReceive('listCampaigns') ->twice() ->with('1111111111') ->andReturn([['id' => 101]]); $adsService->shouldReceive('listCampaignsMetricsById') ->once() ->with('1111111111', '101', '2026-06-01', '2026-06-25') ->andReturn([['actual_spend' => 12.25]]); $adsService->shouldReceive('listCampaignsMetricsById') ->once() ->with('1111111111', '101', '2026-06-10', '2026-06-25') ->andReturn([['actual_spend' => 7.75]]); app()->instance(GoogleAdsService::class, $adsService); $this->artisan('customer:update-current-invoice-item-spending') ->expectsOutputToContain('Done. 2 calculated, 1 skipped, 0 failed.') ->assertSuccessful(); expect((float) $expiredItem->fresh()->spending)->toBe(10.0) ->and((float) $todayItem->fresh()->spending)->toBe(12.25) ->and((float) $futureEndItem->fresh()->spending)->toBe(7.75) ->and((float) $futureStartItem->fresh()->spending)->toBe(30.0) ->and((float) $openEndedItem->fresh()->spending)->toBe(40.0) ->and((float) $otherTypeItem->fresh()->spending)->toBe(50.0); }); test('dry run calculates current spending without updating the item', function () { Carbon::setTestNow('2026-06-25 10:00:00'); BillingItemType::query()->insert([ 'id' => 1, 'name' => 'Media', ]); $client = Client::factory()->create([ 'customer_id' => '3333333333', 'status' => 'ENABLED', 'time_zone' => 'Asia/Kuala_Lumpur', ]); $item = createCurrentSpendingInvoiceItem($client, 1, '2026-06-01', '2026-06-30', 8); $adsService = Mockery::mock(GoogleAdsService::class); $adsService->shouldReceive('listCampaigns') ->once() ->with('3333333333') ->andReturn([]); app()->instance(GoogleAdsService::class, $adsService); $this->artisan('customer:update-current-invoice-item-spending', ['--dry-run' => true]) ->assertSuccessful(); expect((float) $item->fresh()->spending)->toBe(8.0); });