CluPilotCloud/tests/Feature/Billing/GrantSubscriptionTest.php

126 lines
4.5 KiB
PHP

<?php
use App\Actions\GrantSubscription;
use App\Models\Customer;
use App\Models\Operator;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Models\Subscription;
use App\Provisioning\Jobs\AdvanceRunJob;
use App\Services\Billing\PlanCatalogue;
use Illuminate\Support\Facades\Queue;
/**
* A grant is an order without money, not a second code path: it must produce
* the same rows — order, provisioning run, subscription — a paid purchase
* does, with provenance recorded and price_cents set to what the customer
* actually pays.
*/
it('opens a subscription with no money charged, and provenance recorded', function () {
Queue::fake();
$customer = Customer::factory()->create();
$owner = Operator::factory()->create();
$subscription = app(GrantSubscription::class)(
customer: $customer,
grantedBy: $owner,
plan: 'team',
term: Subscription::TERM_MONTHLY,
datacenter: 'fsn',
priceCents: 0,
note: 'Freund des Hauses',
);
$order = Order::query()->where('customer_id', $customer->id)->sole();
expect($order->amount_cents)->toBe(0)
->and($order->stripe_event_id)->toBeNull()
->and($subscription->price_cents)->toBe(0)
->and($subscription->plan)->toBe('team')
// The customer is owed the real machine, so the snapshot is the
// catalogue's — only the price and the provenance were overridden.
->and($subscription->ram_mb)->toBeGreaterThan(0)
->and($subscription->isGranted())->toBeTrue()
->and($subscription->isFreeGrant())->toBeTrue()
->and($subscription->granted_by)->toBe($owner->id)
->and($subscription->grant_note)->toBe('Freund des Hauses')
->and($subscription->granted_until)->toBeNull()
->and($subscription->catalogue_price_cents)->toBeGreaterThan(0);
// Same downstream pipeline as a paid order: a run was opened and dispatched.
$run = ProvisioningRun::query()->where('subject_id', $order->id)->sole();
expect($run->pipeline)->toBe('customer');
Queue::assertPushed(AdvanceRunJob::class, fn ($job) => $job->runUuid === $run->uuid);
});
it('records a discount as what it is: a price below the catalogue, not a free plan', function () {
$customer = Customer::factory()->create();
$owner = Operator::factory()->create();
$subscription = app(GrantSubscription::class)(
customer: $customer,
grantedBy: $owner,
plan: 'team',
term: Subscription::TERM_MONTHLY,
datacenter: 'fsn',
priceCents: 5000,
);
expect($subscription->price_cents)->toBe(5000)
->and($subscription->catalogue_price_cents)->toBeGreaterThan(5000)
->and($subscription->isFreeGrant())->toBeFalse()
->and($subscription->isGranted())->toBeTrue();
});
it('grants a quota bonus beyond the plan, baked into the frozen snapshot', function () {
$customer = Customer::factory()->create();
$owner = Operator::factory()->create();
$plainTeam = app(PlanCatalogue::class)->currentVersion('team');
$subscription = app(GrantSubscription::class)(
customer: $customer,
grantedBy: $owner,
plan: 'team',
term: Subscription::TERM_MONTHLY,
datacenter: 'fsn',
priceCents: 0,
bonus: ['quota_gb' => $plainTeam->quota_gb + 250],
);
expect($subscription->quota_gb)->toBe($plainTeam->quota_gb + 250);
});
it('refuses to charge more for a grant than the plan actually costs', function () {
$customer = Customer::factory()->create();
$owner = Operator::factory()->create();
$price = app(PlanCatalogue::class)->currentVersion('team')->priceFor('monthly')->amount_cents;
expect(fn () => app(GrantSubscription::class)(
customer: $customer,
grantedBy: $owner,
plan: 'team',
term: Subscription::TERM_MONTHLY,
datacenter: 'fsn',
priceCents: $price + 1,
))->toThrow(RuntimeException::class);
});
it('remembers until when a dated grant runs, and leaves it unlimited otherwise', function () {
$customer = Customer::factory()->create();
$owner = Operator::factory()->create();
$until = now()->addMonths(6);
$subscription = app(GrantSubscription::class)(
customer: $customer,
grantedBy: $owner,
plan: 'team',
term: Subscription::TERM_MONTHLY,
datacenter: 'fsn',
priceCents: 0,
until: $until,
);
expect($subscription->granted_until->toDateString())->toBe($until->toDateString());
});