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', ['data'=>['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', ['data'=>['current_billing_period'=>null]])); $license = License::firstOrFail(); $this->assertNotNull($license->revoked_at); $this->assertFalse($license->is_usable); } }