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(); });