105 lines
3.8 KiB
PHP
105 lines
3.8 KiB
PHP
<?php
|
|
|
|
use App\Actions\GrantAddon;
|
|
use App\Actions\GrantSubscription;
|
|
use App\Actions\OpenSubscription;
|
|
use App\Models\Customer;
|
|
use App\Models\Invoice;
|
|
use App\Models\InvoiceSeries;
|
|
use App\Models\Operator;
|
|
use App\Models\Order;
|
|
use App\Models\Subscription;
|
|
use App\Services\Billing\IssueInvoice;
|
|
use App\Support\CompanyProfile;
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
/**
|
|
* The other half of the owner's second decision: a full gift produces no
|
|
* invoice at all, but a discounted grant is invoiced normally, at what the
|
|
* customer actually pays. This is the one place IssueInvoice had to change —
|
|
* a single reject() of free-grant orders — and this file is what proves it.
|
|
*/
|
|
beforeEach(function () {
|
|
CompanyProfile::put([
|
|
'name' => 'CluPilot Cloud e.U.',
|
|
'address' => 'Dreherstraße 66/1/8',
|
|
'postcode' => '1110',
|
|
'city' => 'Wien',
|
|
'vat_id' => 'ATU00000000',
|
|
]);
|
|
});
|
|
|
|
it('skips a fully granted subscription: no invoice, no number consumed', function () {
|
|
Queue::fake();
|
|
|
|
$customer = Customer::factory()->create();
|
|
$owner = Operator::factory()->create();
|
|
|
|
app(GrantSubscription::class)(
|
|
customer: $customer, grantedBy: $owner, plan: 'team',
|
|
term: Subscription::TERM_MONTHLY, datacenter: 'fsn', priceCents: 0,
|
|
);
|
|
|
|
$order = Order::query()->where('customer_id', $customer->id)->sole();
|
|
|
|
expect(fn () => app(IssueInvoice::class)->forOrders($customer, collect([$order])))
|
|
->toThrow(RuntimeException::class);
|
|
|
|
expect(Invoice::query()->count())->toBe(0)
|
|
->and(InvoiceSeries::query()->where('kind', 'invoice')->value('next_number'))->toBe(1);
|
|
});
|
|
|
|
it('invoices a discounted grant normally, at what the customer actually pays', function () {
|
|
Queue::fake();
|
|
|
|
$customer = Customer::factory()->create();
|
|
$owner = Operator::factory()->create();
|
|
|
|
app(GrantSubscription::class)(
|
|
customer: $customer, grantedBy: $owner, plan: 'team',
|
|
term: Subscription::TERM_MONTHLY, datacenter: 'fsn', priceCents: 5000,
|
|
);
|
|
|
|
$order = Order::query()->where('customer_id', $customer->id)->sole();
|
|
$invoice = app(IssueInvoice::class)->forOrders($customer, collect([$order]));
|
|
|
|
expect($invoice->net_cents)->toBe(5000)
|
|
->and($invoice->number)->toStartWith('RE-');
|
|
});
|
|
|
|
it('skips a granted, free module the same way', function () {
|
|
$order = Order::factory()->create(['plan' => 'team', 'datacenter' => 'fsn', 'status' => 'paid']);
|
|
$subscription = app(OpenSubscription::class)($order);
|
|
$owner = Operator::factory()->create();
|
|
$customer = $subscription->customer;
|
|
|
|
app(GrantAddon::class)($subscription, $owner, 'priority_support', 0);
|
|
|
|
$addonOrder = Order::query()->where('type', 'addon')->sole();
|
|
|
|
expect(fn () => app(IssueInvoice::class)->forOrders($customer, collect([$addonOrder])))
|
|
->toThrow(RuntimeException::class);
|
|
|
|
expect(Invoice::query()->count())->toBe(0);
|
|
});
|
|
|
|
it('does not mistake an ordinary fully-discounted Stripe checkout for a grant', function () {
|
|
// The exact case OpenSubscription's own docblock warns about: a real
|
|
// Stripe checkout can also land at amount_cents = 0 (a 100% coupon). That
|
|
// subscription was never granted (granted_at stays null) and is still an
|
|
// owed invoice — isFreeGrant() must key off provenance, not off the
|
|
// amount alone.
|
|
$order = Order::factory()->create([
|
|
'plan' => 'team', 'datacenter' => 'fsn', 'amount_cents' => 0,
|
|
'currency' => 'EUR', 'status' => 'paid', 'stripe_event_id' => 'evt_full_coupon',
|
|
]);
|
|
$subscription = app(OpenSubscription::class)($order, Subscription::TERM_MONTHLY, ['price_cents' => 0]);
|
|
|
|
expect($subscription->isGranted())->toBeFalse();
|
|
|
|
$invoice = app(IssueInvoice::class)->forOrders($order->customer, collect([$order]));
|
|
|
|
expect($invoice->net_cents)->toBe(0)
|
|
->and($invoice->number)->toStartWith('RE-');
|
|
});
|