Stripe: Kundendaten, Rechnungsnummer, manueller Zahlungseingang
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>
This commit is contained in:
parent
30fb031d12
commit
68d230d4e5
31 changed files with 3612 additions and 65 deletions
123
tests/Unit/Services/StripeMetadataBuilderTest.php
Normal file
123
tests/Unit/Services/StripeMetadataBuilderTest.php
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\Payment\Stripe\StripeMetadataBuilder;
|
||||
|
||||
/**
|
||||
* Phase SE2 (dev/2026-08-26-stripe/): Der Builder ist die einzige Quelle für
|
||||
* die Metadaten und die description, die an Stripe gehen. Er arbeitet rein auf
|
||||
* Modell-Attributen — kein DB-Zugriff, daher Unit-Test ohne RefreshDatabase.
|
||||
*/
|
||||
function metadataOrder(array $attributes = []): ShoppingOrder
|
||||
{
|
||||
$order = new ShoppingOrder(array_merge(['mode' => 'live'], $attributes));
|
||||
$order->id = $attributes['id'] ?? 4711;
|
||||
$order->setRelation('user_invoice', null);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
function metadataShoppingUser(array $attributes = []): ShoppingUser
|
||||
{
|
||||
$shoppingUser = new ShoppingUser(array_merge([
|
||||
'billing_firstname' => 'Erika',
|
||||
'billing_lastname' => 'Mustermann',
|
||||
], $attributes));
|
||||
$shoppingUser->id = 99;
|
||||
|
||||
return $shoppingUser;
|
||||
}
|
||||
|
||||
function metadataPayment(): ShoppingPayment
|
||||
{
|
||||
$payment = new ShoppingPayment(['reference' => 'mref123']);
|
||||
$payment->id = 555;
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
it('builds the full metadata set for a checkout', function () {
|
||||
$metadata = StripeMetadataBuilder::forCheckout(
|
||||
metadataOrder(),
|
||||
metadataPayment(),
|
||||
metadataShoppingUser(['number' => 1234, 'member_id' => 77]),
|
||||
'mref123',
|
||||
);
|
||||
|
||||
expect($metadata)->toBe([
|
||||
'shopping_order_id' => '4711',
|
||||
'shopping_payment_id' => '555',
|
||||
'reference' => 'mref123',
|
||||
'mode' => 'live',
|
||||
'order_type' => 'shop',
|
||||
'customer_name' => 'Erika Mustermann',
|
||||
'customer_number' => '1234',
|
||||
'member_id' => '77',
|
||||
]);
|
||||
});
|
||||
|
||||
it('flags abo and recovery orders by order_type', function () {
|
||||
expect(StripeMetadataBuilder::forCheckout(
|
||||
metadataOrder(['is_abo' => true]), metadataPayment(), metadataShoppingUser(), 'r1'
|
||||
)['order_type'])->toBe('abo');
|
||||
|
||||
$recovery = StripeMetadataBuilder::forCheckout(
|
||||
metadataOrder(['recovery_user_abo_id' => 42]), metadataPayment(), metadataShoppingUser(), 'r2'
|
||||
);
|
||||
|
||||
expect($recovery['order_type'])->toBe('abo_recovery')
|
||||
->and($recovery['user_abo_id'])->toBe('42');
|
||||
});
|
||||
|
||||
it('names the person first and the company in brackets', function () {
|
||||
$metadata = StripeMetadataBuilder::forCheckout(
|
||||
metadataOrder(),
|
||||
metadataPayment(),
|
||||
metadataShoppingUser(['billing_company' => 'Muster GmbH']),
|
||||
'mref123',
|
||||
);
|
||||
|
||||
expect($metadata['customer_name'])->toBe('Erika Mustermann (Muster GmbH)');
|
||||
});
|
||||
|
||||
it('uses the company alone when no person name is stored', function () {
|
||||
$metadata = StripeMetadataBuilder::forCheckout(
|
||||
metadataOrder(),
|
||||
metadataPayment(),
|
||||
metadataShoppingUser(['billing_firstname' => null, 'billing_lastname' => null, 'billing_company' => 'Muster GmbH']),
|
||||
'mref123',
|
||||
);
|
||||
|
||||
expect($metadata['customer_name'])->toBe('Muster GmbH');
|
||||
});
|
||||
|
||||
it('drops empty values so no null strings reach the reports', function () {
|
||||
$metadata = StripeMetadataBuilder::sanitize([
|
||||
'a' => null,
|
||||
'b' => '',
|
||||
'c' => 0,
|
||||
'd' => 'wert',
|
||||
'e' => 12,
|
||||
]);
|
||||
|
||||
expect($metadata)->toBe(['d' => 'wert', 'e' => '12']);
|
||||
});
|
||||
|
||||
it('truncates values to the stripe metadata limit', function () {
|
||||
$metadata = StripeMetadataBuilder::sanitize(['lang' => str_repeat('x', 700)]);
|
||||
|
||||
expect(mb_strlen($metadata['lang']))->toBe(StripeMetadataBuilder::MAX_VALUE_LENGTH);
|
||||
});
|
||||
|
||||
it('builds a searchable description with order number, reference and name', function () {
|
||||
$description = StripeMetadataBuilder::description(metadataOrder(), 'mref123', metadataShoppingUser());
|
||||
|
||||
expect($description)->toBe('Bestellung #4711 · mref123 · Erika Mustermann');
|
||||
});
|
||||
|
||||
it('works without a shopping user', function () {
|
||||
expect(StripeMetadataBuilder::description(metadataOrder(), 'mref123', null))
|
||||
->toBe('Bestellung #4711 · mref123');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue