CluPilotCloud/tests/Feature/Billing/DunningControlsTest.php

144 lines
5.0 KiB
PHP

<?php
use App\Livewire\Admin\CloseDunningCase;
use App\Livewire\Admin\ExtendDunningDeadline;
use App\Livewire\Admin\PaymentProblems;
use App\Models\Customer;
use App\Models\DunningCase;
use App\Models\Subscription;
use App\Services\Billing\DunningSchedule;
use Illuminate\Support\Carbon;
use Livewire\Livewire;
/**
* Die zwei Eingriffe, die ein Mensch braucht, und die Einstellungen.
*
* Ohne sie ist der Mahnlauf nach dem ersten Telefonat im Weg: der Kunde sagt
* „ich überweise morgen", und die Automatik schaltet trotzdem am Freitag ab.
*/
function openCase(int $level = 1): DunningCase
{
$customer = Customer::factory()->create(['name' => 'Berger GmbH', 'stripe_customer_id' => 'cus_42']);
$subscription = Subscription::factory()->create([
'customer_id' => $customer->id, 'stripe_subscription_id' => 'sub_42',
]);
return DunningCase::query()->create([
'subscription_id' => $subscription->id,
'stripe_invoice_id' => 'in_1',
'level' => $level,
'opened_at' => Carbon::now()->subDays(3),
'next_step_at' => Carbon::now()->addDay(),
'fee_invoice_ids' => [],
]);
}
// ---- Frist verlängern ----------------------------------------------------
it('pushes the deadline out and says who did it and why', function () {
$case = openCase();
$vorher = $case->next_step_at;
Livewire::actingAs(operator('Owner'), 'operator')
->test(PaymentProblems::class)
->call('onExtended', $case->id, 7, 'Kunde überweist bis Freitag')
->assertHasNoErrors();
$case->refresh();
expect($case->next_step_at->isAfter($vorher->copy()->addDays(6)))->toBeTrue()
->and($case->note)->toContain('Freitag');
});
it('does not push a case that is already suspended', function () {
// Ab der Sperre gibt es keine nächste Frist mehr — die Cloud steht, und
// was jetzt hilft, ist Bezahlen oder das Schliessen von Hand.
$case = openCase(DunningSchedule::SUSPENDED);
$case->update(['next_step_at' => null]);
Livewire::actingAs(operator('Owner'), 'operator')
->test(PaymentProblems::class)
->call('onExtended', $case->id, 7, 'egal')
->assertHasErrors();
expect($case->fresh()->next_step_at)->toBeNull();
});
// ---- Fall von Hand schliessen -------------------------------------------
it('closes a case by hand, with a reason', function () {
// Der Fall, für den es das gibt: jemand hat überwiesen statt die Karte zu
// reparieren. Stripe weiss davon nichts, und die Automatik käme nie zur
// Ruhe.
$case = openCase();
Livewire::actingAs(operator('Owner'), 'operator')
->test(PaymentProblems::class)
->call('onClosed', $case->id, 'Überweisung am 30.7. eingegangen')
->assertHasNoErrors();
$case->refresh();
expect($case->settled_at)->not->toBeNull()
->and($case->note)->toContain('Überweisung');
});
it('refuses to close without a reason', function () {
// „Erledigt" ohne Begründung ist beim nächsten Nachfragen wertlos — und
// hier hängt Geld daran, das niemand mehr eintreibt.
$case = openCase();
Livewire::actingAs(operator('Owner'), 'operator')
->test(PaymentProblems::class)
->call('onClosed', $case->id, ' ')
->assertHasErrors();
expect($case->fresh()->settled_at)->toBeNull();
});
it('keeps both interventions behind billing.manage', function () {
$case = openCase();
Livewire::actingAs(operator('Support'), 'operator')
->test(ExtendDunningDeadline::class, ['id' => $case->id])
->assertForbidden();
Livewire::actingAs(operator('Support'), 'operator')
->test(CloseDunningCase::class, ['id' => $case->id])
->assertForbidden();
});
// ---- Einstellungen -------------------------------------------------------
it('lets the console set the deadlines and the fees', function () {
Livewire::actingAs(operator('Owner'), 'operator')
->test(PaymentProblems::class)
->set('days', [0, 5, 12, 20, 30])
->set('feeFromLevel', 3)
->set('fees', [2 => 500, 3 => 1500])
->call('saveSchedule')
->assertHasNoErrors();
expect(DunningSchedule::dayOfLevel(1))->toBe(5)
->and(DunningSchedule::feeFromLevel())->toBe(3)
->and(DunningSchedule::feeCents(2))->toBe(0)
->and(DunningSchedule::feeCents(3))->toBe(1500);
});
it('refuses a schedule that runs backwards', function () {
// Rückwärts laufende Fristen sind kein Zeitplan — der Tageslauf spränge
// über Stufen. Abgelehnt und gesagt, nicht still sortiert: wer 20/5/12
// eintippt, hat sich vertan und soll es sehen.
Livewire::actingAs(operator('Owner'), 'operator')
->test(PaymentProblems::class)
->set('days', [0, 20, 5, 12, 30])
->call('saveSchedule')
->assertHasErrors('days');
});
it('does not let the settings be changed without billing.manage', function () {
Livewire::actingAs(operator('Support'), 'operator')
->test(PaymentProblems::class)
->assertForbidden();
});