Files
portal/app/tests/Feature/Autopodbor/AutopodborChargeServiceTest.php
T
Дмитрий 4042890b0a feat(автоподбор): идемпотентное списание за прогон (bcmath, only-on-success)
- AutopodborChargeService::chargeForRun — DB::transaction + lockForUpdate
  на AutopodborRun (guard идемпотентности по balance_transaction_id) и Tenant;
  bcmath (bcsub/bccomp/bcmul), никаких float; throw InsufficientBalanceException
  до любых изменений баланса при нехватке средств.
- Миграция 2026_06_28_110100: расширяет CHECK constraint
  balance_transactions_type_check — добавляет 'autopodbor_charge'.
- Тест: 2 money-инварианта (идемпотентность + noop при нехватке).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:41:06 +03:00

38 lines
1.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AutopodborRun;
use App\Models\BalanceTransaction;
use App\Models\Tenant;
use App\Services\Autopodbor\AutopodborChargeService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class, \Tests\Concerns\SharesSupplierPdo::class);
it('списывает один раз и идемпотентно по run_id', function () {
$tenant = Tenant::factory()->create(['balance_rub' => '1000.00']);
DB::statement("SET LOCAL app.current_tenant_id = ".$tenant->id);
$run = AutopodborRun::create(['tenant_id' => $tenant->id, 'kind' => 'search', 'status' => 'running', 'params' => []]);
$svc = app(AutopodborChargeService::class);
$svc->chargeForRun($run, '300.00');
$svc->chargeForRun($run->fresh(), '300.00'); // повтор НЕ должен списать второй раз
expect((string) $tenant->fresh()->balance_rub)->toBe('700.00')
->and($run->fresh()->price_rub_charged)->not->toBeNull()
->and(BalanceTransaction::where('type', 'autopodbor_charge')->where('related_id', $run->id)->count())->toBe(1);
});
it('не списывает при нехватке баланса (бросает, баланс цел)', function () {
$tenant = Tenant::factory()->create(['balance_rub' => '100.00']);
DB::statement("SET LOCAL app.current_tenant_id = ".$tenant->id);
$run = AutopodborRun::create(['tenant_id' => $tenant->id, 'kind' => 'study', 'status' => 'running', 'params' => []]);
expect(fn () => app(AutopodborChargeService::class)->chargeForRun($run, '300.00'))
->toThrow(\App\Exceptions\Billing\InsufficientBalanceException::class);
expect((string) $tenant->fresh()->balance_rub)->toBe('100.00')
->and(BalanceTransaction::where('related_id', $run->id)->count())->toBe(0);
});