'test-token', 'services.moneybird.administration_id' => '123456', ]); }); test('switching from free to starter creates a contact, invoice, and recurring template', function () { Http::fake([ '*/contacts/customer_id/*' => Http::response('', 404), '*/contacts.json' => Http::response(['id' => 'contact-1'], 201), '*/sales_invoices.json' => Http::response([ 'id' => 'invoice-1', 'state' => 'open', 'invoice_date' => today()->toDateString(), 'due_date' => today()->addDays(14)->toDateString(), 'payment_url' => 'https://moneybird.dev/pay/invoice-1', ], 201), '*/recurring_sales_invoices.json' => Http::response(['id' => 'recurring-1'], 201), ]); $user = User::factory()->create(); $subscription = app(SubscriptionManager::class)->changePlan($user, SubscriptionPlan::Starter); expect($subscription->plan)->toBe(SubscriptionPlan::Starter) ->and($subscription->status)->toBe(SubscriptionStatus::AwaitingPayment) ->and($subscription->moneybird_contact_id)->toBe('contact-1') ->and($subscription->moneybird_recurring_invoice_id)->toBe('recurring-1') ->and($subscription->current_period_starts_at->toDateString())->toBe(today()->toDateString()) ->and($subscription->current_period_ends_at->toDateString())->toBe(today()->addYear()->toDateString()); $invoice = SubscriptionInvoice::sole(); expect($invoice->moneybird_invoice_id)->toBe('invoice-1') ->and($invoice->amount_cents)->toBe(SubscriptionPlan::Starter->priceInCents()) ->and($invoice->payment_url)->toBe('https://moneybird.dev/pay/invoice-1'); Http::assertSent(fn ($request) => str_contains((string) $request->url(), '/contacts.json') && $request['contact']['customer_id'] === (string) $user->id); }); test('an existing moneybird contact is reused instead of creating a duplicate', function () { Http::fake([ '*/contacts/customer_id/*' => Http::response(['id' => 'existing-contact'], 200), '*/sales_invoices.json' => Http::response(['id' => 'invoice-1', 'state' => 'open'], 201), '*/recurring_sales_invoices.json' => Http::response(['id' => 'recurring-1'], 201), ]); $user = User::factory()->create(); $subscription = app(SubscriptionManager::class)->changePlan($user, SubscriptionPlan::Starter); expect($subscription->moneybird_contact_id)->toBe('existing-contact'); Http::assertNotSent(fn ($request) => str_contains((string) $request->url(), '/contacts.json') && $request->method() === 'POST'); }); test('upgrading between paid plans bills a prorated top-up and repoints the recurring template', function () { Http::fake([ '*/sales_invoices.json' => Http::response([ 'id' => 'invoice-2', 'state' => 'open', 'invoice_date' => today()->toDateString(), ], 201), '*/recurring_sales_invoices/recurring-1.json' => Http::response(['id' => 'recurring-1'], 200), ]); $user = User::factory()->create(); $subscription = Subscription::factory()->for($user)->starter()->create([ 'moneybird_contact_id' => 'contact-1', 'moneybird_recurring_invoice_id' => 'recurring-1', 'current_period_starts_at' => today(), 'current_period_ends_at' => today()->addYear(), ]); $updated = app(SubscriptionManager::class)->changePlan($user, SubscriptionPlan::Unlimited); expect($updated->plan)->toBe(SubscriptionPlan::Unlimited) ->and($updated->status)->toBe(SubscriptionStatus::AwaitingPayment) // Full year remaining, so the prorated top-up is the full price diff. ->and($updated->current_period_ends_at->toDateString())->toBe($subscription->current_period_ends_at->toDateString()); $invoice = SubscriptionInvoice::sole(); expect($invoice->amount_cents)->toBe(SubscriptionPlan::Unlimited->priceInCents() - SubscriptionPlan::Starter->priceInCents()); Http::assertSent(fn ($request) => str_contains((string) $request->url(), '/recurring_sales_invoices/recurring-1.json') && $request->method() === 'PATCH'); }); test('downgrading to a cheaper paid plan does not create an invoice', function () { Http::fake([ '*/recurring_sales_invoices/recurring-1.json' => Http::response(['id' => 'recurring-1'], 200), ]); $user = User::factory()->create(); Subscription::factory()->for($user)->unlimited()->create([ 'moneybird_recurring_invoice_id' => 'recurring-1', 'current_period_starts_at' => today(), 'current_period_ends_at' => today()->addYear(), ]); $updated = app(SubscriptionManager::class)->changePlan($user, SubscriptionPlan::Starter); expect($updated->plan)->toBe(SubscriptionPlan::Starter); expect(SubscriptionInvoice::count())->toBe(0); Http::assertNotSent(fn ($request) => str_contains((string) $request->url(), '/sales_invoices.json')); }); test('switching to free deletes the recurring template and resets the subscription', function () { Http::fake([ '*/recurring_sales_invoices/recurring-1.json' => Http::response('', 204), ]); $user = User::factory()->create(); Subscription::factory()->for($user)->starter()->create([ 'moneybird_recurring_invoice_id' => 'recurring-1', ]); $subscription = app(SubscriptionManager::class)->changePlan($user, SubscriptionPlan::Free); expect($subscription->plan)->toBe(SubscriptionPlan::Free) ->and($subscription->status)->toBe(SubscriptionStatus::Active) ->and($subscription->moneybird_recurring_invoice_id)->toBeNull() ->and($subscription->current_period_ends_at)->toBeNull(); Http::assertSent(fn ($request) => str_contains((string) $request->url(), '/recurring_sales_invoices/recurring-1.json') && $request->method() === 'DELETE'); });