create([ 'tenant_id' => $tenantId, 'status' => $status, 'ad_text' => 'Приходите к нам за услугой', 'ad_link' => 'https://example.test/promo', 'ord_category' => 'Размещение рекламы', 'budget_cap_rub' => '500.00', 'audience_kind' => Campaign::AUDIENCE_LIST, 'planned_count' => 2, 'estimated_cost_rub' => '315.00', 'created_by' => 1, ]); } it('списывает клиентскую смету с общего баланса один раз и идемпотентно', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = chargeCampaign($tenant->id); $svc = app(TelegramCampaignChargeService::class); $svc->charge($campaign, '315.00'); $svc->charge($campaign->fresh(), '315.00'); // повтор НЕ должен списать второй раз expect((string) $tenant->fresh()->balance_rub)->toBe('685.00') ->and(BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_CHARGE) ->where('related_type', Campaign::class) ->where('related_id', $campaign->id)->count())->toBe(1); }); it('не списывает при нехватке баланса (бросает, баланс цел)', function () { $tenant = Tenant::factory()->create(['balance_rub' => '100.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = chargeCampaign($tenant->id); expect(fn () => app(TelegramCampaignChargeService::class)->charge($campaign, '315.00')) ->toThrow(InsufficientBalanceException::class); expect((string) $tenant->fresh()->balance_rub)->toBe('100.00') ->and(BalanceTransaction::where('related_id', $campaign->id)->count())->toBe(0); }); it('нулевая смета — не списывает и не пишет проводку', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = chargeCampaign($tenant->id); app(TelegramCampaignChargeService::class)->charge($campaign, '0.00'); expect((string) $tenant->fresh()->balance_rub)->toBe('1000.00') ->and(BalanceTransaction::where('related_id', $campaign->id)->count())->toBe(0); }); it('возвращает списанную смету на общий баланс, идемпотентно', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = chargeCampaign($tenant->id); $svc = app(TelegramCampaignChargeService::class); $svc->charge($campaign, '315.00'); expect((string) $tenant->fresh()->balance_rub)->toBe('685.00'); $svc->refund($campaign->fresh()); $svc->refund($campaign->fresh()); // повтор НЕ должен вернуть второй раз expect((string) $tenant->fresh()->balance_rub)->toBe('1000.00') ->and(BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_REFUND) ->where('related_id', $campaign->id)->count())->toBe(1); }); it('после возврата можно списать снова (цикл пересдачи: charge→refund→charge)', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = chargeCampaign($tenant->id); $svc = app(TelegramCampaignChargeService::class); $svc->charge($campaign, '315.00'); // запуск: сальдо +1 $svc->refund($campaign->fresh()); // отказ модерации: сальдо 0 $svc->charge($campaign->fresh(), '315.00'); // пересдача: должно списать СНОВА, сальдо +1 expect((string) $tenant->fresh()->balance_rub)->toBe('685.00') ->and(BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_CHARGE) ->where('related_id', $campaign->id)->count())->toBe(2) ->and(BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_REFUND) ->where('related_id', $campaign->id)->count())->toBe(1); }); it('возврат без списания — ничего не делает', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1000.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $campaign = chargeCampaign($tenant->id); app(TelegramCampaignChargeService::class)->refund($campaign); expect((string) $tenant->fresh()->balance_rub)->toBe('1000.00') ->and(BalanceTransaction::where('related_id', $campaign->id)->count())->toBe(0); });