userId = (string) User::factory()->create()->getKey(); } private function product(string $name): Product { return Product::create([ 'name' => $name, 'sku' => 'SKU-'.uniqid(), 'type' => ProductType::SIMPLE, 'status' => ProductStatus::PUBLISHED, 'manage_stock' => false, ]); } private function price(Product $product, string $stripePrice, int $unitAmount, RecurringInterval|string|null $interval, int $intervalCount = 1): ProductPrice { return ProductPrice::create([ 'purchasable_type' => Product::class, 'purchasable_id' => $product->id, 'stripe_price_id' => $stripePrice, 'type' => $interval === null ? PriceType::ONE_TIME : PriceType::RECURRING, 'currency' => 'EUR', 'unit_amount' => $unitAmount, 'is_default' => true, 'active' => true, 'interval' => $interval, 'interval_count' => $intervalCount, ]); } private function subscription(Product $product, string $status, string $stripePrice, ?\DateTimeInterface $endsAt = null): Subscription { $sub = Subscription::create([ 'user_id' => $this->userId, 'product_id' => $product->id, 'type' => 'default', 'stripe_id' => 'sub_'.uniqid(), 'stripe_status' => $status, 'stripe_price' => $stripePrice, 'quantity' => 1, 'ends_at' => $endsAt, ]); SubscriptionItem::create([ 'subscription_id' => $sub->id, 'stripe_id' => 'si_'.uniqid(), 'stripe_product' => 'prod_x', 'stripe_price' => $stripePrice, 'quantity' => 1, ]); return $sub; } #[Test] public function it_counts_live_subscribers_including_past_due_and_cancel_at_period_end(): void { $seat = $this->product('Full Seat'); $this->price($seat, 'price_seat', 2550, RecurringInterval::MONTH); $this->subscription($seat, 'active', 'price_seat'); $this->subscription($seat, 'past_due', 'price_seat'); // still billing -> counts $this->subscription($seat, 'active', 'price_seat', now()->addMonth()); // cancel-at-period-end -> counts $this->subscription($seat, 'trialing', 'price_seat'); // counts $this->subscription($seat, 'canceled', 'price_seat'); // gone -> excluded $this->assertSame(4, Shop::activeSubscribers()); $this->assertSame(4 * 2550, Shop::mrr()); } #[Test] public function it_normalizes_every_interval_to_a_monthly_run_rate(): void { $p = $this->product('Simulator'); $this->price($p, 'price_month', 1410, RecurringInterval::MONTH); $this->price($p, 'price_year', 13500, RecurringInterval::YEAR); // ÷12 $this->price($p, 'price_week', 610, RecurringInterval::WEEK); // ×30.436875/7 $this->price($p, 'price_quarter', 4900, RecurringInterval::MONTH, 3); // ÷3 $this->subscription($p, 'active', 'price_month'); $this->subscription($p, 'active', 'price_year'); $this->subscription($p, 'active', 'price_week'); $this->subscription($p, 'active', 'price_quarter'); $expected = 1410 + (int) round(13500 / 12) + (int) round(610 * 30.436875 / 7) + (int) round(4900 / 3); $this->assertSame($expected, Shop::mrr()); } #[Test] public function it_surfaces_line_items_whose_price_is_not_mirrored_rather_than_guessing(): void { $p = $this->product('Full Seat'); $this->price($p, 'price_seat', 2550, RecurringInterval::MONTH); $this->subscription($p, 'active', 'price_seat'); $this->subscription($p, 'active', 'price_missing'); // no ProductPrice mirrored $metrics = Shop::subscriptionMetrics(); $this->assertSame(2550, $metrics['mrr']); // unmirrored item excluded, never guessed $this->assertSame(1, $metrics['unpriced_items']); // but surfaced, not silent $this->assertSame(2, $metrics['subscribers']); } #[Test] public function it_splits_mrr_by_product(): void { $seat = $this->product('Full Seat'); $sim = $this->product('Simulator'); $this->price($seat, 'price_seat', 2550, RecurringInterval::MONTH); $this->price($sim, 'price_sim', 890, RecurringInterval::MONTH); $this->subscription($seat, 'active', 'price_seat'); $this->subscription($seat, 'active', 'price_seat'); $this->subscription($sim, 'active', 'price_sim'); $byProduct = Shop::mrrByProduct(); $this->assertSame(Shop::mrr(), $byProduct->sum()); $this->assertCount(2, $byProduct); // arsort() puts the biggest product first; values are label-independent. $this->assertSame([5100, 890], array_values($byProduct->all())); } }