2026-01-27 00:24:22 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
2026-01-27 16:23:12 +00:00
|
|
|
use Core\Mod\Commerce\Exceptions\PauseLimitExceededException;
|
|
|
|
|
use Core\Mod\Commerce\Models\Subscription;
|
|
|
|
|
use Core\Mod\Commerce\Services\ProrationResult;
|
|
|
|
|
use Core\Mod\Commerce\Services\SubscriptionService;
|
2026-01-27 00:24:22 +00:00
|
|
|
use Core\Mod\Tenant\Models\Feature;
|
|
|
|
|
use Core\Mod\Tenant\Models\Package;
|
|
|
|
|
use Core\Mod\Tenant\Models\User;
|
|
|
|
|
use Core\Mod\Tenant\Models\Workspace;
|
|
|
|
|
use Core\Mod\Tenant\Models\WorkspacePackage;
|
|
|
|
|
|
|
|
|
|
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
|
|
|
|
|
|
|
|
|
beforeEach(function () {
|
|
|
|
|
Cache::flush();
|
|
|
|
|
|
|
|
|
|
$this->user = User::factory()->create();
|
|
|
|
|
$this->workspace = Workspace::factory()->create();
|
|
|
|
|
$this->workspace->users()->attach($this->user->id, [
|
|
|
|
|
'role' => 'owner',
|
|
|
|
|
'is_default' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Use existing seeded feature (or create test-specific one)
|
|
|
|
|
$this->aiCreditsFeature = Feature::firstOrCreate(
|
|
|
|
|
['code' => 'ai.credits'],
|
|
|
|
|
[
|
|
|
|
|
'name' => 'AI Credits',
|
|
|
|
|
'category' => 'ai',
|
|
|
|
|
'type' => Feature::TYPE_LIMIT,
|
|
|
|
|
'reset_type' => Feature::RESET_MONTHLY,
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Use existing seeded packages
|
|
|
|
|
$this->creatorPackage = Package::where('code', 'creator')->firstOrFail();
|
|
|
|
|
$this->agencyPackage = Package::where('code', 'agency')->firstOrFail();
|
|
|
|
|
$this->enterprisePackage = Package::where('code', 'enterprise')->firstOrFail();
|
|
|
|
|
|
|
|
|
|
// Ensure packages have expected prices for this test
|
|
|
|
|
$this->creatorPackage->update(['monthly_price' => 19.00, 'yearly_price' => 190.00]);
|
|
|
|
|
$this->agencyPackage->update(['monthly_price' => 49.00, 'yearly_price' => 490.00]);
|
|
|
|
|
$this->enterprisePackage->update(['monthly_price' => 99.00, 'yearly_price' => 990.00]);
|
|
|
|
|
|
|
|
|
|
// Attach features if not already attached
|
|
|
|
|
if (! $this->creatorPackage->features()->where('feature_id', $this->aiCreditsFeature->id)->exists()) {
|
|
|
|
|
$this->creatorPackage->features()->attach($this->aiCreditsFeature->id, ['limit_value' => 100]);
|
|
|
|
|
}
|
|
|
|
|
if (! $this->agencyPackage->features()->where('feature_id', $this->aiCreditsFeature->id)->exists()) {
|
|
|
|
|
$this->agencyPackage->features()->attach($this->aiCreditsFeature->id, ['limit_value' => 500]);
|
|
|
|
|
}
|
|
|
|
|
if (! $this->enterprisePackage->features()->where('feature_id', $this->aiCreditsFeature->id)->exists()) {
|
|
|
|
|
$this->enterprisePackage->features()->attach($this->aiCreditsFeature->id, ['limit_value' => null]); // Unlimited
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->service = app(SubscriptionService::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('SubscriptionService', function () {
|
|
|
|
|
describe('create() method', function () {
|
|
|
|
|
it('creates a monthly subscription', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage, 'monthly');
|
|
|
|
|
|
|
|
|
|
expect($subscription)->toBeInstanceOf(Subscription::class)
|
|
|
|
|
->and($subscription->workspace_id)->toBe($this->workspace->id)
|
|
|
|
|
->and($subscription->workspace_package_id)->toBe($workspacePackage->id)
|
|
|
|
|
->and($subscription->status)->toBe('active')
|
|
|
|
|
->and($subscription->billing_cycle)->toBe('monthly')
|
|
|
|
|
->and((int) $subscription->current_period_start->diffInDays($subscription->current_period_end))->toBe(30);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('creates a yearly subscription', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage, 'yearly');
|
|
|
|
|
|
|
|
|
|
expect($subscription->billing_cycle)->toBe('yearly')
|
|
|
|
|
->and((int) $subscription->current_period_start->diffInDays($subscription->current_period_end))->toBe(365);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('cancel() method', function () {
|
|
|
|
|
it('cancels a subscription', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
$subscription = $this->service->cancel($subscription, 'Too expensive');
|
|
|
|
|
|
|
|
|
|
expect($subscription->cancelled_at)->not->toBeNull()
|
|
|
|
|
->and($subscription->cancellation_reason)->toBe('Too expensive');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('resume() method', function () {
|
|
|
|
|
it('resumes a cancelled subscription within billing period', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
$this->service->cancel($subscription, 'Changed mind');
|
|
|
|
|
$subscription = $this->service->resume($subscription);
|
|
|
|
|
|
|
|
|
|
expect($subscription->cancelled_at)->toBeNull()
|
|
|
|
|
->and($subscription->cancellation_reason)->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not resume if period has ended', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
$subscription->update(['current_period_end' => now()->subDay()]);
|
|
|
|
|
$this->service->cancel($subscription);
|
|
|
|
|
$subscription = $this->service->resume($subscription);
|
|
|
|
|
|
|
|
|
|
// Should still be cancelled
|
|
|
|
|
expect($subscription->cancelled_at)->not->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('renew() method', function () {
|
|
|
|
|
it('renews a subscription for another period', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
$originalEnd = $subscription->current_period_end;
|
|
|
|
|
|
|
|
|
|
// Move time forward
|
|
|
|
|
Carbon::setTestNow($originalEnd);
|
|
|
|
|
$subscription = $this->service->renew($subscription);
|
|
|
|
|
|
|
|
|
|
expect($subscription->current_period_start->toDateString())->toBe($originalEnd->toDateString())
|
|
|
|
|
->and((int) $subscription->current_period_start->diffInDays($subscription->current_period_end))->toBe(30);
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow(); // Reset
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('clears cancellation when renewing', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
$subscription->update([
|
|
|
|
|
'cancelled_at' => now(),
|
|
|
|
|
'cancellation_reason' => 'Test',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->renew($subscription);
|
|
|
|
|
|
|
|
|
|
expect($subscription->cancelled_at)->toBeNull()
|
|
|
|
|
->and($subscription->cancellation_reason)->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('expire() method', function () {
|
|
|
|
|
it('expires a subscription immediately', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
$subscription = $this->service->expire($subscription);
|
|
|
|
|
|
|
|
|
|
expect($subscription->status)->toBe('expired')
|
|
|
|
|
->and($subscription->ended_at)->not->toBeNull();
|
|
|
|
|
|
|
|
|
|
$workspacePackage->refresh();
|
|
|
|
|
expect($workspacePackage->status)->toBe('expired');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('pause() and unpause() methods', function () {
|
|
|
|
|
it('pauses a subscription', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
$subscription = $this->service->pause($subscription);
|
|
|
|
|
|
|
|
|
|
expect($subscription->status)->toBe('paused')
|
|
|
|
|
->and($subscription->paused_at)->not->toBeNull()
|
|
|
|
|
->and($subscription->pause_count)->toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('increments pause count on each pause', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
|
|
|
|
|
// First pause
|
|
|
|
|
$subscription = $this->service->pause($subscription);
|
|
|
|
|
expect($subscription->pause_count)->toBe(1);
|
|
|
|
|
|
|
|
|
|
// Unpause
|
|
|
|
|
$subscription = $this->service->unpause($subscription);
|
|
|
|
|
|
|
|
|
|
// Second pause
|
|
|
|
|
$subscription = $this->service->pause($subscription);
|
|
|
|
|
expect($subscription->pause_count)->toBe(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws exception when pause limit is exceeded', function () {
|
|
|
|
|
config(['commerce.subscriptions.max_pause_cycles' => 2]);
|
|
|
|
|
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
|
|
|
|
|
// First pause
|
|
|
|
|
$subscription = $this->service->pause($subscription);
|
|
|
|
|
$subscription = $this->service->unpause($subscription);
|
|
|
|
|
|
|
|
|
|
// Second pause (at limit)
|
|
|
|
|
$subscription = $this->service->pause($subscription);
|
|
|
|
|
$subscription = $this->service->unpause($subscription);
|
|
|
|
|
|
|
|
|
|
// Third pause should throw
|
|
|
|
|
expect(fn () => $this->service->pause($subscription))
|
|
|
|
|
->toThrow(PauseLimitExceededException::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('allows forced pause even when limit exceeded', function () {
|
|
|
|
|
config(['commerce.subscriptions.max_pause_cycles' => 1]);
|
|
|
|
|
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
|
|
|
|
|
// First pause (uses the limit)
|
|
|
|
|
$subscription = $this->service->pause($subscription);
|
|
|
|
|
$subscription = $this->service->unpause($subscription);
|
|
|
|
|
|
|
|
|
|
// Force pause should work even when limit exceeded
|
|
|
|
|
$subscription = $this->service->pause($subscription, force: true);
|
|
|
|
|
|
|
|
|
|
expect($subscription->status)->toBe('paused')
|
|
|
|
|
->and($subscription->pause_count)->toBe(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('reports canPause correctly', function () {
|
|
|
|
|
config(['commerce.subscriptions.max_pause_cycles' => 2]);
|
|
|
|
|
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
|
|
|
|
|
expect($subscription->canPause())->toBeTrue()
|
|
|
|
|
->and($subscription->remainingPauseCycles())->toBe(2);
|
|
|
|
|
|
|
|
|
|
// First pause
|
|
|
|
|
$subscription = $this->service->pause($subscription);
|
|
|
|
|
$subscription = $this->service->unpause($subscription);
|
|
|
|
|
|
|
|
|
|
expect($subscription->canPause())->toBeTrue()
|
|
|
|
|
->and($subscription->remainingPauseCycles())->toBe(1);
|
|
|
|
|
|
|
|
|
|
// Second pause
|
|
|
|
|
$subscription = $this->service->pause($subscription);
|
|
|
|
|
$subscription = $this->service->unpause($subscription);
|
|
|
|
|
|
|
|
|
|
expect($subscription->canPause())->toBeFalse()
|
|
|
|
|
->and($subscription->remainingPauseCycles())->toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('unpauses a subscription', function () {
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->create($workspacePackage);
|
|
|
|
|
$this->service->pause($subscription);
|
|
|
|
|
$subscription = $this->service->unpause($subscription);
|
|
|
|
|
|
|
|
|
|
expect($subscription->status)->toBe('active')
|
|
|
|
|
->and($subscription->paused_at)->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Proration calculations', function () {
|
|
|
|
|
beforeEach(function () {
|
|
|
|
|
// Freeze time for predictable day calculations
|
|
|
|
|
Carbon::setTestNow(Carbon::now()->startOfDay()->addHours(12));
|
|
|
|
|
|
|
|
|
|
$workspacePackage = WorkspacePackage::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'package_id' => $this->creatorPackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->subscription = Subscription::create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'workspace_package_id' => $workspacePackage->id,
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
'gateway' => 'btcpay',
|
|
|
|
|
'billing_cycle' => 'monthly',
|
|
|
|
|
'current_period_start' => now()->subDays(15),
|
|
|
|
|
'current_period_end' => now()->addDays(15),
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(function () {
|
|
|
|
|
Carbon::setTestNow(); // Reset frozen time
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('calculateProration() method', function () {
|
|
|
|
|
it('calculates proration for upgrade mid-cycle', function () {
|
|
|
|
|
$proration = $this->service->calculateProration(
|
|
|
|
|
$this->subscription,
|
|
|
|
|
$this->creatorPackage, // £19/month
|
|
|
|
|
$this->agencyPackage, // £49/month
|
|
|
|
|
'monthly'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect($proration)->toBeInstanceOf(ProrationResult::class)
|
|
|
|
|
->and($proration->currentPlanPrice)->toBe(19.00)
|
|
|
|
|
->and($proration->newPlanPrice)->toBe(49.00)
|
|
|
|
|
->and($proration->isUpgrade())->toBeTrue()
|
|
|
|
|
->and($proration->requiresPayment())->toBeTrue();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calculates proration for downgrade mid-cycle', function () {
|
|
|
|
|
// Start with agency package
|
|
|
|
|
$this->subscription->workspacePackage->update(['package_id' => $this->agencyPackage->id]);
|
|
|
|
|
|
|
|
|
|
$proration = $this->service->calculateProration(
|
|
|
|
|
$this->subscription,
|
|
|
|
|
$this->agencyPackage, // £49/month
|
|
|
|
|
$this->creatorPackage, // £19/month
|
|
|
|
|
'monthly'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect($proration->isDowngrade())->toBeTrue()
|
|
|
|
|
->and($proration->netAmount)->toBeLessThan(0)
|
|
|
|
|
->and($proration->getCreditBalance())->toBeGreaterThan(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calculates days remaining correctly', function () {
|
|
|
|
|
$proration = $this->service->calculateProration(
|
|
|
|
|
$this->subscription,
|
|
|
|
|
$this->creatorPackage,
|
|
|
|
|
$this->agencyPackage,
|
|
|
|
|
'monthly'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect($proration->daysRemaining)->toBe(15)
|
|
|
|
|
->and($proration->totalPeriodDays)->toBe(30);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calculates credit amount based on unused time', function () {
|
|
|
|
|
$proration = $this->service->calculateProration(
|
|
|
|
|
$this->subscription,
|
|
|
|
|
$this->creatorPackage,
|
|
|
|
|
$this->agencyPackage,
|
|
|
|
|
'monthly'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 15 days remaining out of 30 = 50% unused
|
|
|
|
|
// Credit = £19 * 0.5 = £9.50
|
|
|
|
|
expect($proration->creditAmount)->toBe(9.50)
|
|
|
|
|
->and($proration->usedPercentage)->toBe(0.5);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calculates prorated new plan cost', function () {
|
|
|
|
|
$proration = $this->service->calculateProration(
|
|
|
|
|
$this->subscription,
|
|
|
|
|
$this->creatorPackage,
|
|
|
|
|
$this->agencyPackage,
|
|
|
|
|
'monthly'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 15 days remaining out of 30 = 50%
|
|
|
|
|
// Prorated new plan = £49 * 0.5 = £24.50
|
|
|
|
|
expect($proration->proratedNewPlanCost)->toBe(24.50);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calculates net amount correctly', function () {
|
|
|
|
|
$proration = $this->service->calculateProration(
|
|
|
|
|
$this->subscription,
|
|
|
|
|
$this->creatorPackage,
|
|
|
|
|
$this->agencyPackage,
|
|
|
|
|
'monthly'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Net = Prorated new - Credit = £24.50 - £9.50 = £15.00
|
|
|
|
|
expect($proration->netAmount)->toBe(15.00)
|
|
|
|
|
->and($proration->getAmountDue())->toBe(15.00);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('previewPlanChange() method', function () {
|
|
|
|
|
it('returns proration preview without making changes', function () {
|
|
|
|
|
$proration = $this->service->previewPlanChange(
|
|
|
|
|
$this->subscription,
|
|
|
|
|
$this->agencyPackage
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect($proration)->toBeInstanceOf(ProrationResult::class)
|
|
|
|
|
->and($proration->isUpgrade())->toBeTrue();
|
|
|
|
|
|
|
|
|
|
// Verify no changes were made
|
|
|
|
|
$this->subscription->refresh();
|
|
|
|
|
expect($this->subscription->workspacePackage->package->code)->toBe('creator');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws exception if subscription has no current package', function () {
|
|
|
|
|
// Mock the workspacePackage to have null package
|
|
|
|
|
$this->subscription->workspacePackage->setRelation('package', null);
|
|
|
|
|
|
|
|
|
|
expect(fn () => $this->service->previewPlanChange($this->subscription, $this->agencyPackage))
|
|
|
|
|
->toThrow(\InvalidArgumentException::class, 'no current package');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('scheduled plan changes', function () {
|
|
|
|
|
it('schedules plan change for period end', function () {
|
|
|
|
|
$result = $this->service->changePlan(
|
|
|
|
|
$this->subscription,
|
|
|
|
|
$this->agencyPackage,
|
|
|
|
|
prorate: false,
|
|
|
|
|
immediate: false
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect($result['immediate'])->toBeFalse()
|
|
|
|
|
->and($this->service->hasPendingPlanChange($result['subscription']))->toBeTrue();
|
|
|
|
|
|
|
|
|
|
$pending = $this->service->getPendingPlanChange($result['subscription']);
|
|
|
|
|
expect($pending['to_package_code'])->toBe('agency');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('cancels scheduled plan change', function () {
|
|
|
|
|
$result = $this->service->changePlan(
|
|
|
|
|
$this->subscription,
|
|
|
|
|
$this->agencyPackage,
|
|
|
|
|
immediate: false
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$subscription = $this->service->cancelScheduledPlanChange($result['subscription']);
|
|
|
|
|
|
|
|
|
|
expect($this->service->hasPendingPlanChange($subscription))->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('ProrationResult', function () {
|
|
|
|
|
it('converts to array correctly', function () {
|
|
|
|
|
$result = new ProrationResult(
|
|
|
|
|
daysRemaining: 15,
|
|
|
|
|
totalPeriodDays: 30,
|
|
|
|
|
usedPercentage: 0.5,
|
|
|
|
|
currentPlanPrice: 19.00,
|
|
|
|
|
newPlanPrice: 49.00,
|
|
|
|
|
creditAmount: 9.50,
|
|
|
|
|
proratedNewPlanCost: 24.50,
|
|
|
|
|
netAmount: 15.00,
|
|
|
|
|
currency: 'GBP'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$array = $result->toArray();
|
|
|
|
|
|
|
|
|
|
expect($array)->toHaveKeys([
|
|
|
|
|
'days_remaining',
|
|
|
|
|
'total_period_days',
|
|
|
|
|
'used_percentage',
|
|
|
|
|
'current_plan_price',
|
|
|
|
|
'new_plan_price',
|
|
|
|
|
'credit_amount',
|
|
|
|
|
'prorated_new_plan_cost',
|
|
|
|
|
'net_amount',
|
|
|
|
|
'currency',
|
|
|
|
|
'is_upgrade',
|
|
|
|
|
'is_downgrade',
|
|
|
|
|
'requires_payment',
|
|
|
|
|
])
|
|
|
|
|
->and($array['is_upgrade'])->toBeTrue()
|
|
|
|
|
->and($array['is_downgrade'])->toBeFalse()
|
|
|
|
|
->and($array['requires_payment'])->toBeTrue();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('identifies same price plans', function () {
|
|
|
|
|
$result = new ProrationResult(
|
|
|
|
|
daysRemaining: 15,
|
|
|
|
|
totalPeriodDays: 30,
|
|
|
|
|
usedPercentage: 0.5,
|
|
|
|
|
currentPlanPrice: 49.00,
|
|
|
|
|
newPlanPrice: 49.00,
|
|
|
|
|
creditAmount: 24.50,
|
|
|
|
|
proratedNewPlanCost: 24.50,
|
|
|
|
|
netAmount: 0.00,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect($result->isSamePrice())->toBeTrue()
|
|
|
|
|
->and($result->isUpgrade())->toBeFalse()
|
|
|
|
|
->and($result->isDowngrade())->toBeFalse()
|
|
|
|
|
->and($result->requiresPayment())->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
});
|