- 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>
155 lines
6.1 KiB
PHP
155 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\ShoppingInstance;
|
|
use App\Models\ShoppingUser;
|
|
use App\Models\UserAbo;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Yard;
|
|
|
|
class AboRecoveryCheckoutService
|
|
{
|
|
/**
|
|
* Baut für ein fehlgeschlagenes (Status 3, aktives) Abo einen Recovery-Checkout auf:
|
|
* Warenkorb aus den aktuellen Abo-Artikeln, eine ShoppingInstance und die Checkout-URL,
|
|
* über die der Inhaber die offene Folgerechnung mit freier (abo-fähiger) Zahlart begleicht.
|
|
*
|
|
* Die Attribution wird durch Nachbilden der bewährten Checkout-Pfade korrekt gehalten:
|
|
* - Berater-eigenes Abo (is_for=me) → is_from=user_order, is_for=abo-me (payment=2, auth_user)
|
|
* - Kunden-Abo (is_for=ot) → is_from=shopping, is_for=abo-ot-customer (payment=6)
|
|
*
|
|
* @return string|null Checkout-URL oder null, wenn der Aufbau fehlschlug.
|
|
*/
|
|
public function buildCheckoutUrl(UserAbo $userAbo): ?string
|
|
{
|
|
AboHelper::ensureUserAboItemsFromLatestOrder($userAbo);
|
|
|
|
Yard::instance(AboOrderCart::INSTANCE)->destroy();
|
|
AboOrderCart::initYard($userAbo);
|
|
AboOrderCart::makeOrderYard($userAbo);
|
|
$yard = Yard::instance(AboOrderCart::INSTANCE);
|
|
|
|
if ($yard->content()->isEmpty()) {
|
|
Log::channel('abo_order')->error('AboRecoveryCheckoutService: Leerer Warenkorb, Abbruch', ['abo_id' => $userAbo->id]);
|
|
|
|
return null;
|
|
}
|
|
|
|
$shoppingUser = $userAbo->shopping_user;
|
|
$referenceOrder = $shoppingUser?->shopping_orders()->orderByDesc('id')->first();
|
|
|
|
$isMe = $userAbo->is_for === 'me';
|
|
$consultant = $isMe ? $userAbo->user : $userAbo->member;
|
|
$consultantShop = $consultant?->shop;
|
|
$userShopId = $referenceOrder?->user_shop_id ?? $consultantShop?->id;
|
|
|
|
if (! $userShopId) {
|
|
Log::channel('abo_order')->error('AboRecoveryCheckoutService: Kein user_shop_id ermittelbar', ['abo_id' => $userAbo->id]);
|
|
|
|
return null;
|
|
}
|
|
|
|
$identifier = $this->makeIdentifier();
|
|
|
|
ShoppingInstance::create([
|
|
'identifier' => $identifier,
|
|
'user_shop_id' => $userShopId,
|
|
'auth_user_id' => $isMe ? $userAbo->user_id : null,
|
|
'payment' => $isMe ? 2 : 6,
|
|
'subdomain' => $consultantShop?->getSubdomain(false) ?? url('/'),
|
|
'country_id' => $yard->getShippingCountryId() ?? $referenceOrder?->country_id,
|
|
'language' => $shoppingUser?->getLocale() ?? \App::getLocale(),
|
|
'amount' => $yard->totalWithShipping(2, '.', ''),
|
|
'status' => 0,
|
|
'shopping_user_id' => $userAbo->shopping_user_id,
|
|
'shopping_data' => $this->buildShoppingData($userAbo, $shoppingUser, $yard, $isMe),
|
|
'back' => $userAbo->selfServiceDetailUrl(),
|
|
]);
|
|
|
|
$yard->store($identifier);
|
|
|
|
return route('checkout.checkout_card', ['identifier' => $identifier]);
|
|
}
|
|
|
|
private function makeIdentifier(): string
|
|
{
|
|
do {
|
|
$identifier = Util::getToken();
|
|
} while (ShoppingInstance::where('identifier', $identifier)->exists());
|
|
|
|
return $identifier;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildShoppingData(UserAbo $userAbo, ?ShoppingUser $shoppingUser, mixed $yard, bool $isMe): array
|
|
{
|
|
$data = [
|
|
'is_from' => $isMe ? 'user_order' : 'shopping',
|
|
'is_for' => $isMe ? 'abo-me' : 'abo-ot-customer',
|
|
'is_abo' => true,
|
|
'abo_interval' => (int) $userAbo->abo_interval,
|
|
'recovery_user_abo_id' => $userAbo->id,
|
|
'shopping_user_id' => $userAbo->shopping_user_id,
|
|
'user_price_infos' => $yard->getUserPriceInfos(),
|
|
'mode' => config('app.mode') === 'test' ? 'test' : 'live',
|
|
];
|
|
|
|
if ($shoppingUser) {
|
|
// Lieferadresse immer vorbelegen (mit Rechnungsadresse als Fallback), damit der Checkout
|
|
// keine erneute Eingabe verlangt: der me-Pfad liest shipping_* aus shopping_data
|
|
// (shoppingUserAuthData erzwingt same_as_billing=true), der ot-Pfad füllt den ShoppingUser
|
|
// komplett aus shopping_data (makeCustomerShoppingUser).
|
|
$data += $this->shippingFrom($shoppingUser);
|
|
|
|
if (! $isMe) {
|
|
$data += $this->billingFrom($shoppingUser);
|
|
$data['member_id'] = $userAbo->member_id;
|
|
$data['same_as_billing'] = $shoppingUser->same_as_billing;
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function shippingFrom(ShoppingUser $u): array
|
|
{
|
|
return [
|
|
'shipping_salutation' => $u->shipping_salutation ?: $u->billing_salutation,
|
|
'shipping_company' => $u->shipping_company ?: $u->billing_company,
|
|
'shipping_firstname' => $u->shipping_firstname ?: $u->billing_firstname,
|
|
'shipping_lastname' => $u->shipping_lastname ?: $u->billing_lastname,
|
|
'shipping_address' => $u->shipping_address ?: $u->billing_address,
|
|
'shipping_address_2' => $u->shipping_address_2 ?: $u->billing_address_2,
|
|
'shipping_zipcode' => $u->shipping_zipcode ?: $u->billing_zipcode,
|
|
'shipping_city' => $u->shipping_city ?: $u->billing_city,
|
|
'shipping_phone' => $u->shipping_phone ?: $u->billing_phone,
|
|
'shipping_postnumber' => $u->shipping_postnumber,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function billingFrom(ShoppingUser $u): array
|
|
{
|
|
return [
|
|
'billing_salutation' => $u->billing_salutation,
|
|
'billing_company' => $u->billing_company,
|
|
'billing_firstname' => $u->billing_firstname,
|
|
'billing_lastname' => $u->billing_lastname,
|
|
'billing_address' => $u->billing_address,
|
|
'billing_address_2' => $u->billing_address_2,
|
|
'billing_zipcode' => $u->billing_zipcode,
|
|
'billing_city' => $u->billing_city,
|
|
'billing_country_id' => $u->billing_country_id,
|
|
'billing_phone' => $u->billing_phone,
|
|
'billing_email' => $u->billing_email,
|
|
];
|
|
}
|
|
}
|