stripe = new FakeStripeClient; app()->instance(StripeClient::class, $this->stripe); // Every test here calls Billing::purchase() to place the order it then // marks paid by hand. Since Task 5, that call refuses outright without a // Stripe key for the active mode — binding the fake StripeClient above // does not help, because the guard reads the vault directly rather than // going through the StripeClient interface. withStripeSecret(); }); /** A customer on a live contract, with a machine. */ function paidOrderCustomer(string $plan = 'business'): array { $customer = Customer::factory()->create(); $user = User::factory()->create(['email' => $customer->email]); $order = Order::factory()->withSubscription()->for($customer)->create(['plan' => $plan]); $instance = Instance::factory()->for($customer)->create([ 'order_id' => $order->id, 'plan' => $plan, 'status' => 'active', ]); $subscription = $order->subscription; $subscription->update(['instance_id' => $instance->id, 'stripe_subscription_id' => 'sub_'.$customer->id]); return [$customer, $user, $subscription->fresh()]; } it('books the module a paid order bought', function () { Queue::fake(); [, $user, $subscription] = paidOrderCustomer(); Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'addon', 'collabora_pro'); $order = Order::query()->where('type', 'addon')->sole(); expect(SubscriptionAddon::query()->count())->toBe(0); $order->update(['status' => 'paid']); $addon = SubscriptionAddon::query()->sole(); expect($addon->addon_key)->toBe('collabora_pro') ->and($addon->order_id)->toBe($order->id) // Frozen at the price the customer was shown, and entered in the register. ->and($addon->price_cents)->toBe(1900) ->and(SubscriptionRecord::query()->where('event', SubscriptionRecord::EVENT_ADDON_BOOKED)->count())->toBe(1) // And it is on the Stripe subscription, which is what makes it recur. ->and($this->stripe->subscriptionItems)->toHaveCount(1); // Delivered once, however often the row is touched again. $order->update(['status' => 'paid', 'updated_at' => now()->addMinute()]); expect(SubscriptionAddon::query()->count())->toBe(1); }); it('books the storage pack a paid order bought, one per order', function () { Queue::fake(); [, $user, $subscription] = paidOrderCustomer(); // Two packs is two orders, because that is how the shop writes them. Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'storage', null, 2); Order::query()->where('type', 'storage')->get()->each->update(['status' => 'paid']); $packs = SubscriptionAddon::query()->where('addon_key', AddonCatalogue::STORAGE)->get(); expect($packs)->toHaveCount(2) ->and($packs->pluck('order_id')->unique())->toHaveCount(2); }); it('delivers nothing for a downgrade, which is never paid for', function () { Queue::fake(); [, $user, $subscription] = paidOrderCustomer(); Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'downgrade', 'team'); // A downgrade lands at the end of the term from the date stamped on the // contract. Marked paid by hand it must not move the package today. Order::query()->where('type', 'downgrade')->sole()->update(['status' => 'paid']); expect($subscription->fresh()->plan)->toBe('business') ->and($subscription->fresh()->pending_plan)->toBe('team'); }); it('says out loud that a paid traffic pack has nowhere to go', function () { Queue::fake(); [, $user] = paidOrderCustomer(); Log::spy(); Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'traffic'); Order::query()->where('type', 'traffic')->sole()->update(['status' => 'paid']); // The metered allowance is a standing count of packs on the instance, while a // top-up is sold as a one-off for the month running out — so raising it would // hand over the extra thousand gigabytes every month for ever off one payment, // and doing nothing leaves the customer paying for nothing. Which of the two // is meant is a commercial decision, and until it is made the order has to be // impossible to miss. Log::shouldHaveReceived('error') ->withArgs(fn (string $message) => str_contains($message, 'no fulfilment path')) ->once(); }); it('never lets a failed delivery undo the record that money changed hands', function () { Queue::fake(); [$customer, $user] = paidOrderCustomer(); Livewire::actingAs($user)->test(Billing::class)->call('purchase', 'addon', 'collabora_pro'); $order = Order::query()->where('type', 'addon')->sole(); // The contract goes before the order is settled. Booking will refuse, and the // order must still be paid: that write is the evidence of the payment, and // rolling it back would erase the payment and leave nothing to retry from. Subscription::query()->where('customer_id', $customer->id)->delete(); $order->update(['status' => 'paid']); expect($order->fresh()->status)->toBe('paid') ->and(SubscriptionAddon::query()->count())->toBe(0); });