130 lines
5.8 KiB
PHP
130 lines
5.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Models\AdWallet;
|
||
use App\Models\BalanceTransaction;
|
||
use App\Models\LegalEntity;
|
||
use App\Models\PaymentGateway;
|
||
use App\Models\SaasTransaction;
|
||
use App\Models\Tenant;
|
||
use App\Models\User;
|
||
use App\Services\Billing\Gateway\CreatePaymentResult;
|
||
use App\Services\Billing\Gateway\PaymentGatewayDriver;
|
||
use App\Services\Billing\Gateway\WebhookVerifyResult;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Tests\Concerns\SharesSupplierPdo;
|
||
|
||
uses(DatabaseTransactions::class);
|
||
uses(SharesSupplierPdo::class);
|
||
|
||
/**
|
||
* Рекламный кошелёк, Часть A — оплата картой (ЮKassa) с credit_target='advertising'
|
||
* зачисляет ad_wallets, НЕ tenants.balance_rub (leads). Зеркалит
|
||
* AdWalletInvoiceTopupTest (счёт) и существующий PaymentWebhookTest (webhook/settle).
|
||
*/
|
||
function seedPendingCardTx(Tenant $tenant, PaymentGateway $gw, string $payId, string $creditTarget = 'leads'): SaasTransaction
|
||
{
|
||
return SaasTransaction::create([
|
||
'tenant_id' => $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');
|
||
});
|