144 lines
5.0 KiB
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();
|
|
});
|