- Pay-now-Flow: bei fehlgeschlagenem Abo (Status 3) begleicht der Inhaber die offene Rechnung im Standard-Checkout mit frei waehlbarer, abo-faehiger Zahlart (nur eigenes Abo: Berater me-View + Kundenportal), payNow-Controller + Routen - AboRecoveryCheckoutService baut die ShoppingInstance aus den Abo-Artikeln; neue Spalte shopping_orders.recovery_user_abo_id verknuepft Bestellung<->Abo - Bei Erfolg: AboHelper::advanceAboAfterRecoveryPayment schreibt das Abo fort (Status->2, next_date) und uebernimmt die gewaehlte Zahlart als neues Token fuer kuenftige Abbuchungen - Neue Pay-now-Fehlermail (AboPaymentFailedMail) ersetzt die generische Fehlermail im Abo-Cron; Link fuehrt auf die Abo-Detailseite (CRM bzw. Portal) - Retry-/"extern erledigt"-Buttons nur noch fuer Admins; tote user/portal- Retry-Routen entfernt - Checkout: Recovery-Hinweis statt "Abschluss des Abonnements", Lieferadresse vorbelegt, korrekter Zurueck-Link (Abo statt Shop) - UserAbo::selfServiceDetailUrl zentralisiert die Abo-URL (Mail, Back-Links) - Test-Command abo:test-recovery; Tests fuer payNow, Advance-Logik, Fehlermail Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
188 lines
6.9 KiB
PHP
188 lines
6.9 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Mail\AboPaymentFailedMail;
|
||
use App\Models\PaymentTransaction;
|
||
use App\Models\ShoppingOrder;
|
||
use App\Models\ShoppingPayment;
|
||
use App\Models\UserAbo;
|
||
use App\Services\AboHelper;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\Mail;
|
||
|
||
/**
|
||
* Testhilfe für die Abo-Recovery-Zahlung (fehlgeschlagene Folgezahlung → nachträglich bezahlen).
|
||
*
|
||
* Beispiele:
|
||
* php artisan abo:test-recovery --abo=123 --fail # Abo als fehlgeschlagen markieren + Mail
|
||
* php artisan abo:test-recovery --abo=123 --email=me@test.de # nur Mail an eigene Adresse
|
||
* php artisan abo:test-recovery --abo=123 --advance --no-mail # nur erfolgreiche Recovery simulieren
|
||
* php artisan abo:test-recovery --abo=123 --fail --advance # kompletter Durchlauf
|
||
*/
|
||
class AboTestRecovery extends Command
|
||
{
|
||
protected $signature = 'abo:test-recovery
|
||
{--abo= : Abo-ID (Standard: neuestes Abo)}
|
||
{--email= : Empfänger der Test-Mail (Standard: E-Mail des Abos)}
|
||
{--fail : Abo vorab auf Status 3 (fehlgeschlagen) + aktiv setzen, damit der Pay-now-Button erscheint}
|
||
{--advance : Erfolgreiche Recovery-Zahlung simulieren (Zahlart-Update + next_date), ohne echten Payone-Call}
|
||
{--no-mail : Keine Test-Mail senden}
|
||
{--force : Sicherheitsabfrage bei --advance überspringen}';
|
||
|
||
protected $description = 'Testet die Abo-Recovery: verschickt die "Zahlung fehlgeschlagen"-Mail und/oder simuliert eine erfolgreiche Recovery-Zahlung.';
|
||
|
||
public function handle(): int
|
||
{
|
||
$abo = $this->resolveAbo();
|
||
if (! $abo) {
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$this->info('=== Abo-Recovery-Test ===');
|
||
$this->showAbo($abo, 'Ausgangszustand');
|
||
|
||
if ($this->option('fail')) {
|
||
$abo->forceFill(['status' => 3, 'active' => true])->save();
|
||
$this->warn("Abo #{$abo->id} auf Status 3 (fehlgeschlagen) + aktiv gesetzt.");
|
||
}
|
||
|
||
if (! $this->option('no-mail')) {
|
||
$this->sendMail($abo);
|
||
}
|
||
|
||
if ($this->option('advance')) {
|
||
$this->simulateRecovery($abo);
|
||
}
|
||
|
||
$this->newLine();
|
||
$this->info('Fertig.');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
private function resolveAbo(): ?UserAbo
|
||
{
|
||
if ($id = $this->option('abo')) {
|
||
$abo = UserAbo::find($id);
|
||
if (! $abo) {
|
||
$this->error("Abo mit ID {$id} nicht gefunden.");
|
||
|
||
return null;
|
||
}
|
||
|
||
return $abo;
|
||
}
|
||
|
||
$abo = UserAbo::where('status', '>=', 1)->orderByDesc('id')->first();
|
||
if (! $abo) {
|
||
$this->error('Kein Abo gefunden. Bitte --abo=<ID> angeben.');
|
||
|
||
return null;
|
||
}
|
||
|
||
$this->line("Kein --abo angegeben, nutze neuestes Abo #{$abo->id}.");
|
||
|
||
return $abo;
|
||
}
|
||
|
||
private function sendMail(UserAbo $abo): void
|
||
{
|
||
$recipient = $this->option('email') ?: $abo->email;
|
||
if (! $recipient) {
|
||
$this->warn('Keine Empfänger-Adresse (Abo ohne E-Mail, kein --email). Mail übersprungen.');
|
||
|
||
return;
|
||
}
|
||
|
||
$link = (new AboPaymentFailedMail($abo))->build()->viewData['link'] ?? '(kein Link)';
|
||
$locale = $abo->shopping_user?->getLocale() ?: config('app.locale');
|
||
|
||
Mail::to($recipient)->locale($locale)->send(new AboPaymentFailedMail($abo));
|
||
|
||
$this->newLine();
|
||
$this->info('✓ "Zahlung fehlgeschlagen"-Mail versendet');
|
||
$this->table(['Feld', 'Wert'], [
|
||
['Empfänger', $recipient],
|
||
['Sprache', $locale],
|
||
['Abo-Typ', $abo->is_for.($abo->is_for === 'me' ? ' (Berater-CRM)' : ' (Kundenportal)')],
|
||
['Pay-now-Link', $link],
|
||
]);
|
||
$this->line('Tipp: Mail im Mailpit prüfen (mivita-mail.test).');
|
||
}
|
||
|
||
private function simulateRecovery(UserAbo $abo): void
|
||
{
|
||
$this->newLine();
|
||
$this->warn('ACHTUNG: --advance überschreibt payone_userid/carddata dieses Abos mit TEST-Werten.');
|
||
$this->warn('Das echte Abbuchungs-Token geht dabei verloren – nur auf Wegwerf-/Test-Abos ausführen!');
|
||
if (! $this->option('force') && ! $this->confirm("Trotzdem Recovery-Zahlung für Abo #{$abo->id} simulieren?", false)) {
|
||
$this->line('Simulation abgebrochen.');
|
||
|
||
return;
|
||
}
|
||
|
||
$this->warn('Simuliere erfolgreiche Recovery-Zahlung (erzeugt Test-Order/-Payment, KEIN echter Payone-Call)...');
|
||
|
||
$reference = $abo->shopping_user?->shopping_orders()->orderByDesc('id')->first();
|
||
$newUserId = (int) $abo->payone_userid + 1; // sichtbar geändertes Token
|
||
$fakeCard = ['cardpan' => '411111XXXXXX'.random_int(1000, 9999), 'cardtype' => 'V', 'cardexpiredate' => '3012'];
|
||
|
||
$order = ShoppingOrder::create([
|
||
'shopping_user_id' => $abo->shopping_user_id ?: 1,
|
||
'auth_user_id' => $abo->user_id ?: ($reference->auth_user_id ?? 1),
|
||
'country_id' => $reference->country_id ?? 1,
|
||
'user_shop_id' => $reference->user_shop_id ?? 1,
|
||
'recovery_user_abo_id' => $abo->id,
|
||
'is_abo' => true,
|
||
'abo_interval' => $abo->abo_interval,
|
||
'paid' => true,
|
||
'mode' => 'test',
|
||
]);
|
||
|
||
$payment = ShoppingPayment::create([
|
||
'shopping_order_id' => $order->id,
|
||
'clearingtype' => 'cc',
|
||
'wallettype' => null,
|
||
'carddata' => $fakeCard,
|
||
'reference' => 'recovery-test-'.uniqid(),
|
||
'amount' => 19.9,
|
||
'currency' => 'EUR',
|
||
'mode' => 'test',
|
||
'is_abo' => true,
|
||
'abo_interval' => $abo->abo_interval,
|
||
]);
|
||
|
||
PaymentTransaction::create([
|
||
'shopping_payment_id' => $payment->id,
|
||
'userid' => $newUserId,
|
||
'txid' => 'test-'.uniqid(),
|
||
'status' => 'APPROVED',
|
||
'txaction' => 'appointed',
|
||
'mode' => 'test',
|
||
]);
|
||
|
||
AboHelper::advanceAboAfterRecoveryPayment($payment->fresh());
|
||
|
||
$this->showAbo($abo->fresh(), 'Nach Recovery-Zahlung');
|
||
$this->info('✓ Abo fortgeschrieben (Status→2, next_date gesetzt, Zahlart/Token aktualisiert).');
|
||
}
|
||
|
||
private function showAbo(UserAbo $abo, string $title): void
|
||
{
|
||
$this->newLine();
|
||
$this->line("<info>{$title}</info> — Abo #{$abo->id}");
|
||
$this->table(['Feld', 'Wert'], [
|
||
['Status', $abo->status.' ('.($abo->getStatusType() ?? '?').')'],
|
||
['Aktiv', $abo->active ? 'Ja' : 'Nein'],
|
||
['is_for', $abo->is_for],
|
||
['E-Mail', $abo->email],
|
||
['clearingtype', $abo->clearingtype ?? '-'],
|
||
['wallettype', $abo->wallettype ?? '-'],
|
||
['payone_userid', $abo->payone_userid],
|
||
['carddata (maskiert)', $abo->maskedCard() ?? '-'],
|
||
['next_date', $abo->next_date],
|
||
['last_date', $abo->last_date],
|
||
]);
|
||
}
|
||
}
|