137 lines
6.1 KiB
PHP
137 lines
6.1 KiB
PHP
<?php
|
|
|
|
use App\Enums\SubscriptionPlan;
|
|
use App\Enums\SubscriptionStatus;
|
|
use App\Models\Subscription;
|
|
use App\Models\SubscriptionInvoice;
|
|
use App\Models\User;
|
|
use App\Services\Billing\SubscriptionManager;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
beforeEach(function () {
|
|
config([
|
|
'services.moneybird.api_token' => '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');
|
|
});
|