internal_note = 'host boot hook ran'; }); static::created(function () { static::$bootHookRuns++; }); } } /** * `shop.models.order` is honoured on the real Stripe webhook path and by the * ShopService / Cart finders and creators that previously used `Order::`. */ class StripeWebhookOrderModelTest extends TestCase { use RefreshDatabase; protected StripeWebhookController $controller; protected function setUp(): void { parent::setUp(); config([ 'shop.stripe.enabled' => true, 'shop.models.order' => HostWebhookOrder::class, ]); HostWebhookOrder::$bootHookRuns = 0; $this->controller = new StripeWebhookController; } protected function invoke(string $method, array $args = []) { $ref = (new ReflectionClass($this->controller))->getMethod($method); $ref->setAccessible(true); return $ref->invokeArgs($this->controller, $args); } protected function mockSession(array $overrides = []): object { return (object) array_merge([ 'id' => 'cs_test_'.uniqid(), 'payment_intent' => 'pi_test_'.uniqid(), 'metadata' => (object) ['cart_id' => null], 'client_reference_id' => null, 'amount_total' => 10000, 'currency' => 'eur', 'payment_status' => 'paid', 'customer' => 'cus_test_123', ], $overrides); } protected function product(int $price = 10000): Product { return Product::factory()->withPrices(unit_amount: $price)->create(['manage_stock' => false]); } #[Test] public function checkout_session_completed_builds_the_configured_order_subclass(): void { $customer = User::factory()->create(); $customer->addToCart($this->product(10000)); $cart = $customer->currentCart(); $this->assertEquals(CartStatus::ACTIVE, $cart->status); $this->assertNull($cart->order); Event::fake([OrderPaid::class]); $result = $this->invoke('handleCheckoutSessionCompleted', [ $this->mockSession(['metadata' => (object) ['cart_id' => $cart->id], 'amount_total' => 10000]), ]); $this->assertTrue($result); $order = $cart->fresh()->order; $this->assertInstanceOf(HostWebhookOrder::class, $order); $this->assertSame('host boot hook ran', $order->internal_note, 'the subclass creating hook must have run'); $this->assertSame(1, HostWebhookOrder::$bootHookRuns); $this->assertSame(1, HostWebhookOrder::count()); $this->assertEquals(10000, $order->amount_paid); $this->assertNotNull($order->paid_at); Event::assertDispatchedTimes(OrderPaid::class, 1); Event::assertDispatched( OrderPaid::class, fn (OrderPaid $e) => $e->order instanceof HostWebhookOrder && $e->order->is($order) ); } #[Test] public function webhook_finders_return_the_configured_order_subclass(): void { $order = HostWebhookOrder::create(Order::factory()->pending()->raw([ 'payment_reference' => 'pi_host_ref', ])); $byIntent = $this->invoke('findOrderByPaymentIntent', ['pi_host_ref']); $byCharge = $this->invoke('findOrderByChargeId', ['pi_host_ref']); $this->assertInstanceOf(HostWebhookOrder::class, $byIntent); $this->assertInstanceOf(HostWebhookOrder::class, $byCharge); $this->assertTrue($byIntent->is($order)); } #[Test] public function shop_service_finders_return_the_configured_order_subclass(): void { $order = HostWebhookOrder::create(Order::factory()->pending()->raw()); $service = new ShopService; $this->assertInstanceOf(HostWebhookOrder::class, $service->order($order->id)); $this->assertInstanceOf(HostWebhookOrder::class, $service->orderByNumber($order->order_number)); $this->assertInstanceOf(HostWebhookOrder::class, $service->orders()->first()); $this->assertInstanceOf(HostWebhookOrder::class, $service->pendingOrders()->first()); $this->assertSame(1, $service->ordersToday()->count()); } #[Test] public function cart_checkout_builds_the_configured_order_subclass(): void { $customer = User::factory()->create(); $customer->addToCart($this->product(5000)); $cart = $customer->checkoutCart(); $order = $cart->fresh()->order; $this->assertInstanceOf(HostWebhookOrder::class, $order); $this->assertSame('host boot hook ran', $order->internal_note); } }