HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/tests/Unit/Cashier/PaymentIntentFactoryTest.php
<?php

namespace Tests\Unit\Cashier;

use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Model\Invoice;
use App\Model\PaymentGateway;
use App\Model\PaymentIntent as PaymentIntentModel;
use App\Cashier\Factories\PaymentIntentFactory;
use App\Cashier\DTO\PaymentIntent as PaymentIntentDto;

class PaymentIntentFactoryTest extends TestCase
{
    use DatabaseTransactions;

    private function factory(): PaymentIntentFactory
    {
        return app(PaymentIntentFactory::class);
    }

    public function test_create_from_invoice_inserts_row_with_pending_status()
    {
        $invoice = Invoice::factory()->create();
        $gateway = PaymentGateway::factory()->stripe()->create();

        $countBefore = PaymentIntentModel::count();
        $dto = $this->factory()->createFromInvoice($invoice, $gateway);
        $countAfter = PaymentIntentModel::count();

        $this->assertSame($countBefore + 1, $countAfter);

        $row = PaymentIntentModel::where('uid', $dto->uid)->first();
        $this->assertNotNull($row);
        $this->assertSame(PaymentIntentModel::STATUS_PENDING, $row->status);
        $this->assertSame($invoice->id, $row->invoice_id);
        $this->assertSame($gateway->id, $row->payment_gateway_id);
    }

    public function test_create_from_invoice_returns_dto_with_matching_fields()
    {
        $invoice = Invoice::factory()->create();
        $gateway = PaymentGateway::factory()->stripe()->create();

        $dto = $this->factory()->createFromInvoice($invoice, $gateway);

        $this->assertInstanceOf(PaymentIntentDto::class, $dto);
        $this->assertSame($gateway->uid, $dto->paymentGatewayId);
        $this->assertNull($dto->subscription);
        $this->assertFalse($dto->isSubscription());
    }

    public function test_sub_spec_hydrates_subscription_in_dto_and_metadata()
    {
        $invoice = Invoice::factory()->create();
        $gateway = PaymentGateway::factory()->stripeSubscription()->create();

        $dto = $this->factory()->createFromInvoice(
            $invoice,
            $gateway,
            ['remote_plan_id' => 'price_xyz']
        );

        $this->assertTrue($dto->isSubscription());
        $this->assertSame('price_xyz', $dto->subscription->remotePlanId);

        $row = PaymentIntentModel::where('uid', $dto->uid)->first();
        $this->assertSame('price_xyz', $row->metadata['remote_plan_id']);
    }

    public function test_concurrent_create_returns_existing_intent_when_locked()
    {
        // Race scenario: user clicks Pay in 2 tabs. Without the lock both calls
        // would create parallel intents → potential double-charge. With the lock,
        // the second call sees invoice.processing_intent_id is set and reuses it.
        $invoice = Invoice::factory()->create();
        $gateway = PaymentGateway::factory()->stripe()->create();

        $dto1 = $this->factory()->createFromInvoice($invoice, $gateway);
        $dto2 = $this->factory()->createFromInvoice($invoice, $gateway);

        $this->assertSame($dto1->uid, $dto2->uid, 'Second concurrent call must reuse the in-flight intent');
        $this->assertSame(
            1,
            PaymentIntentModel::where('invoice_id', $invoice->id)->count(),
            'Lock prevents parallel intents on same invoice + gateway'
        );

        $invoice->refresh();
        $this->assertNotNull($invoice->processing_intent_id, 'Lock pointer must be set');
    }

    public function test_create_with_different_gateway_supersedes_old_intent()
    {
        // User starts with Stripe, then switches to StripeSubscription (different gateway).
        // Old intent gets cancelled, new intent is created.
        $invoice  = Invoice::factory()->create();
        $stripeGw = PaymentGateway::factory()->stripe()->create();
        $subGw    = PaymentGateway::factory()->stripeSubscription()->create();

        $dto1 = $this->factory()->createFromInvoice($invoice, $stripeGw);
        $dto2 = $this->factory()->createFromInvoice($invoice, $subGw, ['remote_plan_id' => 'price_xyz']);

        $this->assertNotSame($dto1->uid, $dto2->uid);

        $old = PaymentIntentModel::where('uid', $dto1->uid)->firstOrFail();
        $this->assertSame(PaymentIntentModel::STATUS_CANCELLED, $old->status, 'Old intent on different gateway must be cancelled');

        $invoice->refresh();
        $new = PaymentIntentModel::where('uid', $dto2->uid)->firstOrFail();
        $this->assertSame($new->id, $invoice->processing_intent_id, 'Lock pointer must point to the new intent');
    }

    public function test_create_after_terminal_intent_creates_fresh_one()
    {
        // After lock is released (intent reaches terminal), next attempt creates a new intent.
        $invoice = Invoice::factory()->create();
        $gateway = PaymentGateway::factory()->stripe()->create();

        $dto1 = $this->factory()->createFromInvoice($invoice, $gateway);

        // Simulate handler releasing the lock on terminal transition
        $first = PaymentIntentModel::where('uid', $dto1->uid)->firstOrFail();
        $first->update(['status' => PaymentIntentModel::STATUS_FAILED]);
        $invoice->update(['processing_intent_id' => null]);

        $dto2 = $this->factory()->createFromInvoice($invoice, $gateway);

        $this->assertNotSame($dto1->uid, $dto2->uid, 'After lock release, next call must create a fresh intent');
        $this->assertSame(
            2,
            PaymentIntentModel::where('invoice_id', $invoice->id)->count(),
            'Two distinct intents — one failed, one new attempt'
        );
    }
}