$tenant->id, 'type' => 'topup', 'amount_rub' => '500.00', 'gateway_id' => $gw->id, 'gateway_code' => 'yookassa', 'gateway_payment_id' => $payId, 'status' => 'pending', 'created_at' => now(), ]); } beforeEach(function () { $this->tenant = Tenant::factory()->create(['balance_rub' => '0.00']); $legalEntity = LegalEntity::create([ 'code' => 'test_le_'.uniqid(), 'name' => 'ООО Тест', 'legal_form' => 'OOO', 'inn' => '7700000000', ]); $this->gw = PaymentGateway::create([ 'code' => 'yookassa', 'name' => 'ЮKassa', 'driver' => 'yookassa', 'legal_entity_id' => $legalEntity->id, 'config' => '', 'is_active' => true, 'accepts_methods' => ['card'], 'min_amount_rub' => '100.00', ]); }); it('зачисляет баланс при succeeded и помечает tx success', function () { $tx = seedPendingTx($this->tenant, $this->gw, 'pay_ok'); $this->mock(PaymentGatewayDriver::class, function ($m) { $m->shouldReceive('verifyPayment')->once() ->andReturn(new WebhookVerifyResult('pay_ok', 'succeeded', '500.00', 'bank_card')); }); $resp = $this->postJson('/api/webhook/payment', [ 'event' => 'payment.succeeded', 'object' => ['id' => 'pay_ok'], ]); $resp->assertOk(); $ledgerId = BalanceTransaction::where('tenant_id', $this->tenant->id) ->where('type', 'topup')->latest('id')->value('id'); expect($this->tenant->fresh()->balance_rub)->toBe('500.00') ->and($tx->fresh()->status)->toBe('success') ->and($tx->fresh()->balance_rub_after)->toBe('500.00') ->and($tx->fresh()->balance_transaction_id)->toBe($ledgerId); // provenance-связка }); it('идемпотентен — повторный webhook не зачисляет дважды', function () { seedPendingTx($this->tenant, $this->gw, 'pay_dup'); $this->mock(PaymentGatewayDriver::class, function ($m) { $m->shouldReceive('verifyPayment')->twice() ->andReturn(new WebhookVerifyResult('pay_dup', 'succeeded', '500.00', 'bank_card')); }); $payload = ['event' => 'payment.succeeded', 'object' => ['id' => 'pay_dup']]; $this->postJson('/api/webhook/payment', $payload)->assertOk(); $this->postJson('/api/webhook/payment', $payload)->assertOk(); expect($this->tenant->fresh()->balance_rub)->toBe('500.00'); // не 1000 }); it('не зачисляет если статус не succeeded', function () { $tx = seedPendingTx($this->tenant, $this->gw, 'pay_pending'); $this->mock(PaymentGatewayDriver::class, function ($m) { $m->shouldReceive('verifyPayment')->once() ->andReturn(new WebhookVerifyResult('pay_pending', 'pending', '500.00', null)); }); $this->postJson('/api/webhook/payment', [ 'event' => 'payment.waiting_for_capture', 'object' => ['id' => 'pay_pending'], ])->assertOk(); expect($this->tenant->fresh()->balance_rub)->toBe('0.00') ->and($tx->fresh()->status)->toBe('pending'); }); it('возвращает 200 на неизвестный платёж не падая', function () { $this->mock(PaymentGatewayDriver::class, function ($m) { $m->shouldReceive('verifyPayment')->never(); }); $this->postJson('/api/webhook/payment', [ 'event' => 'payment.succeeded', 'object' => ['id' => 'unknown_pay'], ])->assertOk(); });