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>
123 lines
3.9 KiB
PHP
123 lines
3.9 KiB
PHP
<?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');
|
|
});
|