133 lines
5.5 KiB
PHP
Executable File
133 lines
5.5 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use Modules\Licensing\Models\License;
|
|
use Modules\Licensing\Models\LicenseProduct;
|
|
use App\Models\User;
|
|
use Modules\Paddle\Services\PurchaseProvisioner;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class PaddleSubscriptionProvisioningTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private function product(string $price, string $edition, int $activations): LicenseProduct
|
|
{
|
|
return LicenseProduct::create([
|
|
'code' => strtolower($edition).'-'.$price,
|
|
'name' => $edition,
|
|
'edition' => $edition,
|
|
'activation_limit' => $activations,
|
|
'max_major_version' => 1,
|
|
'payment_provider' => 'paddle',
|
|
'external_product_id' => $edition === 'Personal' ? 'pro_personal' : 'pro_professional',
|
|
'external_price_id' => $price,
|
|
'active' => true,
|
|
]);
|
|
}
|
|
|
|
private function subscription(string $type, string $status, string $price, string $product, array $extra = []): array
|
|
{
|
|
return [
|
|
'event_type' => $type,
|
|
'data' => array_replace_recursive([
|
|
'id' => 'sub_test',
|
|
'status' => $status,
|
|
'customer_id' => 'ctm_test',
|
|
'transaction_id' => 'txn_test',
|
|
'custom_data' => ['customer_name' => 'Mark', 'customer_email' => 'mark@example.com'],
|
|
'current_billing_period' => ['starts_at' => '2026-09-09T00:00:00Z', 'ends_at' => '2026-10-09T00:00:00Z'],
|
|
'scheduled_change' => null,
|
|
'items' => [[
|
|
'recurring' => true,
|
|
'price' => ['id' => $price, 'product_id' => $product],
|
|
]],
|
|
], $extra),
|
|
];
|
|
}
|
|
|
|
public function test_trialing_subscription_issues_a_usable_license(): void
|
|
{
|
|
$this->product('pri_personal_monthly', 'Personal', 1);
|
|
|
|
app(PurchaseProvisioner::class)->provision('paddle', $this->subscription(
|
|
'subscription.created', 'trialing', 'pri_personal_monthly', 'pro_personal'
|
|
));
|
|
|
|
$license = License::firstOrFail();
|
|
$this->assertSame('Personal', $license->edition);
|
|
$this->assertSame('sub_test', $license->external_subscription_id);
|
|
$this->assertSame('trialing', $license->subscription_status);
|
|
$this->assertTrue($license->is_usable);
|
|
}
|
|
|
|
public function test_upgrade_changes_entitlement_without_changing_license(): void
|
|
{
|
|
$this->product('pri_personal_monthly', 'Personal', 1);
|
|
$this->product('pri_professional_monthly', 'Professional', 3);
|
|
$service = app(PurchaseProvisioner::class);
|
|
|
|
$service->provision('paddle', $this->subscription('subscription.created', 'active', 'pri_personal_monthly', 'pro_personal'));
|
|
$id = License::firstOrFail()->id;
|
|
$service->provision('paddle', $this->subscription('subscription.updated', 'active', 'pri_professional_monthly', 'pro_professional'));
|
|
|
|
$license = License::firstOrFail();
|
|
$this->assertSame($id, $license->id);
|
|
$this->assertSame('Professional', $license->edition);
|
|
$this->assertSame(3, $license->activation_limit);
|
|
}
|
|
|
|
public function test_scheduled_cancellation_does_not_revoke_before_effective_date(): void
|
|
{
|
|
$this->product('pri_personal_monthly', 'Personal', 1);
|
|
app(PurchaseProvisioner::class)->provision('paddle', $this->subscription(
|
|
'subscription.updated', 'active', 'pri_personal_monthly', 'pro_personal',
|
|
['scheduled_change' => ['action' => 'cancel', 'effective_at' => '2026-10-09T00:00:00Z']]
|
|
));
|
|
|
|
$license = License::firstOrFail();
|
|
$this->assertNull($license->revoked_at);
|
|
$this->assertNotNull($license->cancel_scheduled_at);
|
|
}
|
|
|
|
public function test_canceled_subscription_revokes_license(): void
|
|
{
|
|
$this->product('pri_personal_monthly', 'Personal', 1);
|
|
$service = app(PurchaseProvisioner::class);
|
|
$service->provision('paddle', $this->subscription('subscription.created', 'active', 'pri_personal_monthly', 'pro_personal'));
|
|
$service->provision('paddle', $this->subscription('subscription.canceled', 'canceled', 'pri_personal_monthly', 'pro_personal', ['current_billing_period' => null]));
|
|
|
|
$license = License::firstOrFail();
|
|
$this->assertNotNull($license->revoked_at);
|
|
$this->assertFalse($license->is_usable);
|
|
}
|
|
|
|
public function test_new_purchase_creates_a_portal_user_for_the_customer(): void
|
|
{
|
|
$this->product('pri_personal_monthly', 'Personal', 1);
|
|
|
|
app(PurchaseProvisioner::class)->provision('paddle', $this->subscription(
|
|
'subscription.created', 'active', 'pri_personal_monthly', 'pro_personal'
|
|
));
|
|
|
|
$user = User::where('email', 'mark@example.com')->firstOrFail();
|
|
$this->assertSame('Mark', $user->name);
|
|
$this->assertNotNull($user->email_verified_at);
|
|
}
|
|
|
|
public function test_existing_customer_account_is_reused(): void
|
|
{
|
|
User::factory()->create(['name' => 'Existing Mark', 'email' => 'mark@example.com']);
|
|
$this->product('pri_personal_monthly', 'Personal', 1);
|
|
|
|
app(PurchaseProvisioner::class)->provision('paddle', $this->subscription(
|
|
'subscription.created', 'active', 'pri_personal_monthly', 'pro_personal'
|
|
));
|
|
|
|
$this->assertSame(1, User::where('email', 'mark@example.com')->count());
|
|
$this->assertSame('Existing Mark', User::where('email', 'mark@example.com')->firstOrFail()->name);
|
|
}
|
|
}
|