Rueckmeldung der Buchhaltung und des Stripe-Calls (dev/2026-08-26-stripe/): Stripe kannte bisher nur die E-Mail des Kunden, Zahlungen waren im Dashboard weder ueber die Bestell- noch ueber die Rechnungsnummer auffindbar, und ein Zahlungseingang mit falschem Verwendungszweck zwang die Buchhaltung dazu, die Rechnung von Hand zu schreiben. - SE1: Der Checkout haengt einen Stripe-Customer mit Name, E-Mail und Rechnungsanschrift an die Session (customer statt customer_email); die ID liegt an shopping_users und wird wiederverwendet. Namensformat "Vorname Nachname (Firma)", reine Firmenkunden unter der Firma. Faellt die Customer-Anlage aus, laeuft der Checkout auf dem bisherigen Weg weiter. DSGVO-Loeschungen entfernen jetzt auch das Customer-Objekt; die Charges bleiben als Buchungsbeleg bestehen. - SE2: StripeMetadataBuilder als einzige Quelle fuer Metadaten und die description (Bestellnummer, Referenz, Kundenname) - damit greift die Dashboard-Suche, die bisher nichts Lesbares zu fassen bekam. - SE3: Die Rechnungsnummer entsteht erst nach der Zahlung und wird daher per Job am PaymentIntent nachgetragen (Vorab-Reservierung wuerde Luecken im Nummernkreis erzeugen). stripe:backfill-invoice-metadata traegt sie rueckwirkend ab dem Go-Live 2026-07-10 nach. - SE4: Neuer Admin-Button "Zahlungseingang erfassen" an jeder unbezahlten Bestellung - bisher gab es ihn nur bei Rechnungskauf, und er setzte nur paid, ohne Rechnung, Punkte, Abo-Aktivierung oder Mail. Der neue Weg laeuft durch paymentStatusPaidAction, verlangt den Verwendungszweck als Beleg und laesst einen abweichenden Betrag nur mit ausdruecklicher Bestaetigung zu. - S6.4 (aus dem Juli-Plan vorgezogen): stripe:sync-payment-status gleicht offene Zahlungen aktiv gegen die API ab. Aufgefallen beim Testlauf - ein nie zugestellter Webhook war bisher nicht reparierbar, das Geld lag bei Stripe waehrend die Bestellung auf offen stand. Laeuft stuendlich im Scheduler. - 110 Tests (Pest) fuer alle Pfade, Stripe-Client durchgehend gemockt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
103 lines
3.5 KiB
PHP
103 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Requests\Payment;
|
|
|
|
use App\Models\ShoppingPayment;
|
|
use App\Services\ManualPaymentService;
|
|
use Illuminate\Foundation\Http\FormRequest;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\Validator;
|
|
|
|
/**
|
|
* Erfassung eines Zahlungseingangs durch die Buchhaltung
|
|
* (Plan dev/2026-08-26-stripe/, Phase SE4).
|
|
*
|
|
* Der Verwendungszweck bzw. die Bankreferenz ist Pflicht — sie ist der Beleg,
|
|
* mit dem die Buchung später nachvollziehbar bleibt.
|
|
*/
|
|
class RecordManualPaymentRequest extends FormRequest
|
|
{
|
|
public function authorize(): bool
|
|
{
|
|
// Die Route liegt hinter der admin-Middleware.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array<int, mixed>>
|
|
*/
|
|
public function rules(): array
|
|
{
|
|
return [
|
|
'id' => ['required', 'integer', 'exists:shopping_orders,id'],
|
|
'payment_id' => ['required', 'integer', 'exists:shopping_payments,id'],
|
|
'paid_at' => ['required', 'date', 'before_or_equal:today'],
|
|
'amount' => ['required', 'numeric', 'min:0.01', 'max:99999'],
|
|
'method' => ['required', Rule::in(array_keys(ManualPaymentService::methods()))],
|
|
'bank_reference' => ['required', 'string', 'max:255'],
|
|
'note' => ['nullable', 'string', 'max:1000'],
|
|
'send_mail' => ['nullable', 'boolean'],
|
|
'confirm_deviation' => ['nullable', 'boolean'],
|
|
'back' => ['nullable', 'string'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public function messages(): array
|
|
{
|
|
return [
|
|
'bank_reference.required' => __('payment.manual_error_reference_required'),
|
|
'paid_at.before_or_equal' => __('payment.manual_error_future_date'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Ein abweichender Betrag ist erlaubt (Teilzahlung, Rundungsdifferenz),
|
|
* muss aber bewusst bestätigt werden — sonst wird aus einem Tippfehler
|
|
* still eine falsche Buchung.
|
|
*/
|
|
public function withValidator(Validator $validator): void
|
|
{
|
|
$validator->after(function (Validator $validator): void {
|
|
if ($validator->errors()->isNotEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$payment = ShoppingPayment::find($this->integer('payment_id'));
|
|
if (! $payment) {
|
|
return;
|
|
}
|
|
|
|
if ($this->amountCents() !== (int) $payment->amount && ! $this->boolean('confirm_deviation')) {
|
|
$validator->errors()->add('amount', __('payment.manual_error_amount_deviation', [
|
|
'expected' => number_format($payment->amount / 100, 2, ',', '.'),
|
|
]));
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Erfasster Betrag in Cent (die Zahlungen werden in Cent geführt).
|
|
*/
|
|
public function amountCents(): int
|
|
{
|
|
return (int) round(((float) str_replace(',', '.', (string) $this->input('amount'))) * 100);
|
|
}
|
|
|
|
/**
|
|
* @return array{amount_cents: int, paid_at: string, method: string, bank_reference: string, note: string|null, send_mail: bool}
|
|
*/
|
|
public function toServiceData(): array
|
|
{
|
|
return [
|
|
'amount_cents' => $this->amountCents(),
|
|
'paid_at' => (string) $this->input('paid_at'),
|
|
'method' => (string) $this->input('method'),
|
|
'bank_reference' => trim((string) $this->input('bank_reference')),
|
|
'note' => $this->filled('note') ? trim((string) $this->input('note')) : null,
|
|
'send_mail' => $this->boolean('send_mail'),
|
|
];
|
|
}
|
|
}
|