Strangler-Refactoring der PAYONE-Zahlungslogik in eine testbare
Service-Schicht, ohne Verhalten in Checkout/Cron/IPN zu ändern.
Neue Services (app/Services/Payment/Payone):
- PayonePaymentService (+ PayonePaymentResult-DTO) für CC-/SEPA-Check
und Abo-Abbuchung
- PayoneRequestBuilder, PaymentMethodResolver, PayoneMethodConfig
- PayoneTransactionRecorder: zentrale PaymentTransaction-Erstellung
(API-Response, Invoice, Webhook) inkl. PCI-Redaction (cardpan/cardcvc2)
- PayoneWebhookHandler (+ PayoneWebhookResult): IPN/TSOK-Logik aus
Api\PayoneController; response('TSOK') statt echo/exit
- PayoneTransportException + Handler-Mapping (403) statt abort() im Service
Hotfixes (R1): var_dump/exit & bare exit() entfernt, Log-Redaction.
Security (R-Review):
- PAYONE_KEY_HASH (MD5-Hash, kein Klartext) mit Boot-Hard-Fail; schließt
zugleich die ''-Webhook-Bypass-Lücke. Artisan-Command payone:hash-key
(verdeckte Eingabe, optional --write) zur Hash-Erzeugung.
- Webhook: txaction Pflichtfeld, mode-Fallback auf payment->mode.
Pay\PayoneController ist @deprecated-Fassade (614 → 328 Zeilen),
delegiert an Services via Container-Singleton.
Migration: fehlende Spalte shopping_payments.carddata nachgezogen.
Tests: 47 grün (PayonePaymentService, Recorder, Webhook, Resolver,
TransportException, hash-key). End-to-End im Test-System verifiziert
(siehe dev/2026-06-09-payments).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
86 lines
2.6 KiB
PHP
86 lines
2.6 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use Illuminate\Console\Command;
|
||
|
||
use function Laravel\Prompts\confirm;
|
||
use function Laravel\Prompts\password;
|
||
|
||
class PayoneHashKey extends Command
|
||
{
|
||
protected $signature = 'payone:hash-key
|
||
{--write : Schreibt den erzeugten Hash als PAYONE_KEY_HASH in die .env}';
|
||
|
||
protected $description = 'Erzeugt aus dem PAYONE-Portal-Key (Klartext) den benötigten MD5-Hash für PAYONE_KEY_HASH';
|
||
|
||
public function handle(): int
|
||
{
|
||
$this->components->info('Der Klartext-Key wird nur flüchtig zur Hash-Erzeugung verwendet und nirgends gespeichert oder geloggt.');
|
||
|
||
$plainKey = trim((string) password(
|
||
label: 'PAYONE-Portal-Key (Klartext, verdeckte Eingabe)',
|
||
required: true,
|
||
));
|
||
|
||
if ($plainKey === '') {
|
||
$this->components->error('Es wurde kein Key eingegeben.');
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$hash = hash('md5', $plainKey);
|
||
|
||
$this->newLine();
|
||
$this->components->twoColumnDetail('PAYONE_KEY_HASH', $hash);
|
||
$this->newLine();
|
||
|
||
if (! $this->option('write')) {
|
||
$this->line(' Ohne <comment>--write</comment>: Wert manuell als <comment>PAYONE_KEY_HASH</comment> in die .env übernehmen.');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
if (! confirm("PAYONE_KEY_HASH in {$this->envPath()} schreiben/aktualisieren?", default: true)) {
|
||
$this->components->warn('Abgebrochen – .env wurde nicht verändert.');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
if (! $this->writeHashToEnv($hash)) {
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$this->components->info('PAYONE_KEY_HASH in die .env geschrieben.');
|
||
$this->line(' Danach <comment>php artisan config:clear</comment> ausführen (bzw. config neu cachen).');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
private function envPath(): string
|
||
{
|
||
return base_path('.env');
|
||
}
|
||
|
||
private function writeHashToEnv(string $hash): bool
|
||
{
|
||
$path = $this->envPath();
|
||
|
||
if (! is_file($path) || ! is_writable($path)) {
|
||
$this->components->error(".env nicht gefunden oder nicht beschreibbar: {$path}");
|
||
|
||
return false;
|
||
}
|
||
|
||
$contents = (string) file_get_contents($path);
|
||
$line = 'PAYONE_KEY_HASH='.$hash;
|
||
|
||
if (preg_match('/^PAYONE_KEY_HASH=.*$/m', $contents)) {
|
||
$contents = preg_replace('/^PAYONE_KEY_HASH=.*$/m', $line, $contents);
|
||
} else {
|
||
$contents = rtrim($contents, "\n")."\n".$line."\n";
|
||
}
|
||
|
||
return file_put_contents($path, $contents) !== false;
|
||
}
|
||
}
|