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/ai.naniguide.com/tests/Unit/RemoteSubscription/PlanRemoteMappingValidationTest.php
<?php

namespace Tests\Unit\RemoteSubscription;

use Tests\TestCase;
use App\Model\PlanRemoteMapping;
use App\Cashier\DTO\RemotePlanDTO;
use Mockery;

class PlanRemoteMappingValidationTest extends TestCase
{
    private function makeMappingWithPlan(
        float $localPrice,
        string $localCurrency,
        string $localFreqAmount,
        string $localFreqUnit,
        ?int $localTrialAmount,
        ?string $localTrialUnit,
        float $remotePrice,
        string $remoteCurrency,
        int $remoteIntervalCount,
        string $remoteIntervalUnit,
        ?int $remoteTrialDays,
    ): PlanRemoteMapping {
        // Create mock plan
        $currency = Mockery::mock();
        $currency->code = $localCurrency;

        $plan = Mockery::mock(\App\Model\Plan::class)->makePartial();
        $plan->price = $localPrice;
        $plan->frequency_amount = $localFreqAmount;
        $plan->frequency_unit = $localFreqUnit;
        $plan->trial_amount = $localTrialAmount;
        $plan->trial_unit = $localTrialUnit;
        $plan->name = 'Test Plan';
        $plan->shouldReceive('getAttribute')->with('currency')->andReturn($currency);
        $plan->shouldReceive('hasTrial')->andReturn($localTrialAmount && $localTrialAmount > 0);

        // Create mock gateway
        $gateway = Mockery::mock(\App\Model\PaymentGateway::class)->makePartial();
        $gateway->name = 'Test Gateway';
        $gateway->type = 'stripe-subscription';

        // Create mapping without hitting DB
        $mapping = new PlanRemoteMapping();
        $mapping->remote_plan_name = 'Remote Plan';
        $mapping->remote_price = $remotePrice;
        $mapping->remote_currency = $remoteCurrency;
        $mapping->remote_interval_count = $remoteIntervalCount;
        $mapping->remote_interval_unit = $remoteIntervalUnit;
        $mapping->remote_trial_days = $remoteTrialDays;

        // Set relationships directly
        $mapping->setRelation('plan', $plan);
        $mapping->setRelation('paymentGateway', $gateway);

        return $mapping;
    }

    public function test_perfectly_matched_mapping_has_no_mismatches()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 0,
        );

        $this->assertEmpty($mapping->getMismatches());
        $this->assertTrue($mapping->isInSync());
    }

    public function test_price_mismatch_detected()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 39.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 0,
        );

        $mismatches = $mapping->getMismatches();
        $this->assertNotEmpty($mismatches);
        $this->assertEquals('price', $mismatches[0]['field']);
        $this->assertEquals(29.99, $mismatches[0]['local']);
        $this->assertEquals(39.99, $mismatches[0]['remote']);
        $this->assertFalse($mapping->isInSync());
    }

    public function test_currency_mismatch_detected()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 29.99, remoteCurrency: 'EUR',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 0,
        );

        $mismatches = $mapping->getMismatches();
        $fields = array_column($mismatches, 'field');
        $this->assertContains('currency', $fields);
    }

    public function test_interval_count_mismatch()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 3, remoteIntervalUnit: 'month',
            remoteTrialDays: 0,
        );

        $mismatches = $mapping->getMismatches();
        $fields = array_column($mismatches, 'field');
        $this->assertContains('interval_count', $fields);
    }

    public function test_interval_unit_mismatch()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'year',
            remoteTrialDays: 0,
        );

        $mismatches = $mapping->getMismatches();
        $fields = array_column($mismatches, 'field');
        $this->assertContains('interval_unit', $fields);
    }

    public function test_trial_days_mismatch()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: 14, localTrialUnit: 'day',
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 30,
        );

        $mismatches = $mapping->getMismatches();
        $fields = array_column($mismatches, 'field');
        $this->assertContains('trial_days', $fields);

        // Find the trial mismatch
        $trial = collect($mismatches)->firstWhere('field', 'trial_days');
        $this->assertEquals(14, $trial['local']);
        $this->assertEquals(30, $trial['remote']);
    }

    public function test_trial_week_unit_converts_correctly()
    {
        // 2 weeks = 14 days → should match remote 14 days
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: 2, localTrialUnit: 'week',
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 14,
        );

        $fields = array_column($mapping->getMismatches(), 'field');
        $this->assertNotContains('trial_days', $fields);
    }

    public function test_trial_month_unit_converts_correctly()
    {
        // 1 month = 30 days → should match remote 30 days
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: 1, localTrialUnit: 'month',
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 30,
        );

        $fields = array_column($mapping->getMismatches(), 'field');
        $this->assertNotContains('trial_days', $fields);
    }

    public function test_trial_year_unit_converts_correctly()
    {
        // 1 year = 365 days → should match remote 365 days
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: 1, localTrialUnit: 'year',
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 365,
        );

        $fields = array_column($mapping->getMismatches(), 'field');
        $this->assertNotContains('trial_days', $fields);
    }

    public function test_trial_week_unit_detects_mismatch()
    {
        // 2 weeks = 14 days → should NOT match remote 7 days
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: 2, localTrialUnit: 'week',
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 7,
        );

        $mismatches = $mapping->getMismatches();
        $trial = collect($mismatches)->firstWhere('field', 'trial_days');
        $this->assertNotNull($trial);
        $this->assertEquals(14, $trial['local']);
        $this->assertEquals(7, $trial['remote']);
    }

    public function test_trial_match_when_both_zero()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: null,
        );

        $fields = array_column($mapping->getMismatches(), 'field');
        $this->assertNotContains('trial_days', $fields);
    }

    public function test_multiple_mismatches_detected_at_once()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 49.99, remoteCurrency: 'EUR',
            remoteIntervalCount: 3, remoteIntervalUnit: 'year',
            remoteTrialDays: 14,
        );

        $mismatches = $mapping->getMismatches();
        // Should catch: price, currency, interval_count, interval_unit, trial_days
        $this->assertGreaterThanOrEqual(4, count($mismatches));

        $fields = array_column($mismatches, 'field');
        $this->assertContains('price', $fields);
        $this->assertContains('currency', $fields);
        $this->assertContains('interval_count', $fields);
        $this->assertContains('interval_unit', $fields);
        $this->assertContains('trial_days', $fields);
    }

    public function test_validate_mapping_throws_on_mismatch()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 39.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 0,
        );

        $this->expectException(\Exception::class);
        $this->expectExceptionMessageMatches('/Price mismatch/');
        $mapping->validateMapping();
    }

    public function test_validate_mapping_passes_when_in_sync()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 0,
        );

        // Should not throw
        $mapping->validateMapping();
        $this->assertTrue(true);
    }

    public function test_comparison_summary()
    {
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: 14, localTrialUnit: 'day',
            remotePrice: 29.99, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 14,
        );

        $summary = $mapping->getComparisonSummary();

        $this->assertEquals('Test Plan', $summary['local_plan']);
        $this->assertEquals('Remote Plan', $summary['remote_plan']);
        $this->assertEquals('Test Gateway', $summary['gateway']);
        $this->assertTrue($summary['in_sync']);
        $this->assertEmpty($summary['mismatches']);

        $this->assertEquals(29.99, $summary['fields']['price']['local']);
        $this->assertEquals(29.99, $summary['fields']['price']['remote']);
        $this->assertEquals('USD', $summary['fields']['currency']['local']);
        $this->assertEquals('USD', $summary['fields']['currency']['remote']);
    }

    public function test_sync_from_remote_updates_all_fields()
    {
        $mapping = new PlanRemoteMapping();

        $remotePlan = new RemotePlanDTO(
            id: 'price_new',
            name: 'Updated Plan',
            price: 49.99,
            currency: 'GBP',
            intervalCount: 3,
            intervalUnit: 'month',
            status: 'active',
            trialDays: 7,
            metadata: ['key' => 'value'],
        );

        // Can't call save() without DB, so we'll test attribute assignment
        // Override save to prevent DB hit
        $mapping = Mockery::mock(PlanRemoteMapping::class)->makePartial();
        $mapping->shouldReceive('save')->once();

        $mapping->syncFromRemote($remotePlan);

        $this->assertEquals('Updated Plan', $mapping->remote_plan_name);
        $this->assertEquals(49.99, $mapping->remote_price);
        $this->assertEquals('GBP', $mapping->remote_currency);
        $this->assertEquals(3, $mapping->remote_interval_count);
        $this->assertEquals('month', $mapping->remote_interval_unit);
        $this->assertEquals(7, $mapping->remote_trial_days);
        $this->assertEquals(['key' => 'value'], $mapping->remote_metadata);
    }

    public function test_small_price_difference_within_tolerance()
    {
        // $0.009 difference should be within 0.01 tolerance
        $mapping = $this->makeMappingWithPlan(
            localPrice: 29.99, localCurrency: 'USD',
            localFreqAmount: '1', localFreqUnit: 'month',
            localTrialAmount: null, localTrialUnit: null,
            remotePrice: 29.999, remoteCurrency: 'USD',
            remoteIntervalCount: 1, remoteIntervalUnit: 'month',
            remoteTrialDays: 0,
        );

        $fields = array_column($mapping->getMismatches(), 'field');
        $this->assertNotContains('price', $fields);
    }

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }
}