Besseres Handling Rückerstattungen
This commit is contained in:
@@ -232,12 +232,26 @@ class ParticipantRefundTest extends TestCase
|
||||
string $owner = 'Mika Muster',
|
||||
string $iban = 'DE02120300000000202051',
|
||||
bool $declarationAccepted = true,
|
||||
bool $accountDeclarationAccepted = true,
|
||||
) {
|
||||
return new AcceptRefundCommand(new AcceptRefundRequest(
|
||||
refund: $refund,
|
||||
accountOwner: $owner,
|
||||
accountIban: $iban,
|
||||
declarationAccepted: $declarationAccepted,
|
||||
accountDeclarationAccepted: $accountDeclarationAccepted,
|
||||
))->execute();
|
||||
}
|
||||
|
||||
/** Der zweite Weg: Der Teili verzichtet auf die Auszahlung und spendet -- ohne Bankverbindung. */
|
||||
private function acceptAsDonation(?ParticipantRefund $refund, bool $declarationAccepted = true)
|
||||
{
|
||||
return new AcceptRefundCommand(new AcceptRefundRequest(
|
||||
refund: $refund,
|
||||
accountOwner: '',
|
||||
accountIban: '',
|
||||
declarationAccepted: $declarationAccepted,
|
||||
donation: true,
|
||||
))->execute();
|
||||
}
|
||||
|
||||
@@ -459,6 +473,105 @@ class ParticipantRefundTest extends TestCase
|
||||
$this->assertSame(ParticipantRefund::STATUS_PENDING, $refund->fresh()->status);
|
||||
}
|
||||
|
||||
public function test_accept_is_rejected_without_the_account_declaration(): void
|
||||
{
|
||||
$refund = $this->release($this->makeParticipant($this->makeEvent()))->refund;
|
||||
|
||||
// Erstattet wird nur auf das Konto, von dem der Beitrag kam. Fehlt die Bestätigung, ließe sich
|
||||
// über eine Erstattung Geld auf ein fremdes Konto umleiten.
|
||||
$response = $this->accept($refund, accountDeclarationAccepted: false);
|
||||
|
||||
$this->assertFalse($response->success);
|
||||
$this->assertArrayHasKey('accountDeclaration', $response->errorTypes);
|
||||
$this->assertSame(ParticipantRefund::STATUS_PENDING, $refund->fresh()->status);
|
||||
$this->assertNull($refund->fresh()->account_iban);
|
||||
}
|
||||
|
||||
public function test_the_account_declaration_cannot_be_skipped_over_http(): void
|
||||
{
|
||||
$refund = $this->release($this->makeParticipant($this->makeEvent()))->refund;
|
||||
|
||||
$this->postJson('/api/v1/participant-refund/' . $refund->token . '/accept', [
|
||||
'accountOwner' => 'Mika Muster',
|
||||
'accountIban' => 'DE02 1203 0000 0000 2020 51',
|
||||
'declarationAccepted' => true,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('status', 'error')
|
||||
->assertJsonStructure(['error_types' => ['accountDeclaration']]);
|
||||
|
||||
$this->assertSame(ParticipantRefund::STATUS_PENDING, $refund->fresh()->status);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Spenden statt auszahlen
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function test_a_donation_is_accepted_without_bank_details(): void
|
||||
{
|
||||
$refund = $this->release($this->makeParticipant($this->makeEvent()))->refund;
|
||||
|
||||
$response = $this->acceptAsDonation($refund);
|
||||
|
||||
$this->assertTrue($response->success);
|
||||
$this->assertSame(ParticipantRefund::STATUS_ACCEPTED, $refund->fresh()->status);
|
||||
$this->assertTrue($refund->fresh()->isDonation());
|
||||
}
|
||||
|
||||
public function test_a_donation_still_needs_the_declaration(): void
|
||||
{
|
||||
$refund = $this->release($this->makeParticipant($this->makeEvent()))->refund;
|
||||
|
||||
// Der Verzicht ist die Erklärung, die anschließend auf dem Beleg steht -- ohne sie nichts.
|
||||
$response = $this->acceptAsDonation($refund, declarationAccepted: false);
|
||||
|
||||
$this->assertFalse($response->success);
|
||||
$this->assertArrayHasKey('declaration', $response->errorTypes);
|
||||
$this->assertSame(ParticipantRefund::STATUS_PENDING, $refund->fresh()->status);
|
||||
}
|
||||
|
||||
public function test_a_donation_ignores_bank_details_sent_along(): void
|
||||
{
|
||||
$refund = $this->release($this->makeParticipant($this->makeEvent()))->refund;
|
||||
|
||||
// Wer im Formular erst ein Konto eintippt und dann doch spendet, soll es nicht hinterlassen.
|
||||
$response = new AcceptRefundCommand(new AcceptRefundRequest(
|
||||
refund: $refund,
|
||||
accountOwner: 'Mika Muster',
|
||||
accountIban: 'DE02120300000000202051',
|
||||
declarationAccepted: true,
|
||||
donation: true,
|
||||
))->execute();
|
||||
|
||||
$this->assertTrue($response->success);
|
||||
$this->assertNull($refund->fresh()->account_owner);
|
||||
$this->assertNull($refund->fresh()->account_iban);
|
||||
}
|
||||
|
||||
public function test_a_donation_over_http_needs_no_iban(): void
|
||||
{
|
||||
$refund = $this->release($this->makeParticipant($this->makeEvent()))->refund;
|
||||
|
||||
$this->postJson('/api/v1/participant-refund/' . $refund->token . '/accept', [
|
||||
'donation' => true,
|
||||
'declarationAccepted' => true,
|
||||
])->assertOk()->assertJsonPath('status', 'success');
|
||||
|
||||
$this->assertTrue($refund->fresh()->isDonation());
|
||||
}
|
||||
|
||||
public function test_the_page_shows_a_finished_donation_as_donated(): void
|
||||
{
|
||||
$refund = $this->release($this->makeParticipant($this->makeEvent()))->refund;
|
||||
$this->acceptAsDonation($refund);
|
||||
|
||||
$this->get('/rueckerstattung/' . $refund->token)
|
||||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page->where('state', 'accepted')->where('donation', true));
|
||||
}
|
||||
|
||||
public function test_the_public_page_serves_the_declaration_text_name(): void
|
||||
{
|
||||
$refund = $this->release($this->makeParticipant($this->makeEvent()))->refund;
|
||||
@@ -473,6 +586,19 @@ class ParticipantRefundTest extends TestCase
|
||||
->assertSee('Ich versichere', false);
|
||||
}
|
||||
|
||||
public function test_the_public_page_serves_the_new_declaration_texts(): void
|
||||
{
|
||||
// Beide Erklärungen stehen auf der Seite, die ohne Login erreichbar ist -- käme eine davon
|
||||
// nicht durch, stünde dort eine leere Checkbox, die sich trotzdem ankreuzen ließe.
|
||||
$this->get('/api/v1/core/retrieve-text-resource/' . CreateRefundDocumentCommand::ACCOUNT_DECLARATION_TEXT)
|
||||
->assertOk()
|
||||
->assertSee('dass das angegebene Konto dasselbe ist', false);
|
||||
|
||||
$this->get('/api/v1/core/retrieve-text-resource/' . CreateRefundDocumentCommand::DONATION_DECLARATION_TEXT)
|
||||
->assertOk()
|
||||
->assertSee('verzichte auf die Auszahlung', false);
|
||||
}
|
||||
|
||||
public function test_accept_requires_an_account_owner(): void
|
||||
{
|
||||
$refund = $this->release($this->makeParticipant($this->makeEvent()))->refund;
|
||||
@@ -716,6 +842,7 @@ class ParticipantRefundTest extends TestCase
|
||||
'accountOwner' => 'Mika Muster',
|
||||
'accountIban' => 'DE02 1203 0000 0000 2020 51',
|
||||
'declarationAccepted' => true,
|
||||
'accountDeclarationAccepted' => true,
|
||||
])->assertOk()->assertJsonPath('status', 'success');
|
||||
|
||||
$this->assertSame('DE02120300000000202051', $refund->fresh()->account_iban);
|
||||
|
||||
@@ -412,6 +412,94 @@ class RefundDirectCaptureTest extends TestCase
|
||||
$this->assertNotNull(ParticipantRefund::first()->invoice_id);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Der Teili spendet -- die Aktionsleitung nimmt es auf
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function test_a_donation_is_submitted_right_away_without_bank_details(): void
|
||||
{
|
||||
$participant = $this->makeParticipant();
|
||||
|
||||
$response = new ReleaseRefundCommand(new ReleaseRefundRequest(
|
||||
participant: $participant,
|
||||
amount: new Amount(220.0, 'Euro'),
|
||||
reason: RefundReason::SICKNESS,
|
||||
retentionReason: RetentionReason::CANCELLATION_FEE,
|
||||
donation: true,
|
||||
))->execute();
|
||||
|
||||
$this->assertTrue($response->success);
|
||||
$this->assertStringContainsString('Spende', $response->message);
|
||||
|
||||
$refund = ParticipantRefund::first();
|
||||
$this->assertSame(ParticipantRefund::STATUS_ACCEPTED, $refund->status);
|
||||
$this->assertNull($refund->account_iban);
|
||||
$this->assertTrue($refund->isDonation());
|
||||
// Wer sie aufgenommen hat, wird auch hier festgehalten -- der Teili hat nichts angekreuzt.
|
||||
$this->assertSame($this->management->id, $refund->captured_by);
|
||||
$this->assertTrue((bool) Invoice::first()->donation);
|
||||
}
|
||||
|
||||
public function test_a_donation_with_bank_details_is_refused(): void
|
||||
{
|
||||
$participant = $this->makeParticipant();
|
||||
|
||||
// Beides zusammen ist widersprüchlich: Lieber nachfragen, als eines stillschweigend zu verwerfen.
|
||||
$response = new ReleaseRefundCommand(new ReleaseRefundRequest(
|
||||
participant: $participant,
|
||||
amount: new Amount(220.0, 'Euro'),
|
||||
reason: RefundReason::SICKNESS,
|
||||
accountOwner: 'Mika Muster',
|
||||
accountIban: 'DE02120300000000202051',
|
||||
retentionReason: RetentionReason::CANCELLATION_FEE,
|
||||
donation: true,
|
||||
))->execute();
|
||||
|
||||
$this->assertFalse($response->success);
|
||||
$this->assertStringContainsString('Bankverbindung', $response->message);
|
||||
$this->assertSame(0, ParticipantRefund::count());
|
||||
}
|
||||
|
||||
public function test_a_donation_without_a_cost_unit_is_refused(): void
|
||||
{
|
||||
// Gebucht wird sie trotzdem -- ohne Kostenstelle gibt es nichts, worauf.
|
||||
$participant = $this->makeParticipant($this->makeEvent(['cost_unit_id' => null]));
|
||||
|
||||
$response = new ReleaseRefundCommand(new ReleaseRefundRequest(
|
||||
participant: $participant,
|
||||
amount: new Amount(220.0, 'Euro'),
|
||||
reason: RefundReason::SICKNESS,
|
||||
retentionReason: RetentionReason::CANCELLATION_FEE,
|
||||
donation: true,
|
||||
))->execute();
|
||||
|
||||
$this->assertFalse($response->success);
|
||||
$this->assertStringContainsString('Kostenstelle', $response->message);
|
||||
$this->assertSame(0, ParticipantRefund::count());
|
||||
}
|
||||
|
||||
public function test_release_over_http_submits_a_donation(): void
|
||||
{
|
||||
$participant = $this->makeParticipant();
|
||||
|
||||
$this->postJson('/api/v1/participant-refund/' . $participant->identifier . '/release', [
|
||||
'amount' => '220,00',
|
||||
'reason' => RefundReason::SICKNESS,
|
||||
'retentionReason' => RetentionReason::CANCELLATION_FEE,
|
||||
'accountOwner' => '',
|
||||
'accountIban' => '',
|
||||
'donation' => true,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('status', 'success')
|
||||
->assertJsonPath('refund.status', ParticipantRefund::STATUS_ACCEPTED)
|
||||
->assertJsonPath('refund.donation', true);
|
||||
|
||||
$this->assertTrue((bool) Invoice::first()->donation);
|
||||
}
|
||||
|
||||
public function test_release_over_http_without_bank_details_keeps_the_old_way(): void
|
||||
{
|
||||
$participant = $this->makeParticipant();
|
||||
|
||||
@@ -98,7 +98,8 @@ class RefundDocumentTest extends TestCase
|
||||
DocumentTemplate::create([
|
||||
'document_type' => DocumentTemplate::TYPE_PARTICIPANT_REFUND,
|
||||
'block' => DocumentTemplate::BLOCK_BODY,
|
||||
'content' => '{details_table}<p>{refund_amount} / {refund_reason} / {refund_reason_text}</p>'
|
||||
'content' => '<p>{intro_text}</p>{details_table}'
|
||||
. '<p>{refund_amount} / {refund_reason} / {refund_reason_text}</p>'
|
||||
. '<p>{account_owner} / {account_iban}</p>'
|
||||
. '<p>{paid_amount} / {invoice_number}</p>'
|
||||
. '<p>{declaration_text}</p>',
|
||||
@@ -195,15 +196,19 @@ class RefundDocumentTest extends TestCase
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
private function document(ParticipantRefund $refund)
|
||||
private function document(ParticipantRefund $refund, bool $donation = false)
|
||||
{
|
||||
return new CreateRefundDocumentCommand(new CreateRefundDocumentRequest($refund))->execute();
|
||||
return new CreateRefundDocumentCommand(
|
||||
new CreateRefundDocumentRequest($refund, donation: $donation)
|
||||
)->execute();
|
||||
}
|
||||
|
||||
/** Das gerenderte HTML -- die Zwischenstufe vor dem PDF, an der sich der Inhalt prüfen lässt. */
|
||||
private function html(ParticipantRefund $refund): string
|
||||
private function html(ParticipantRefund $refund, bool $donation = false): string
|
||||
{
|
||||
$command = new CreateRefundDocumentCommand(new CreateRefundDocumentRequest($refund));
|
||||
$command = new CreateRefundDocumentCommand(
|
||||
new CreateRefundDocumentRequest($refund, donation: $donation)
|
||||
);
|
||||
$number = new ReflectionMethod($command, 'documentNumber')->invoke($command);
|
||||
$tokens = new ReflectionMethod($command, 'buildTokens')->invoke($command, $number);
|
||||
|
||||
@@ -380,6 +385,55 @@ class RefundDocumentTest extends TestCase
|
||||
$this->assertStringContainsString('Ich versichere', $this->html($this->makeRefund()));
|
||||
}
|
||||
|
||||
public function test_the_payout_receipt_carries_the_account_declaration_too(): void
|
||||
{
|
||||
// Beide Sätze wurden angekreuzt, beide gehören auf den Beleg: Die Kontoerklärung ist der Grund,
|
||||
// warum die Auszahlung auf genau dieses Konto zulässig ist.
|
||||
$html = $this->html($this->makeRefund());
|
||||
|
||||
$this->assertStringContainsString('Ich versichere, dass ich den genannten Betrag beglichen habe', $html);
|
||||
$this->assertStringContainsString('dass das angegebene Konto dasselbe ist', $html);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Der Beleg über eine Spende
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function test_a_donation_receipt_carries_the_waiver_instead(): void
|
||||
{
|
||||
$html = $this->html($this->makeRefund(['account_owner' => null, 'account_iban' => null]), donation: true);
|
||||
|
||||
$this->assertStringContainsString('verzichte auf die Auszahlung', $html);
|
||||
// Die Erklärungen des Auszahlungswegs haben hier nichts zu suchen -- es gibt kein Konto.
|
||||
$this->assertStringNotContainsString('dass das angegebene Konto dasselbe ist', $html);
|
||||
}
|
||||
|
||||
public function test_a_donation_receipt_shows_no_bank_details(): void
|
||||
{
|
||||
$html = $this->html($this->makeRefund(['account_owner' => null, 'account_iban' => null]), donation: true);
|
||||
|
||||
$this->assertStringNotContainsString('IBAN', $html);
|
||||
$this->assertStringContainsString('Auf die Auszahlung wird verzichtet', $html);
|
||||
}
|
||||
|
||||
public function test_the_intro_sentence_follows_the_chosen_way(): void
|
||||
{
|
||||
$refund = $this->makeRefund();
|
||||
|
||||
$this->assertStringContainsString('bitte um die Rückerstattung', $this->html($refund));
|
||||
$this->assertStringContainsString('spende ihn an den Verband', $this->html($refund, donation: true));
|
||||
}
|
||||
|
||||
public function test_a_donation_receipt_is_a_pdf_as_well(): void
|
||||
{
|
||||
$response = $this->document($this->makeRefund(['account_owner' => null, 'account_iban' => null]), donation: true);
|
||||
|
||||
$this->assertTrue($response->success);
|
||||
$this->assertStringStartsWith('%PDF', $response->pdfContent);
|
||||
}
|
||||
|
||||
public function test_free_text_reason_replaces_the_catalog_text(): void
|
||||
{
|
||||
$html = $this->html($this->makeRefund([
|
||||
|
||||
@@ -221,6 +221,7 @@ class RefundInvoiceTest extends TestCase
|
||||
float $amount = 220.0,
|
||||
string $reason = RefundReason::SICKNESS,
|
||||
?string $retentionReason = RetentionReason::CANCELLATION_FEE,
|
||||
bool $donation = false,
|
||||
): ParticipantRefund {
|
||||
$participant ??= $this->makeParticipant($this->makeEvent());
|
||||
|
||||
@@ -231,11 +232,14 @@ class RefundInvoiceTest extends TestCase
|
||||
retentionReason: $retentionReason,
|
||||
))->execute()->refund;
|
||||
|
||||
// Wer spendet, gibt keine Bankverbindung an -- der Server verlangt sie dann auch nicht.
|
||||
new AcceptRefundCommand(new AcceptRefundRequest(
|
||||
refund: $refund,
|
||||
accountOwner: 'Mika Muster',
|
||||
accountIban: 'DE02120300000000202051',
|
||||
accountOwner: $donation ? '' : 'Mika Muster',
|
||||
accountIban: $donation ? '' : 'DE02120300000000202051',
|
||||
declarationAccepted: true,
|
||||
donation: $donation,
|
||||
accountDeclarationAccepted: !$donation,
|
||||
))->execute();
|
||||
|
||||
return $refund->fresh();
|
||||
@@ -262,6 +266,80 @@ class RefundInvoiceTest extends TestCase
|
||||
$this->assertFalse((bool) $invoice->donation);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Spenden statt auszahlen
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function test_a_donated_refund_becomes_a_donated_invoice(): void
|
||||
{
|
||||
$this->runRefund(donation: true);
|
||||
|
||||
$invoice = Invoice::first();
|
||||
|
||||
$this->assertNotNull($invoice);
|
||||
$this->assertTrue((bool) $invoice->donation);
|
||||
// Derselbe Weg wie sonst: Nummernkreis, Status und Typ der Abrechnung ändern sich nicht.
|
||||
$this->assertSame(InvoiceStatus::INVOICE_STATUS_NEW, $invoice->status);
|
||||
$this->assertSame(InvoiceType::INVOICE_TYPE_PARTICIPATION_REFUND, $invoice->type);
|
||||
$this->assertEqualsWithDelta(220.0, $invoice->amount, 0.001);
|
||||
}
|
||||
|
||||
public function test_a_donation_carries_no_bank_details(): void
|
||||
{
|
||||
$refund = $this->runRefund(donation: true);
|
||||
|
||||
// Weder am Vorgang noch an der Abrechnung -- und `null`, nicht Leerstring: Der SEPA-Export
|
||||
// unterscheidet daran, ob es etwas auszuzahlen gibt.
|
||||
$this->assertNull($refund->account_owner);
|
||||
$this->assertNull($refund->account_iban);
|
||||
$this->assertNull(Invoice::first()->contact_bank_owner);
|
||||
$this->assertNull(Invoice::first()->contact_bank_iban);
|
||||
}
|
||||
|
||||
public function test_the_refund_reads_the_donation_from_its_invoice(): void
|
||||
{
|
||||
// Am Vorgang steht sie nicht: Die Abrechnung führt sie, damit beide nicht auseinanderlaufen.
|
||||
$this->assertTrue($this->runRefund(donation: true)->isDonation());
|
||||
}
|
||||
|
||||
public function test_a_payout_is_no_donation(): void
|
||||
{
|
||||
$this->assertFalse($this->runRefund()->isDonation());
|
||||
}
|
||||
|
||||
public function test_a_donation_still_gets_a_receipt(): void
|
||||
{
|
||||
$this->runRefund(donation: true);
|
||||
|
||||
$invoice = Invoice::first();
|
||||
|
||||
$this->assertNotNull($invoice->document_filename);
|
||||
Storage::disk('local')->assertExists($invoice->document_filename);
|
||||
$this->assertStringStartsWith('%PDF', Storage::disk('local')->get($invoice->document_filename));
|
||||
}
|
||||
|
||||
public function test_a_donation_settles_the_amount_paid_like_a_payout(): void
|
||||
{
|
||||
$participant = $this->makeParticipant($this->makeEvent());
|
||||
$this->runRefund($participant, donation: true);
|
||||
|
||||
// Was einbehalten wurde, bleibt einbehaltener Teilnahmebeitrag; der erstattungsfähige Teil ist
|
||||
// ab jetzt eine Spende und wird über die Abrechnung geführt, nicht mehr über den Beitrag.
|
||||
$this->assertEqualsWithDelta(80.0, $participant->fresh()->amount_paid->getAmount(), 0.001);
|
||||
}
|
||||
|
||||
public function test_the_notice_of_a_donation_names_it(): void
|
||||
{
|
||||
$this->runRefund(donation: true);
|
||||
|
||||
// Die Schatzmeisterei sieht die Abrechnung ohne den Vorgang dahinter -- warum keine
|
||||
// Bankverbindung dabei ist, steht nur hier.
|
||||
$this->assertStringContainsString('Spende statt Rückerstattung', Invoice::first()->comment);
|
||||
$this->assertStringContainsString('Gezahlter Beitrag vor Erstattung: 300,00 Euro', Invoice::first()->comment);
|
||||
}
|
||||
|
||||
public function test_contact_details_come_from_the_participant(): void
|
||||
{
|
||||
$this->runRefund();
|
||||
@@ -384,6 +462,7 @@ class RefundInvoiceTest extends TestCase
|
||||
accountOwner: 'Mika Muster',
|
||||
accountIban: 'DE02120300000000202051',
|
||||
declarationAccepted: true,
|
||||
accountDeclarationAccepted: true,
|
||||
))->execute();
|
||||
|
||||
$this->assertFalse($response->success);
|
||||
|
||||
@@ -222,6 +222,7 @@ class RefundRetentionTest extends TestCase
|
||||
accountOwner: 'Mika Muster',
|
||||
accountIban: 'DE02120300000000202051',
|
||||
declarationAccepted: true,
|
||||
accountDeclarationAccepted: true,
|
||||
))->execute();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user