NativePHP Ultra is a premium subscription that gives you
- access to all first-party plugins, the Claude Code
+ access to all first-party plugins, around 30% off
+ third-party plugins, the Claude Code
Plugin Dev Kit, discounts on NativePHP courses and
apps, Teams support, premium support through private channels with expedited
turnaround times, and up to 90% revenue share on
diff --git a/tests/Feature/AddUltraPricesToThirdPartyPluginsMigrationTest.php b/tests/Feature/AddUltraPricesToThirdPartyPluginsMigrationTest.php
new file mode 100644
index 000000000..b8b64b000
--- /dev/null
+++ b/tests/Feature/AddUltraPricesToThirdPartyPluginsMigrationTest.php
@@ -0,0 +1,78 @@
+paid()->create(['is_official' => false, 'tier' => PluginTier::Bronze]);
+ $silver = Plugin::factory()->paid()->create(['is_official' => false, 'tier' => PluginTier::Silver]);
+ $gold = Plugin::factory()->paid()->create(['is_official' => false, 'tier' => PluginTier::Gold]);
+
+ $this->migration()->up();
+
+ $this->assertSame(2000, $this->ultraPriceOf($bronze)->amount);
+ $this->assertSame(3500, $this->ultraPriceOf($silver)->amount);
+ $this->assertSame(7000, $this->ultraPriceOf($gold)->amount);
+
+ $this->assertTrue($this->ultraPriceOf($gold)->is_active);
+ $this->assertSame('USD', $this->ultraPriceOf($gold)->currency);
+ }
+
+ public function test_official_plugins_and_plugins_without_a_tier_are_left_alone(): void
+ {
+ $official = Plugin::factory()->paid()->create(['is_official' => true, 'tier' => PluginTier::Gold]);
+ $withoutTier = Plugin::factory()->paid()->create(['is_official' => false, 'tier' => null]);
+ PluginPrice::factory()->regular()->amount(2200)->create(['plugin_id' => $withoutTier->id]);
+
+ $this->migration()->up();
+
+ $this->assertNull($this->ultraPriceOf($official));
+ $this->assertNull($this->ultraPriceOf($withoutTier));
+ }
+
+ public function test_an_existing_ultra_price_is_not_duplicated_or_changed(): void
+ {
+ $plugin = Plugin::factory()->paid()->create(['is_official' => false, 'tier' => PluginTier::Gold]);
+ PluginPrice::factory()->ultra()->amount(6000)->create(['plugin_id' => $plugin->id]);
+
+ $this->migration()->up();
+
+ $this->assertSame(1, $plugin->prices()->forTier(PriceTier::Ultra)->count());
+ $this->assertSame(6000, $this->ultraPriceOf($plugin)->amount);
+ }
+
+ public function test_rolling_back_removes_the_ultra_prices(): void
+ {
+ $plugin = Plugin::factory()->paid()->create(['is_official' => false, 'tier' => PluginTier::Gold]);
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+
+ $migration = $this->migration();
+ $migration->up();
+ $migration->down();
+
+ $this->assertNull($this->ultraPriceOf($plugin));
+ $this->assertSame(1, $plugin->prices()->count());
+ }
+
+ private function ultraPriceOf(Plugin $plugin): ?PluginPrice
+ {
+ return $plugin->prices()->forTier(PriceTier::Ultra)->first();
+ }
+
+ private function migration(): Migration
+ {
+ return require database_path('migrations/2026_10_01_124505_add_ultra_prices_to_third_party_plugins.php');
+ }
+}
diff --git a/tests/Feature/CheckoutCouponTest.php b/tests/Feature/CheckoutCouponTest.php
index a55e25027..6bb55076a 100644
--- a/tests/Feature/CheckoutCouponTest.php
+++ b/tests/Feature/CheckoutCouponTest.php
@@ -2,6 +2,8 @@
namespace Tests\Feature;
+use App\Models\Plugin;
+use App\Models\PluginPrice;
use App\Models\Product;
use App\Models\ProductPrice;
use App\Models\User;
@@ -285,6 +287,59 @@ public function cart_checkout_keeps_manual_promotion_codes_when_none_pre_applied
$this->assertArrayNotHasKey('discounts', $captured->params);
}
+ #[Test]
+ public function cart_checkout_charges_the_ultra_price_and_still_takes_promotion_codes(): void
+ {
+ $captured = $this->captureStripeCheckoutParams();
+
+ $ultraSubscriber = User::factory()->create(['stripe_id' => 'cus_test123']);
+ Subscription::factory()
+ ->for($ultraSubscriber)
+ ->active()
+ ->create(['stripe_price' => config('subscriptions.plans.max.stripe_price_id')]);
+
+ $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => false]);
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->ultra()->amount(7000)->create(['plugin_id' => $plugin->id]);
+
+ $cartService = resolve(CartService::class);
+ $cart = $cartService->getCart($ultraSubscriber);
+ $cartService->addPlugin($cart, $plugin);
+
+ $this->actingAs($ultraSubscriber)
+ ->post(route('cart.checkout'))
+ ->assertRedirect('https://checkout.stripe.com/test-session');
+
+ $this->assertNotNull($captured->params, 'Stripe checkout session should have been created');
+ $this->assertSame(7000, $captured->params['line_items'][0]['price_data']['unit_amount']);
+ $this->assertSame($plugin->name, $captured->params['line_items'][0]['price_data']['product_data']['name']);
+ $this->assertTrue($captured->params['allow_promotion_codes']);
+ $this->assertArrayNotHasKey('discounts', $captured->params);
+ }
+
+ #[Test]
+ public function cart_checkout_charges_the_regular_price_to_a_buyer_without_ultra(): void
+ {
+ $captured = $this->captureStripeCheckoutParams();
+ $user = User::factory()->create(['stripe_id' => 'cus_test123']);
+
+ $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => false]);
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->ultra()->amount(7000)->create(['plugin_id' => $plugin->id]);
+
+ $cartService = resolve(CartService::class);
+ $cart = $cartService->getCart($user);
+ $cartService->addPlugin($cart, $plugin);
+
+ $this->actingAs($user)
+ ->post(route('cart.checkout'))
+ ->assertRedirect('https://checkout.stripe.com/test-session');
+
+ $this->assertNotNull($captured->params, 'Stripe checkout session should have been created');
+ $this->assertSame(9900, $captured->params['line_items'][0]['price_data']['unit_amount']);
+ $this->assertTrue($captured->params['allow_promotion_codes']);
+ }
+
#[Test]
public function course_checkout_uses_stripe_price_id_from_the_database_over_config(): void
{
diff --git a/tests/Feature/Commands/BackfillMissingPayoutsTest.php b/tests/Feature/Commands/BackfillMissingPayoutsTest.php
index 5a4cf6c2e..c7f6b43fc 100644
--- a/tests/Feature/Commands/BackfillMissingPayoutsTest.php
+++ b/tests/Feature/Commands/BackfillMissingPayoutsTest.php
@@ -4,6 +4,7 @@
use App\Enums\PayoutStatus;
use App\Enums\PluginType;
+use App\Enums\PriceTier;
use App\Models\DeveloperAccount;
use App\Models\Plugin;
use App\Models\PluginLicense;
@@ -52,6 +53,28 @@ public function test_creates_pending_payout_using_developer_percentage(): void
$this->assertTrue($payout->eligible_for_payout_at->isPast());
}
+ public function test_creates_payout_without_platform_fee_for_sale_at_ultra_price(): void
+ {
+ $developerAccount = DeveloperAccount::factory()->create(['payout_percentage' => 70]);
+ $plugin = $this->createThirdPartyPlugin($developerAccount);
+
+ PluginLicense::factory()->create([
+ 'plugin_id' => $plugin->id,
+ 'price_paid' => 7000,
+ 'price_tier' => PriceTier::Ultra,
+ ]);
+
+ $this->artisan('payouts:backfill')
+ ->expectsOutputToContain('Created 1 payout record(s).')
+ ->assertExitCode(0);
+
+ $payout = PluginPayout::first();
+ $this->assertNotNull($payout);
+ $this->assertEquals(7000, $payout->gross_amount);
+ $this->assertEquals(0, $payout->platform_fee);
+ $this->assertEquals(7000, $payout->developer_amount);
+ }
+
public function test_creates_held_payout_when_developer_cannot_receive_payouts(): void
{
$developerAccount = DeveloperAccount::factory()->pending()->create();
diff --git a/tests/Feature/Filament/PluginPricesRelationManagerTest.php b/tests/Feature/Filament/PluginPricesRelationManagerTest.php
new file mode 100644
index 000000000..2bb2e602f
--- /dev/null
+++ b/tests/Feature/Filament/PluginPricesRelationManagerTest.php
@@ -0,0 +1,67 @@
+admin = User::factory()->create(['email' => 'admin@test.com']);
+ config(['filament.users' => ['admin@test.com']]);
+ }
+
+ public function test_pricing_table_lists_an_ultra_price(): void
+ {
+ $plugin = Plugin::factory()->paid()->approved()->create(['is_official' => false]);
+ $regularPrice = PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ $ultraPrice = PluginPrice::factory()->ultra()->amount(7000)->create(['plugin_id' => $plugin->id]);
+
+ Livewire::actingAs($this->admin)
+ ->test(PricesRelationManager::class, [
+ 'ownerRecord' => $plugin,
+ 'pageClass' => EditPlugin::class,
+ ])
+ ->assertSuccessful()
+ ->assertCanSeeTableRecords([$regularPrice, $ultraPrice])
+ ->assertSee('$70.00');
+ }
+
+ public function test_admin_can_add_an_ultra_price(): void
+ {
+ $plugin = Plugin::factory()->paid()->approved()->create(['is_official' => false]);
+
+ Livewire::actingAs($this->admin)
+ ->test(PricesRelationManager::class, [
+ 'ownerRecord' => $plugin,
+ 'pageClass' => EditPlugin::class,
+ ])
+ ->callTableAction('create', data: [
+ 'tier' => 'ultra',
+ 'amount' => 7000,
+ 'currency' => 'USD',
+ 'is_active' => true,
+ ])
+ ->assertHasNoTableActionErrors();
+
+ $this->assertDatabaseHas('plugin_prices', [
+ 'plugin_id' => $plugin->id,
+ 'tier' => 'ultra',
+ 'amount' => 7000,
+ ]);
+ }
+}
diff --git a/tests/Feature/Filament/ThirdPartySaleResourceTest.php b/tests/Feature/Filament/ThirdPartySaleResourceTest.php
index f15169998..9a6cd492b 100644
--- a/tests/Feature/Filament/ThirdPartySaleResourceTest.php
+++ b/tests/Feature/Filament/ThirdPartySaleResourceTest.php
@@ -3,6 +3,7 @@
namespace Tests\Feature\Filament;
use App\Enums\PluginType;
+use App\Enums\PriceTier;
use App\Filament\Resources\ThirdPartySaleResource\Pages\ListThirdPartySales;
use App\Models\DeveloperAccount;
use App\Models\Plugin;
@@ -159,6 +160,36 @@ public function test_totals_paid_and_due_to_seller(): void
->assertSee('$49.00');
}
+ public function test_marks_sales_made_at_the_ultra_price(): void
+ {
+ $plugin = $this->createThirdPartyPlugin();
+ $ultraSale = PluginLicense::factory()->create([
+ 'plugin_id' => $plugin->id,
+ 'price_paid' => 7000,
+ 'price_tier' => PriceTier::Ultra,
+ ]);
+
+ Livewire::actingAs($this->admin)
+ ->test(ListThirdPartySales::class)
+ ->assertCanSeeTableRecords([$ultraSale])
+ ->assertSee('Ultra price');
+ }
+
+ public function test_does_not_mark_sales_made_at_the_regular_price(): void
+ {
+ $plugin = $this->createThirdPartyPlugin();
+ $regularSale = PluginLicense::factory()->create([
+ 'plugin_id' => $plugin->id,
+ 'price_paid' => 9900,
+ 'price_tier' => PriceTier::Regular,
+ ]);
+
+ Livewire::actingAs($this->admin)
+ ->test(ListThirdPartySales::class)
+ ->assertCanSeeTableRecords([$regularSale])
+ ->assertDontSee('Ultra price');
+ }
+
public function test_filters_sales_missing_payouts(): void
{
$plugin = $this->createThirdPartyPlugin();
diff --git a/tests/Feature/Livewire/Customer/PluginPaidOnboardingTest.php b/tests/Feature/Livewire/Customer/PluginPaidOnboardingTest.php
index faa9288e6..4985b6254 100644
--- a/tests/Feature/Livewire/Customer/PluginPaidOnboardingTest.php
+++ b/tests/Feature/Livewire/Customer/PluginPaidOnboardingTest.php
@@ -165,6 +165,27 @@ public function test_edit_draft_shows_paid_option_enabled_with_onboarding(): voi
->assertDontSee('complete developer onboarding');
}
+ public function test_edit_draft_shows_what_ultra_subscribers_pay_for_each_tier(): void
+ {
+ $user = $this->createGitHubUser();
+ DeveloperAccount::factory()->for($user)->create();
+ $plugin = Plugin::factory()->draft()->for($user)->create([
+ 'name' => 'testuser/tier-prices',
+ ]);
+
+ [$vendor, $package] = explode('/', $plugin->name);
+
+ Livewire::actingAs($user)->test(Show::class, [
+ 'vendor' => $vendor,
+ 'package' => $package,
+ ])
+ ->set('pluginType', 'paid')
+ ->assertSee('$20 for Ultra subscribers')
+ ->assertSee('$35 for Ultra subscribers')
+ ->assertSee('$70 for Ultra subscribers')
+ ->assertSee('you keep 100% of what they pay');
+ }
+
public function test_save_draft_as_paid_blocked_without_onboarding(): void
{
$user = $this->createGitHubUser();
diff --git a/tests/Feature/MaxSubscriberPayoutTest.php b/tests/Feature/MaxSubscriberPayoutTest.php
index 0f9ea93df..380bfb908 100644
--- a/tests/Feature/MaxSubscriberPayoutTest.php
+++ b/tests/Feature/MaxSubscriberPayoutTest.php
@@ -3,14 +3,21 @@
namespace Tests\Feature;
use App\Enums\PayoutStatus;
+use App\Enums\PriceTier;
use App\Jobs\HandleInvoicePaidJob;
+use App\Models\BundlePrice;
use App\Models\Cart;
use App\Models\CartItem;
use App\Models\DeveloperAccount;
use App\Models\Plugin;
+use App\Models\PluginBundle;
+use App\Models\PluginLicense;
use App\Models\PluginPayout;
use App\Models\PluginPrice;
+use App\Models\Product;
+use App\Models\ProductPrice;
use App\Models\User;
+use App\Services\CartService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Cashier\Subscription;
use PHPUnit\Framework\Attributes\Test;
@@ -32,7 +39,11 @@ protected function setUp(): void
config(['subscriptions.plans.max.stripe_price_id' => self::MAX_PRICE_ID]);
}
- private function createStripeInvoice(string $cartId, string $customerId): Invoice
+ /**
+ * @param array $cartItemIds
+ * @param array $attributes
+ */
+ private function createStripeInvoice(string $cartId, string $customerId, array $cartItemIds = [], array $attributes = []): Invoice
{
$invoice = Invoice::constructFrom([
'id' => 'in_test_'.uniqid(),
@@ -40,13 +51,32 @@ private function createStripeInvoice(string $cartId, string $customerId): Invoic
'customer' => $customerId,
'payment_intent' => 'pi_test_'.uniqid(),
'currency' => 'usd',
- 'metadata' => ['cart_id' => $cartId],
+ 'metadata' => array_filter([
+ 'cart_id' => $cartId,
+ 'cart_item_ids' => implode(',', $cartItemIds),
+ ]),
'lines' => [],
+ ...$attributes,
]);
return $invoice;
}
+ /**
+ * An invoice line as Stripe reports it, with the discount taken off that line.
+ *
+ * @return array
+ */
+ private function invoiceLine(string $description, int $amount, int $discount = 0): array
+ {
+ return [
+ 'object' => 'line_item',
+ 'description' => $description,
+ 'amount' => $amount,
+ 'discount_amounts' => $discount > 0 ? [['amount' => $discount, 'discount' => 'di_test']] : [],
+ ];
+ }
+
private function createSubscription(User $user, string $priceId): Subscription
{
return Subscription::factory()
@@ -55,6 +85,21 @@ private function createSubscription(User $user, string $priceId): Subscription
->create(['stripe_price' => $priceId]);
}
+ private function createThirdPartyPluginWithUltraPrice(DeveloperAccount $developerAccount): Plugin
+ {
+ $plugin = Plugin::factory()->approved()->paid()->create([
+ 'is_active' => true,
+ 'is_official' => false,
+ 'user_id' => $developerAccount->user_id,
+ 'developer_account_id' => $developerAccount->id,
+ ]);
+
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->ultra()->amount(7000)->create(['plugin_id' => $plugin->id]);
+
+ return $plugin;
+ }
+
#[Test]
public function max_subscriber_pays_normal_platform_fee_for_third_party_plugin(): void
{
@@ -90,6 +135,296 @@ public function max_subscriber_pays_normal_platform_fee_for_third_party_plugin()
$this->assertEquals(PayoutStatus::Pending, $payout->status);
}
+ #[Test]
+ public function max_subscriber_buying_at_the_ultra_price_pays_no_platform_fee(): void
+ {
+ $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]);
+ $this->createSubscription($buyer, self::MAX_PRICE_ID);
+
+ $plugin = $this->createThirdPartyPluginWithUltraPrice(DeveloperAccount::factory()->create());
+
+ $cart = Cart::factory()->for($buyer)->create();
+ $item = resolve(CartService::class)->addPlugin($cart, $plugin);
+
+ $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id, [$item->id]);
+ $job = new HandleInvoicePaidJob($invoice);
+ $job->handle();
+
+ $license = PluginLicense::first();
+ $this->assertNotNull($license);
+ $this->assertEquals(7000, $license->price_paid);
+ $this->assertEquals(PriceTier::Ultra, $license->price_tier);
+
+ $payout = PluginPayout::first();
+ $this->assertNotNull($payout);
+ $this->assertEquals(7000, $payout->gross_amount);
+ $this->assertEquals(0, $payout->platform_fee);
+ $this->assertEquals(7000, $payout->developer_amount);
+ $this->assertEquals(PayoutStatus::Pending, $payout->status);
+ }
+
+ #[Test]
+ public function ultra_priced_sale_pays_the_developer_in_full_whatever_their_payout_percentage(): void
+ {
+ $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]);
+ $this->createSubscription($buyer, self::MAX_PRICE_ID);
+
+ $plugin = $this->createThirdPartyPluginWithUltraPrice(
+ DeveloperAccount::factory()->create(['payout_percentage' => 80])
+ );
+
+ $cart = Cart::factory()->for($buyer)->create();
+ $item = resolve(CartService::class)->addPlugin($cart, $plugin);
+
+ $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id, [$item->id]);
+ $job = new HandleInvoicePaidJob($invoice);
+ $job->handle();
+
+ $payout = PluginPayout::first();
+ $this->assertNotNull($payout);
+ $this->assertEquals(7000, $payout->gross_amount);
+ $this->assertEquals(0, $payout->platform_fee);
+ $this->assertEquals(7000, $payout->developer_amount);
+ }
+
+ #[Test]
+ public function buyer_without_ultra_pays_the_regular_price_and_the_normal_platform_fee(): void
+ {
+ $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]);
+ $this->createSubscription($buyer, self::PRO_PRICE_ID);
+
+ $plugin = $this->createThirdPartyPluginWithUltraPrice(DeveloperAccount::factory()->create());
+
+ $cart = Cart::factory()->for($buyer)->create();
+ $item = resolve(CartService::class)->addPlugin($cart, $plugin);
+
+ $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id, [$item->id]);
+ $job = new HandleInvoicePaidJob($invoice);
+ $job->handle();
+
+ $license = PluginLicense::first();
+ $this->assertNotNull($license);
+ $this->assertEquals(9900, $license->price_paid);
+ $this->assertEquals(PriceTier::Regular, $license->price_tier);
+
+ $payout = PluginPayout::first();
+ $this->assertNotNull($payout);
+ $this->assertEquals(9900, $payout->gross_amount);
+ $this->assertEquals(2970, $payout->platform_fee);
+ $this->assertEquals(6930, $payout->developer_amount);
+ }
+
+ #[Test]
+ public function promotion_code_on_an_ultra_priced_sale_pays_the_developer_what_the_buyer_paid(): void
+ {
+ $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]);
+ $this->createSubscription($buyer, self::MAX_PRICE_ID);
+
+ $plugin = $this->createThirdPartyPluginWithUltraPrice(DeveloperAccount::factory()->create());
+
+ $cart = Cart::factory()->for($buyer)->create();
+ $item = resolve(CartService::class)->addPlugin($cart, $plugin);
+
+ $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id, [$item->id], [
+ 'subtotal' => 7000,
+ 'total' => 6300,
+ 'total_discount_amounts' => [['amount' => 700, 'discount' => 'di_test']],
+ 'lines' => [
+ 'object' => 'list',
+ 'data' => [$this->invoiceLine($plugin->name, 7000, discount: 700)],
+ ],
+ ]);
+ $job = new HandleInvoicePaidJob($invoice);
+ $job->handle();
+
+ $license = PluginLicense::first();
+ $this->assertNotNull($license);
+ $this->assertEquals(6300, $license->price_paid);
+ $this->assertEquals(PriceTier::Ultra, $license->price_tier);
+
+ $payout = PluginPayout::first();
+ $this->assertNotNull($payout);
+ $this->assertEquals(6300, $payout->gross_amount);
+ $this->assertEquals(0, $payout->platform_fee);
+ $this->assertEquals(6300, $payout->developer_amount);
+ }
+
+ #[Test]
+ public function coupon_on_another_item_does_not_reduce_an_ultra_priced_payout(): void
+ {
+ $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]);
+ $this->createSubscription($buyer, self::MAX_PRICE_ID);
+
+ $plugin = $this->createThirdPartyPluginWithUltraPrice(DeveloperAccount::factory()->create());
+
+ $product = Product::factory()->active()->create();
+ ProductPrice::factory()->for($product)->regular()->amount(29900)->create();
+
+ $cartService = resolve(CartService::class);
+ $cart = Cart::factory()->for($buyer)->create();
+ $pluginItem = $cartService->addPlugin($cart, $plugin);
+ $productItem = $cartService->addProduct($cart, $product);
+
+ $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id, [$pluginItem->id, $productItem->id], [
+ 'subtotal' => 36900,
+ 'total' => 26900,
+ 'total_discount_amounts' => [['amount' => 10000, 'discount' => 'di_test']],
+ 'lines' => [
+ 'object' => 'list',
+ 'data' => [
+ $this->invoiceLine($plugin->name, 7000),
+ $this->invoiceLine($product->name, 29900, discount: 10000),
+ ],
+ ],
+ ]);
+ $job = new HandleInvoicePaidJob($invoice);
+ $job->handle();
+
+ $this->assertEquals(7000, PluginLicense::first()->price_paid);
+
+ $payout = PluginPayout::first();
+ $this->assertNotNull($payout);
+ $this->assertEquals(7000, $payout->gross_amount);
+ $this->assertEquals(0, $payout->platform_fee);
+ $this->assertEquals(7000, $payout->developer_amount);
+ }
+
+ #[Test]
+ public function discount_is_shared_out_when_the_ultra_priced_plugin_has_no_invoice_line(): void
+ {
+ $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]);
+ $this->createSubscription($buyer, self::MAX_PRICE_ID);
+
+ $plugin = $this->createThirdPartyPluginWithUltraPrice(DeveloperAccount::factory()->create());
+
+ $cart = Cart::factory()->for($buyer)->create();
+ $item = resolve(CartService::class)->addPlugin($cart, $plugin);
+
+ $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id, [$item->id], [
+ 'subtotal' => 10500,
+ 'total' => 9400,
+ 'total_discount_amounts' => [['amount' => 1100, 'discount' => 'di_test']],
+ ]);
+ $job = new HandleInvoicePaidJob($invoice);
+ $job->handle();
+
+ // 7000 of a 10500 order that had 1100 taken off is 6266.67, rounded down
+ $this->assertEquals(6266, PluginLicense::first()->price_paid);
+
+ $payout = PluginPayout::first();
+ $this->assertNotNull($payout);
+ $this->assertEquals(6266, $payout->gross_amount);
+ $this->assertEquals(0, $payout->platform_fee);
+ $this->assertEquals(6266, $payout->developer_amount);
+ }
+
+ #[Test]
+ public function ultra_priced_sale_fully_covered_by_a_promotion_code_creates_no_payout(): void
+ {
+ $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]);
+ $this->createSubscription($buyer, self::MAX_PRICE_ID);
+
+ $plugin = $this->createThirdPartyPluginWithUltraPrice(DeveloperAccount::factory()->create());
+
+ $cart = Cart::factory()->for($buyer)->create();
+ $item = resolve(CartService::class)->addPlugin($cart, $plugin);
+
+ $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id, [$item->id], [
+ 'subtotal' => 7000,
+ 'total' => 0,
+ 'total_discount_amounts' => [['amount' => 7000, 'discount' => 'di_test']],
+ 'lines' => [
+ 'object' => 'list',
+ 'data' => [$this->invoiceLine($plugin->name, 7000, discount: 7000)],
+ ],
+ ]);
+ $job = new HandleInvoicePaidJob($invoice);
+ $job->handle();
+
+ $this->assertEquals(0, PluginLicense::first()->price_paid);
+ $this->assertEquals(0, PluginPayout::count());
+ }
+
+ #[Test]
+ public function promotion_code_on_a_regular_priced_sale_pays_the_developer_their_share_of_what_the_buyer_paid(): void
+ {
+ $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]);
+
+ $plugin = $this->createThirdPartyPluginWithUltraPrice(DeveloperAccount::factory()->create());
+
+ $cart = Cart::factory()->for($buyer)->create();
+ $item = resolve(CartService::class)->addPlugin($cart, $plugin);
+
+ $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id, [$item->id], [
+ 'subtotal' => 9900,
+ 'total' => 8910,
+ 'total_discount_amounts' => [['amount' => 990, 'discount' => 'di_test']],
+ 'lines' => [
+ 'object' => 'list',
+ 'data' => [$this->invoiceLine($plugin->name, 9900, discount: 990)],
+ ],
+ ]);
+ $job = new HandleInvoicePaidJob($invoice);
+ $job->handle();
+
+ $license = PluginLicense::first();
+ $this->assertEquals(8910, $license->price_paid);
+ $this->assertEquals(PriceTier::Regular, $license->price_tier);
+
+ $payout = PluginPayout::first();
+ $this->assertNotNull($payout);
+ $this->assertEquals(8910, $payout->gross_amount);
+ $this->assertEquals(2673, $payout->platform_fee);
+ $this->assertEquals(6237, $payout->developer_amount);
+ }
+
+ #[Test]
+ public function promotion_code_on_a_bundle_is_shared_between_its_plugins(): void
+ {
+ $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]);
+
+ $firstPlugin = $this->createThirdPartyPluginWithUltraPrice(DeveloperAccount::factory()->create());
+ $secondPlugin = Plugin::factory()->approved()->paid()->create([
+ 'is_active' => true,
+ 'is_official' => false,
+ 'developer_account_id' => DeveloperAccount::factory()->create()->id,
+ ]);
+ PluginPrice::factory()->regular()->amount(4900)->create(['plugin_id' => $secondPlugin->id]);
+
+ $bundle = PluginBundle::factory()->active()->create();
+ $bundle->plugins()->attach([
+ $firstPlugin->id => ['sort_order' => 1],
+ $secondPlugin->id => ['sort_order' => 2],
+ ]);
+ BundlePrice::factory()->regular()->amount(10000)->create(['plugin_bundle_id' => $bundle->id]);
+
+ $cart = Cart::factory()->for($buyer)->create();
+ $item = resolve(CartService::class)->addBundle($cart, $bundle);
+
+ $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id, [$item->id], [
+ 'subtotal' => 10000,
+ 'total' => 9000,
+ 'total_discount_amounts' => [['amount' => 1000, 'discount' => 'di_test']],
+ 'lines' => [
+ 'object' => 'list',
+ 'data' => [$this->invoiceLine($bundle->name.' (Bundle)', 10000, discount: 1000)],
+ ],
+ ]);
+ $job = new HandleInvoicePaidJob($invoice);
+ $job->handle();
+
+ // The 9000 paid is split by regular price: 9900 and 4900 of 14800
+ $firstLicense = PluginLicense::where('plugin_id', $firstPlugin->id)->sole();
+ $secondLicense = PluginLicense::where('plugin_id', $secondPlugin->id)->sole();
+ $this->assertEquals(6020, $firstLicense->price_paid);
+ $this->assertEquals(2980, $secondLicense->price_paid);
+
+ $this->assertEquals(6020, $firstLicense->payout->gross_amount);
+ $this->assertEquals(4214, $firstLicense->payout->developer_amount);
+ $this->assertEquals(2980, $secondLicense->payout->gross_amount);
+ $this->assertEquals(2086, $secondLicense->payout->developer_amount);
+ }
+
#[Test]
public function non_max_subscriber_gets_normal_platform_fee_for_third_party_plugin(): void
{
diff --git a/tests/Feature/MobileRouteTest.php b/tests/Feature/MobileRouteTest.php
index d02166a77..d2ebd697d 100644
--- a/tests/Feature/MobileRouteTest.php
+++ b/tests/Feature/MobileRouteTest.php
@@ -28,4 +28,15 @@ public function pricing_route_returns_pricing_page()
->assertOk()
->assertSeeLivewire('mobile-pricing');
}
+
+ #[Test]
+ public function pricing_page_lists_the_third_party_plugin_discount()
+ {
+ $this
+ ->withoutVite()
+ ->get(route('pricing'))
+ ->assertOk()
+ ->assertSee('Around 30% off third-party plugins')
+ ->assertSee('around 30% off');
+ }
}
diff --git a/tests/Feature/PluginShowUltraCardTest.php b/tests/Feature/PluginShowUltraCardTest.php
index 79dd0b2f2..ed550ea8b 100644
--- a/tests/Feature/PluginShowUltraCardTest.php
+++ b/tests/Feature/PluginShowUltraCardTest.php
@@ -26,6 +26,16 @@ protected function setUp(): void
config(['subscriptions.plans.max.stripe_price_id' => self::MAX_PRICE_ID]);
}
+ private function createThirdPartyPluginWithUltraPrice(): Plugin
+ {
+ $plugin = Plugin::factory()->approved()->paid()->create(['is_official' => false]);
+
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->ultra()->amount(7000)->create(['plugin_id' => $plugin->id]);
+
+ return $plugin;
+ }
+
public function test_ultra_card_shows_on_paid_first_party_plugin_for_guest(): void
{
$plugin = Plugin::factory()->approved()->paid()->create(['is_official' => true]);
@@ -57,6 +67,71 @@ public function test_ultra_card_is_hidden_on_third_party_plugin(): void
->assertDontSee('Included with Ultra');
}
+ public function test_ultra_offer_shows_on_third_party_plugin_for_guest(): void
+ {
+ $plugin = $this->createThirdPartyPluginWithUltraPrice();
+
+ $this->get(route('plugins.show', $plugin->routeParams()))
+ ->assertStatus(200)
+ ->assertSee('Get this plugin for $70 with Ultra')
+ ->assertSee('save $29 on this plugin')
+ ->assertSee('Learn more')
+ ->assertSee(route('pricing'))
+ ->assertDontSee('Included with Ultra')
+ ->assertDontSee('pricing applied');
+ }
+
+ public function test_ultra_offer_shows_on_third_party_plugin_for_user_without_ultra(): void
+ {
+ $plugin = $this->createThirdPartyPluginWithUltraPrice();
+
+ $this->actingAs(User::factory()->create())
+ ->get(route('plugins.show', $plugin->routeParams()))
+ ->assertStatus(200)
+ ->assertSee('Get this plugin for $70 with Ultra');
+ }
+
+ public function test_ultra_subscriber_sees_the_ultra_price_instead_of_the_offer(): void
+ {
+ $user = User::factory()->create();
+ Subscription::factory()
+ ->for($user)
+ ->active()
+ ->create(['stripe_price' => self::MAX_PRICE_ID]);
+
+ $plugin = $this->createThirdPartyPluginWithUltraPrice();
+
+ $this->actingAs($user)
+ ->get(route('plugins.show', $plugin->routeParams()))
+ ->assertStatus(200)
+ ->assertDontSee('Get this plugin for')
+ ->assertSee('Ultra pricing applied')
+ ->assertSeeInOrder(['line-through', '$99', '$70'], false);
+ }
+
+ public function test_ultra_offer_is_hidden_when_third_party_plugin_has_no_ultra_price(): void
+ {
+ $plugin = Plugin::factory()->approved()->paid()->create(['is_official' => false]);
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+
+ $this->get(route('plugins.show', $plugin->routeParams()))
+ ->assertStatus(200)
+ ->assertSee('$99')
+ ->assertDontSee('Get this plugin for');
+ }
+
+ public function test_ultra_offer_is_hidden_on_first_party_plugin(): void
+ {
+ $plugin = Plugin::factory()->approved()->paid()->create(['is_official' => true]);
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->ultra()->amount(7000)->create(['plugin_id' => $plugin->id]);
+
+ $this->get(route('plugins.show', $plugin->routeParams()))
+ ->assertStatus(200)
+ ->assertSee('Included with Ultra')
+ ->assertDontSee('Get this plugin for');
+ }
+
public function test_ultra_subscriber_sees_dashboard_link(): void
{
$user = User::factory()->create();
diff --git a/tests/Feature/TierBasedPricingTest.php b/tests/Feature/TierBasedPricingTest.php
index 95145bdb9..1e52ce97f 100644
--- a/tests/Feature/TierBasedPricingTest.php
+++ b/tests/Feature/TierBasedPricingTest.php
@@ -348,6 +348,28 @@ public function subscriber_sees_subscriber_price_when_it_is_lower_than_eap_for_o
$this->assertEquals(PriceTier::Subscriber, $bestPrice->tier);
}
+ #[Test]
+ public function regular_plugin_price_wins_a_tie_with_a_discounted_tier(): void
+ {
+ $user = User::factory()->create();
+ License::factory()
+ ->for($user)
+ ->mini()
+ ->active()
+ ->eapEligible()
+ ->withoutSubscriptionItem()
+ ->create();
+
+ $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]);
+ PluginPrice::factory()->eap()->amount(2999)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]);
+
+ $bestPrice = $plugin->getBestPriceForUser($user);
+
+ $this->assertEquals(2999, $bestPrice->amount);
+ $this->assertEquals(PriceTier::Regular, $bestPrice->tier);
+ }
+
#[Test]
public function plugin_falls_back_to_regular_price_when_user_tier_not_available(): void
{
@@ -940,7 +962,7 @@ public function third_party_plugin_always_returns_regular_price_for_eap_customer
}
#[Test]
- public function third_party_plugin_always_returns_regular_price_for_max_subscriber(): void
+ public function third_party_plugin_ignores_subscriber_and_eap_prices_for_max_subscriber(): void
{
$user = User::factory()->create();
$this->createSubscription($user, self::MAX_PRICE_ID);
diff --git a/tests/Feature/UltraBenefitsPageTest.php b/tests/Feature/UltraBenefitsPageTest.php
index 950b1792b..d4db4ffd6 100644
--- a/tests/Feature/UltraBenefitsPageTest.php
+++ b/tests/Feature/UltraBenefitsPageTest.php
@@ -70,6 +70,7 @@ public function test_benefits_page_displays_expected_content(): void
$response->assertSee('Ultra');
$response->assertSee('Your premium subscription benefits');
$response->assertSee('All first-party plugins');
+ $response->assertSee('Discounted third-party plugins');
$response->assertSee('Claude Code Plugin Dev Kit');
$response->assertSee('Teams');
$response->assertSee('Premium support');
diff --git a/tests/Feature/UltraPluginAccessTest.php b/tests/Feature/UltraPluginAccessTest.php
index cd1304d19..c215d0f91 100644
--- a/tests/Feature/UltraPluginAccessTest.php
+++ b/tests/Feature/UltraPluginAccessTest.php
@@ -136,7 +136,7 @@ public function test_ultra_user_gets_subscriber_price_for_official_plugin(): voi
$this->assertEquals(1999, $bestPrice->amount);
}
- public function test_ultra_user_gets_regular_price_for_third_party_plugin(): void
+ public function test_ultra_user_does_not_get_subscriber_price_for_third_party_plugin(): void
{
$user = User::factory()->create();
$this->createPaidMaxSubscription($user);
@@ -640,7 +640,7 @@ public function test_team_member_without_own_subscription_sees_regular_price():
$this->assertEquals(4900, $bestPrice->amount);
}
- public function test_team_member_with_own_subscription_sees_regular_price_for_third_party_plugin(): void
+ public function test_team_member_with_own_subscription_does_not_get_subscriber_price_for_third_party_plugin(): void
{
$owner = User::factory()->create();
$this->createPaidMaxSubscription($owner);
diff --git a/tests/Feature/UltraPluginPricingTest.php b/tests/Feature/UltraPluginPricingTest.php
new file mode 100644
index 000000000..82370ccdb
--- /dev/null
+++ b/tests/Feature/UltraPluginPricingTest.php
@@ -0,0 +1,443 @@
+ self::ULTRA_PRICE_ID,
+ 'subscriptions.plans.max.stripe_price_id_comped' => self::COMPED_ULTRA_PRICE_ID,
+ 'subscriptions.plans.pro.stripe_price_id' => self::PRO_PRICE_ID,
+ ]);
+ }
+
+ /**
+ * @param array $attributes
+ */
+ private function createSubscriber(string $priceId, array $attributes = []): User
+ {
+ $user = User::factory()->create();
+
+ Subscription::factory()
+ ->for($user)
+ ->active()
+ ->create(['stripe_price' => $priceId, ...$attributes]);
+
+ return $user;
+ }
+
+ private function createThirdPartyPlugin(?int $ultraAmount = 7000): Plugin
+ {
+ $plugin = Plugin::factory()->approved()->paid()->create([
+ 'is_active' => true,
+ 'is_official' => false,
+ ]);
+
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+
+ if ($ultraAmount !== null) {
+ PluginPrice::factory()->ultra()->amount($ultraAmount)->create(['plugin_id' => $plugin->id]);
+ }
+
+ return $plugin;
+ }
+
+ // ---- Who is eligible for the Ultra price tier ----
+
+ public function test_ultra_subscriber_is_eligible_for_the_ultra_price_tier(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID);
+
+ $this->assertContains(PriceTier::Ultra, $user->getEligiblePriceTiers());
+ }
+
+ public function test_pro_subscriber_is_not_eligible_for_the_ultra_price_tier(): void
+ {
+ $user = $this->createSubscriber(self::PRO_PRICE_ID);
+
+ $this->assertNotContains(PriceTier::Ultra, $user->getEligiblePriceTiers());
+ }
+
+ public function test_user_without_a_subscription_is_not_eligible_for_the_ultra_price_tier(): void
+ {
+ $user = User::factory()->create();
+
+ $this->assertNotContains(PriceTier::Ultra, $user->getEligiblePriceTiers());
+ }
+
+ // ---- What each buyer pays for a third-party plugin ----
+
+ public function test_ultra_subscriber_pays_the_ultra_price_for_a_third_party_plugin(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID);
+ $plugin = $this->createThirdPartyPlugin();
+
+ $bestPrice = $plugin->getBestPriceForUser($user);
+
+ $this->assertEquals(7000, $bestPrice->amount);
+ $this->assertEquals(PriceTier::Ultra, $bestPrice->tier);
+ }
+
+ public function test_comped_ultra_subscriber_pays_the_ultra_price_for_a_third_party_plugin(): void
+ {
+ $user = $this->createSubscriber(self::COMPED_ULTRA_PRICE_ID);
+ $plugin = $this->createThirdPartyPlugin();
+
+ $this->assertEquals(7000, $plugin->getBestPriceForUser($user)->amount);
+ }
+
+ public function test_ultra_subscriber_with_extra_seats_pays_the_ultra_price_for_a_third_party_plugin(): void
+ {
+ config(['subscriptions.plans.max.stripe_extra_seat_price_id' => 'price_test_extra_seat']);
+
+ $user = User::factory()->create();
+ $subscription = Subscription::factory()
+ ->for($user)
+ ->active()
+ ->create(['stripe_price' => null]);
+ SubscriptionItem::factory()
+ ->for($subscription, 'subscription')
+ ->create(['stripe_price' => self::ULTRA_PRICE_ID, 'quantity' => 1]);
+ SubscriptionItem::factory()
+ ->for($subscription, 'subscription')
+ ->create(['stripe_price' => 'price_test_extra_seat', 'quantity' => 2]);
+
+ $plugin = $this->createThirdPartyPlugin();
+
+ $this->assertEquals(7000, $plugin->getBestPriceForUser($user)->amount);
+ }
+
+ public function test_guest_pays_the_regular_price_for_a_third_party_plugin(): void
+ {
+ $plugin = $this->createThirdPartyPlugin();
+
+ $bestPrice = $plugin->getBestPriceForUser(null);
+
+ $this->assertEquals(9900, $bestPrice->amount);
+ $this->assertEquals(PriceTier::Regular, $bestPrice->tier);
+ }
+
+ public function test_user_without_a_subscription_pays_the_regular_price_for_a_third_party_plugin(): void
+ {
+ $user = User::factory()->create();
+ $plugin = $this->createThirdPartyPlugin();
+
+ $this->assertEquals(9900, $plugin->getBestPriceForUser($user)->amount);
+ }
+
+ public function test_pro_subscriber_pays_the_regular_price_for_a_third_party_plugin(): void
+ {
+ $user = $this->createSubscriber(self::PRO_PRICE_ID);
+ $plugin = $this->createThirdPartyPlugin();
+
+ $this->assertEquals(9900, $plugin->getBestPriceForUser($user)->amount);
+ }
+
+ public function test_legacy_comped_max_subscriber_pays_the_regular_price_for_a_third_party_plugin(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID, ['is_comped' => true]);
+ $plugin = $this->createThirdPartyPlugin();
+
+ $this->assertEquals(9900, $plugin->getBestPriceForUser($user)->amount);
+ }
+
+ public function test_user_whose_ultra_subscription_has_ended_pays_the_regular_price_for_a_third_party_plugin(): void
+ {
+ $user = User::factory()->create();
+ Subscription::factory()
+ ->for($user)
+ ->canceled()
+ ->create(['stripe_price' => self::ULTRA_PRICE_ID]);
+ $plugin = $this->createThirdPartyPlugin();
+
+ $this->assertEquals(9900, $plugin->getBestPriceForUser($user)->amount);
+ }
+
+ public function test_team_member_without_their_own_ultra_subscription_pays_the_regular_price_for_a_third_party_plugin(): void
+ {
+ $owner = $this->createSubscriber(self::ULTRA_PRICE_ID);
+ $team = Team::factory()->create(['user_id' => $owner->id]);
+ $member = User::factory()->create();
+ TeamUser::factory()->active()->create([
+ 'team_id' => $team->id,
+ 'user_id' => $member->id,
+ 'email' => $member->email,
+ ]);
+ $plugin = $this->createThirdPartyPlugin();
+
+ $this->assertEquals(9900, $plugin->getBestPriceForUser($member)->amount);
+ }
+
+ public function test_ultra_subscriber_pays_the_regular_price_when_the_plugin_has_no_ultra_price(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID);
+ $plugin = $this->createThirdPartyPlugin(ultraAmount: null);
+
+ $bestPrice = $plugin->getBestPriceForUser($user);
+
+ $this->assertEquals(9900, $bestPrice->amount);
+ $this->assertEquals(PriceTier::Regular, $bestPrice->tier);
+ }
+
+ public function test_ultra_subscriber_pays_the_regular_price_when_the_ultra_price_is_inactive(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID);
+ $plugin = $this->createThirdPartyPlugin(ultraAmount: null);
+ PluginPrice::factory()->ultra()->inactive()->amount(7000)->create(['plugin_id' => $plugin->id]);
+
+ $this->assertEquals(9900, $plugin->getBestPriceForUser($user)->amount);
+ }
+
+ public function test_regular_price_wins_when_the_ultra_price_is_not_cheaper(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID);
+
+ $plugin = Plugin::factory()->approved()->paid()->create([
+ 'is_active' => true,
+ 'is_official' => false,
+ ]);
+ PluginPrice::factory()->ultra()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+
+ $bestPrice = $plugin->getBestPriceForUser($user);
+
+ $this->assertEquals(9900, $bestPrice->amount);
+ $this->assertEquals(PriceTier::Regular, $bestPrice->tier);
+ }
+
+ public function test_official_plugin_never_uses_an_ultra_price(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID);
+
+ $plugin = Plugin::factory()->approved()->paid()->create([
+ 'is_active' => true,
+ 'is_official' => true,
+ ]);
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->subscriber()->amount(3100)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->ultra()->amount(2000)->create(['plugin_id' => $plugin->id]);
+
+ $bestPrice = $plugin->getBestPriceForUser($user);
+
+ $this->assertEquals(3100, $bestPrice->amount);
+ $this->assertEquals(PriceTier::Subscriber, $bestPrice->tier);
+ }
+
+ // ---- The Ultra price shown to everyone else ----
+
+ public function test_ultra_price_is_available_for_a_paid_third_party_plugin(): void
+ {
+ $plugin = $this->createThirdPartyPlugin();
+
+ $this->assertEquals(7000, $plugin->getUltraPrice()->amount);
+ }
+
+ public function test_ultra_price_is_null_when_the_plugin_has_none(): void
+ {
+ $plugin = $this->createThirdPartyPlugin(ultraAmount: null);
+
+ $this->assertNull($plugin->getUltraPrice());
+ }
+
+ public function test_ultra_price_is_null_when_it_is_not_lower_than_the_regular_price(): void
+ {
+ $plugin = $this->createThirdPartyPlugin(ultraAmount: 9900);
+
+ $this->assertNull($plugin->getUltraPrice());
+ }
+
+ public function test_ultra_price_is_null_for_an_official_plugin(): void
+ {
+ $plugin = Plugin::factory()->approved()->paid()->create(['is_official' => true]);
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->ultra()->amount(7000)->create(['plugin_id' => $plugin->id]);
+
+ $this->assertNull($plugin->getUltraPrice());
+ }
+
+ public function test_ultra_price_is_null_for_a_free_plugin(): void
+ {
+ $plugin = Plugin::factory()->approved()->free()->create(['is_official' => false]);
+ PluginPrice::factory()->regular()->amount(9900)->create(['plugin_id' => $plugin->id]);
+ PluginPrice::factory()->ultra()->amount(7000)->create(['plugin_id' => $plugin->id]);
+
+ $this->assertNull($plugin->getUltraPrice());
+ }
+
+ // ---- Ultra prices come from the plugin's pricing tier ----
+
+ #[DataProvider('pricesByPluginTier')]
+ public function test_setting_a_tier_on_a_third_party_plugin_creates_its_ultra_price(PluginTier $tier, int $regularAmount, int $ultraAmount): void
+ {
+ $plugin = Plugin::factory()->paid()->create(['is_official' => false]);
+
+ $plugin->update(['tier' => $tier]);
+
+ $this->assertSame($regularAmount, $plugin->prices()->forTier(PriceTier::Regular)->sole()->amount);
+ $this->assertSame($ultraAmount, $plugin->prices()->forTier(PriceTier::Ultra)->sole()->amount);
+ }
+
+ /**
+ * @return array
+ */
+ public static function pricesByPluginTier(): array
+ {
+ return [
+ 'bronze' => [PluginTier::Bronze, 2900, 2000],
+ 'silver' => [PluginTier::Silver, 4900, 3500],
+ 'gold' => [PluginTier::Gold, 9900, 7000],
+ ];
+ }
+
+ public function test_changing_the_tier_updates_the_ultra_price(): void
+ {
+ $plugin = Plugin::factory()->paid()->create(['is_official' => false]);
+ $plugin->update(['tier' => PluginTier::Gold]);
+
+ $plugin->update(['tier' => PluginTier::Silver]);
+
+ $this->assertSame(3500, $plugin->prices()->forTier(PriceTier::Ultra)->sole()->amount);
+ }
+
+ public function test_setting_a_tier_on_an_official_plugin_does_not_create_an_ultra_price(): void
+ {
+ $plugin = Plugin::factory()->paid()->create(['is_official' => true]);
+
+ $plugin->update(['tier' => PluginTier::Gold]);
+
+ $this->assertTrue($plugin->prices()->forTier(PriceTier::Regular)->exists());
+ $this->assertFalse($plugin->prices()->forTier(PriceTier::Ultra)->exists());
+ }
+
+ // ---- The cart charges the price the buyer is entitled to ----
+
+ public function test_cart_adds_a_third_party_plugin_at_the_ultra_price_for_an_ultra_subscriber(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID);
+ $cart = Cart::factory()->for($user)->create();
+ $plugin = $this->createThirdPartyPlugin();
+
+ $item = (new CartService)->addPlugin($cart, $plugin);
+
+ $this->assertEquals(7000, $item->price_at_addition);
+ $this->assertEquals(PriceTier::Ultra, $item->pluginPrice->tier);
+ }
+
+ public function test_cart_adds_a_third_party_plugin_at_the_regular_price_for_everyone_else(): void
+ {
+ $user = $this->createSubscriber(self::PRO_PRICE_ID);
+ $cart = Cart::factory()->for($user)->create();
+ $plugin = $this->createThirdPartyPlugin();
+
+ $item = (new CartService)->addPlugin($cart, $plugin);
+
+ $this->assertEquals(9900, $item->price_at_addition);
+ $this->assertEquals(PriceTier::Regular, $item->pluginPrice->tier);
+ }
+
+ public function test_cart_drops_to_the_ultra_price_once_the_buyer_subscribes_to_ultra(): void
+ {
+ $user = User::factory()->create();
+ $cart = Cart::factory()->for($user)->create();
+ $plugin = $this->createThirdPartyPlugin();
+
+ $cartService = new CartService;
+ $item = $cartService->addPlugin($cart, $plugin);
+ $this->assertEquals(9900, $item->price_at_addition);
+
+ Subscription::factory()
+ ->for($user)
+ ->active()
+ ->create(['stripe_price' => self::ULTRA_PRICE_ID]);
+
+ $changes = $cartService->refreshPrices($cart->fresh());
+
+ $this->assertCount(1, $changes);
+ $this->assertEquals(9900, $changes[0]['old_price']);
+ $this->assertEquals(7000, $changes[0]['new_price']);
+
+ $item->refresh();
+ $this->assertEquals(7000, $item->price_at_addition);
+ $this->assertEquals(PriceTier::Ultra, $item->pluginPrice->tier);
+ }
+
+ public function test_cart_returns_to_the_regular_price_once_the_ultra_subscription_ends(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID);
+ $cart = Cart::factory()->for($user)->create();
+ $plugin = $this->createThirdPartyPlugin();
+
+ $cartService = new CartService;
+ $item = $cartService->addPlugin($cart, $plugin);
+ $this->assertEquals(7000, $item->price_at_addition);
+
+ $user->subscription()->update(['stripe_status' => 'canceled', 'ends_at' => now()]);
+
+ $cartService->refreshPrices($cart->fresh());
+
+ $item->refresh();
+ $this->assertEquals(9900, $item->price_at_addition);
+ $this->assertEquals(PriceTier::Regular, $item->pluginPrice->tier);
+ }
+
+ public function test_cart_page_labels_a_plugin_sold_at_the_ultra_price(): void
+ {
+ $user = $this->createSubscriber(self::ULTRA_PRICE_ID);
+ $plugin = $this->createThirdPartyPlugin();
+
+ $cartService = resolve(CartService::class);
+ $cartService->addPlugin($cartService->getCart($user), $plugin);
+
+ $this->actingAs($user)
+ ->get(route('cart.show'))
+ ->assertOk()
+ ->assertSee('$70')
+ ->assertSee('Ultra price');
+ }
+
+ public function test_cart_page_does_not_label_a_plugin_sold_at_the_regular_price(): void
+ {
+ $user = User::factory()->create();
+ $plugin = $this->createThirdPartyPlugin();
+
+ $cartService = resolve(CartService::class);
+ $cartService->addPlugin($cartService->getCart($user), $plugin);
+
+ $this->actingAs($user)
+ ->get(route('cart.show'))
+ ->assertOk()
+ ->assertSee('$99')
+ ->assertDontSee('Ultra price');
+ }
+}