$tenant->id, 'type' => 'topup', 'amount_rub' => '500.00', 'gateway_id' => $gw->id, 'gateway_code' => 'yookassa', 'gateway_payment_id' => $payId, 'credit_target' => $creditTarget, 'status' => 'pending', 'created_at' => now(), ]); } beforeEach(function () { $this->tenant = Tenant::factory()->create(['balance_rub' => '0.00']); $legalEntity = LegalEntity::create([ 'code' => 'test_le_adv_'.uniqid(), 'name' => 'ООО Тест', 'legal_form' => 'OOO', 'inn' => '7700000000', ]); $this->gw = PaymentGateway::create([ 'code' => 'yookassa_adv_'.uniqid(), 'name' => 'ЮKassa', 'driver' => 'yookassa', 'legal_entity_id' => $legalEntity->id, 'config' => '', 'is_active' => true, 'accepts_methods' => ['card'], 'min_amount_rub' => '100.00', ]); }); it('settle с credit_target=advertising зачисляет рекламный кошелёк, не баланс за лиды', function () { $tx = seedPendingCardTx($this->tenant, $this->gw, 'pay_adv_ok', 'advertising'); $this->mock(PaymentGatewayDriver::class, function ($m) { $m->shouldReceive('verifyPayment')->once() ->andReturn(new WebhookVerifyResult('pay_adv_ok', 'succeeded', '500.00', 'RUB', 'bank_card')); }); $resp = $this->postJson('/api/webhook/payment', [ 'event' => 'payment.succeeded', 'object' => ['id' => 'pay_adv_ok'], ]); $resp->assertOk(); $wallet = AdWallet::where('tenant_id', $this->tenant->id)->first(); expect((string) $wallet->balance_rub)->toBe('500.00') ->and($this->tenant->fresh()->balance_rub)->toBe('0.00') // основной баланс НЕ тронут ->and($tx->fresh()->status)->toBe('success'); }); it('settle с credit_target=leads (регресс) зачисляет баланс за лиды как раньше', function () { $tx = seedPendingCardTx($this->tenant, $this->gw, 'pay_leads_ok', 'leads'); $this->mock(PaymentGatewayDriver::class, function ($m) { $m->shouldReceive('verifyPayment')->once() ->andReturn(new WebhookVerifyResult('pay_leads_ok', 'succeeded', '500.00', 'RUB', 'bank_card')); }); $resp = $this->postJson('/api/webhook/payment', [ 'event' => 'payment.succeeded', 'object' => ['id' => 'pay_leads_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(AdWallet::where('tenant_id', $this->tenant->id)->exists())->toBeFalse() ->and($tx->fresh()->status)->toBe('success') ->and($tx->fresh()->balance_rub_after)->toBe('500.00') ->and($tx->fresh()->balance_transaction_id)->toBe($ledgerId); }); it('идемпотентность: повторный settle advertising не пополняет рекламный кошелёк дважды', function () { seedPendingCardTx($this->tenant, $this->gw, 'pay_adv_dup', 'advertising'); $this->mock(PaymentGatewayDriver::class, function ($m) { $m->shouldReceive('verifyPayment')->twice() ->andReturn(new WebhookVerifyResult('pay_adv_dup', 'succeeded', '500.00', 'RUB', 'bank_card')); }); $payload = ['event' => 'payment.succeeded', 'object' => ['id' => 'pay_adv_dup']]; $this->postJson('/api/webhook/payment', $payload)->assertOk(); $this->postJson('/api/webhook/payment', $payload)->assertOk(); $wallet = AdWallet::where('tenant_id', $this->tenant->id)->first(); expect((string) $wallet->balance_rub)->toBe('500.00'); // не 1000 }); it('POST /api/billing/topup с credit_target=advertising при флаге ВКЛ создаёт pending-транзакцию рекламного кошелька', function () { DB::table('system_settings')->updateOrInsert( ['key' => 'billing_yookassa_enabled'], ['value' => 'true', 'type' => 'bool', 'updated_at' => now()] ); $user = User::factory()->create(['tenant_id' => $this->tenant->id]); $this->mock(PaymentGatewayDriver::class, function ($m) { $m->shouldReceive('createPayment')->once() ->andReturn(new CreatePaymentResult('pay_adv_start', 'https://yoomoney.ru/checkout/pay_adv_start')); }); $resp = $this->actingAs($user)->postJson('/api/billing/topup', [ 'amount_rub' => 500, 'credit_target' => 'advertising', ]); $resp->assertCreated()->assertJson(['confirmation_url' => 'https://yoomoney.ru/checkout/pay_adv_start']); $tx = SaasTransaction::where('gateway_payment_id', 'pay_adv_start')->firstOrFail(); expect($tx->credit_target)->toBe('advertising') ->and($tx->status)->toBe('pending'); });