143 lines
5.3 KiB
PHP
143 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Event\Actions\BookBankStatementPayments;
|
|
|
|
use App\Domains\Event\Actions\ParticipantPayment\ParticipantPaymentCommand;
|
|
use App\Domains\Event\Actions\ParticipantPayment\ParticipantPaymentRequest;
|
|
use App\EventPaymentModules\EventPaymentModuleRegistry;
|
|
use App\EventPaymentModules\ReadsBankStatements;
|
|
use App\Models\EventParticipant;
|
|
use App\Models\PaymentMethod;
|
|
use App\Repositories\EventParticipantRepository;
|
|
use App\ValueObjects\Amount;
|
|
use App\ValueObjects\BankTransaction;
|
|
use Carbon\CarbonImmutable;
|
|
|
|
/**
|
|
* Bucht die in der Prüfansicht bestätigten Zahlungseingänge -- alle in einem Durchgang.
|
|
*
|
|
* Bewusst **ohne** umspannende Transaktion: Gebucht wird über {@see ParticipantPaymentCommand}, und der
|
|
* verschickt die Zahlungsmail gleich mit. Das Projekt kennt keine Queue, die Mails gehen also synchron
|
|
* raus; eine offene Transaktion über achtzig Mailversände wäre die schlechtere Wahl. Jede Zeile ist
|
|
* für sich vollständig, und das Wasserzeichen schützt sie gegen eine Wiederholung.
|
|
*/
|
|
class BookBankStatementPaymentsCommand
|
|
{
|
|
public function __construct(private readonly BookBankStatementPaymentsRequest $request)
|
|
{
|
|
}
|
|
|
|
public function execute(): BookBankStatementPaymentsResponse
|
|
{
|
|
$response = new BookBankStatementPaymentsResponse();
|
|
|
|
if ($this->request->bookings === []) {
|
|
$response->message = 'Es wurden keine Zahlungen zum Buchen ausgewählt.';
|
|
|
|
return $response;
|
|
}
|
|
|
|
$module = EventPaymentModuleRegistry::forSlug(PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION);
|
|
$participants = new EventParticipantRepository();
|
|
|
|
foreach ($this->request->bookings as $booking) {
|
|
$participant = $participants->findInEventByIdentifier(
|
|
$this->request->event,
|
|
(string) ($booking['participantIdentifier'] ?? ''),
|
|
);
|
|
|
|
$transaction = $this->toTransaction($booking);
|
|
|
|
if ($participant === null || $transaction === null) {
|
|
$response->failed++;
|
|
continue;
|
|
}
|
|
|
|
// Zweite Prüfung, nicht bloß Gürtel und Hosenträger: In der Prüfansicht lässt sich eine
|
|
// Zeile von Hand einer anderen Person zuordnen, und deren Wasserzeichen hat der erste
|
|
// Durchlauf nie gesehen.
|
|
if ($this->isBeforeWatermark($participant, $transaction)) {
|
|
$response->skipped++;
|
|
continue;
|
|
}
|
|
|
|
if ($module instanceof ReadsBankStatements) {
|
|
$module->recordTransaction($participant, $transaction);
|
|
}
|
|
|
|
$participant->last_payment_date = $transaction->paymentDate;
|
|
|
|
$this->book($participant, $transaction->amount);
|
|
$response->booked++;
|
|
}
|
|
|
|
$response->success = true;
|
|
$response->message = $this->summaryMessage($response);
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Bucht den Betrag **zusätzlich** zum bereits Gezahlten.
|
|
*
|
|
* Die manuelle Erfassung setzt `amount_paid` absolut, weil dort jemand den Gesamtstand eintippt.
|
|
* Hier kommt eine einzelne Zahlung an -- eine zweite Rate darf die erste nicht überschreiben.
|
|
* Der {@see ParticipantPaymentCommand} speichert dabei die zuvor gesetzten Felder mit und schickt
|
|
* die passende Mail (bezahlt / fehlt noch / überzahlt).
|
|
*/
|
|
private function book(EventParticipant $participant, Amount $amount): void
|
|
{
|
|
$total = new Amount(
|
|
round(($participant->amount_paid?->getAmount() ?? 0.0) + $amount->getAmount(), 2),
|
|
'Euro',
|
|
);
|
|
|
|
new ParticipantPaymentCommand(new ParticipantPaymentRequest($participant, $total))->execute();
|
|
}
|
|
|
|
/** @param array<string, mixed> $booking */
|
|
private function toTransaction(array $booking): ?BankTransaction
|
|
{
|
|
$date = CarbonImmutable::createFromFormat('!Y-m-d', (string) ($booking['paymentDate'] ?? ''));
|
|
$amount = $booking['amount'] ?? null;
|
|
|
|
if ($date === false || !is_numeric($amount) || (float) $amount <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return new BankTransaction(
|
|
paymentDate: $date,
|
|
amount: new Amount(round((float) $amount, 2), 'Euro'),
|
|
purpose: (string) ($booking['purpose'] ?? ''),
|
|
payerName: (string) ($booking['payerName'] ?? ''),
|
|
payerIban: (string) ($booking['payerIban'] ?? ''),
|
|
rowNumber: (int) ($booking['rowNumber'] ?? 0),
|
|
);
|
|
}
|
|
|
|
private function isBeforeWatermark(EventParticipant $participant, BankTransaction $transaction): bool
|
|
{
|
|
$watermark = $participant->last_payment_date;
|
|
|
|
return $watermark !== null && $transaction->paymentDate->startOfDay()->lt($watermark->startOfDay());
|
|
}
|
|
|
|
private function summaryMessage(BookBankStatementPaymentsResponse $response): string
|
|
{
|
|
$message = sprintf('%d Zahlungen wurden gebucht.', $response->booked);
|
|
|
|
if ($response->skipped > 0) {
|
|
$message .= sprintf(
|
|
' %d wurden übersprungen, weil sie älter sind als die zuletzt erfasste Zahlung der Person.',
|
|
$response->skipped,
|
|
);
|
|
}
|
|
|
|
if ($response->failed > 0) {
|
|
$message .= sprintf(' %d konnten nicht zugeordnet werden.', $response->failed);
|
|
}
|
|
|
|
return $message;
|
|
}
|
|
}
|