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();
}
}