mivita/app/Console/Commands/RepairAboOrderUserAssignment.php
Kevin 9b7665c59a Abo-Wartungs-Commands & Ausfuehrungs-Reminder aktivieren
Neue Artisan-Commands CreateAboReplacementOrder (Ersatzbestellung fuer ein
Abo anlegen) und RepairAboOrderUserAssignment (fehlerhafte User-Zuordnung
von Abo-Bestellungen korrigieren), jeweils mit Feature-Tests.
Ausserdem den abo:send-execution-reminder-Schedule (06:00) aktiviert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:37:39 +00:00

252 lines
11 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\ShippingCountry;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserSalesVolume;
use App\Repositories\InvoiceRepository;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RepairAboOrderUserAssignment extends Command
{
protected $signature = 'abo:repair-order-user
{--fix : Reparatur ausfuehren (ohne: nur Vorschau)}
{--force : Mit --fix ohne Rueckfrage}
{--abo=3 : user_abos.id, Standard ist der Live-Fall}
{--user=455 : Ziel users.id, Standard ist der Live-Fall}
{--order= : Konkrete shopping_orders.id (ohne: letzte Bestellung zum Abo)}
{--old-user= : Erwartete falsche users.id zur zusaetzlichen Absicherung}
{--no-documents : Rechnung/Lieferschein nicht neu rendern}';
protected $description = 'Korrigiert die letzte Abo-Bestellung, wenn ein Abo mit falschem User ausgefuehrt wurde';
public function handle(): int
{
$userAbo = UserAbo::with(['user.account', 'shopping_user'])->find((int) $this->option('abo'));
if (! $userAbo) {
$this->error('UserAbo wurde nicht gefunden.');
return self::FAILURE;
}
$targetUser = User::with('account')->find((int) $this->option('user'));
if (! $targetUser || ! $targetUser->account) {
$this->error('Ziel-User wurde nicht gefunden oder hat keinen Account.');
return self::FAILURE;
}
$userAboOrder = $this->resolveUserAboOrder($userAbo);
if (! $userAboOrder || ! $userAboOrder->shopping_order) {
$this->error('Keine passende Abo-Bestellung gefunden.');
return self::FAILURE;
}
$order = $userAboOrder->shopping_order;
$order->loadMissing(['shopping_user', 'user_invoices', 'user_sales_volume']);
if ($this->option('old-user') !== null && (int) $order->auth_user_id !== (int) $this->option('old-user')) {
$this->error('Abbruch: auth_user_id der Bestellung entspricht nicht --old-user.');
return self::FAILURE;
}
$this->printPreview($userAbo, $order, $targetUser);
if (! $this->option('fix')) {
$this->warn('Trockenlauf. Nutze --fix zur Reparatur.');
return self::SUCCESS;
}
if (! $this->option('force') && ! $this->confirm('Diese Abo-Bestellung wirklich auf den Ziel-User korrigieren?')) {
return self::SUCCESS;
}
$salesVolumeRecalculations = [];
$newShoppingUserId = null;
DB::transaction(function () use ($userAbo, $order, $targetUser, &$salesVolumeRecalculations, &$newShoppingUserId) {
$lockedAbo = UserAbo::whereKey($userAbo->id)->lockForUpdate()->firstOrFail();
$lockedOrder = ShoppingOrder::whereKey($order->id)->lockForUpdate()->firstOrFail();
$newShoppingUser = $this->makeShoppingUserSnapshot($lockedAbo, $lockedOrder, $targetUser);
$newShoppingUser->save();
$newShoppingUserId = $newShoppingUser->id;
$lockedAbo->update([
'user_id' => $targetUser->id,
'email' => $targetUser->email,
]);
$lockedOrder->update([
'shopping_user_id' => $newShoppingUser->id,
'auth_user_id' => $targetUser->id,
'member_id' => $lockedAbo->member_id,
'country_id' => $this->resolveShippingCountryId($targetUser, $lockedOrder),
'language' => $targetUser->getLocale(),
]);
UserSalesVolume::where('shopping_order_id', $lockedOrder->id)
->get()
->each(function (UserSalesVolume $salesVolume) use ($targetUser, &$salesVolumeRecalculations) {
$salesVolumeRecalculations[] = [
'user_id' => $salesVolume->user_id,
'month' => $salesVolume->month,
'year' => $salesVolume->year,
];
$salesVolume->user_id = $targetUser->id;
$salesVolume->save();
$salesVolumeRecalculations[] = [
'user_id' => $targetUser->id,
'month' => $salesVolume->month,
'year' => $salesVolume->year,
];
});
});
$this->recalculateSalesVolumes($salesVolumeRecalculations);
$this->info("Daten korrigiert. Neuer ShoppingUser: {$newShoppingUserId}");
if (! $this->option('no-documents')) {
$this->regenerateDocuments($order->fresh(['user_invoice', 'shopping_user', 'shopping_order_items', 'shopping_payments.payment_transactions']));
} else {
$this->warn('Dokumente wurden wegen --no-documents nicht neu gerendert.');
}
return self::SUCCESS;
}
private function resolveUserAboOrder(UserAbo $userAbo): ?UserAboOrder
{
$query = UserAboOrder::with('shopping_order')
->where('user_abo_id', $userAbo->id);
if ($this->option('order')) {
$query->where('shopping_order_id', (int) $this->option('order'));
}
return $query->orderByDesc('id')->first();
}
private function printPreview(UserAbo $userAbo, ShoppingOrder $order, User $targetUser): void
{
$this->table(
['Feld', 'Aktuell', 'Neu'],
[
['user_abo_id', $userAbo->id, $userAbo->id],
['shopping_order_id', $order->id, $order->id],
['user_abos.user_id', $userAbo->user_id, $targetUser->id],
['shopping_orders.auth_user_id', $order->auth_user_id, $targetUser->id],
['shopping_orders.member_id', $order->member_id ?: '-', $userAbo->member_id ?: '-'],
['shopping_orders.shopping_user_id', $order->shopping_user_id, 'neuer Snapshot'],
['Rechnungen/Lieferscheine', $order->user_invoices->count(), $this->option('no-documents') ? 'nicht neu rendern' : 'neu rendern'],
['SalesVolume-Eintraege', UserSalesVolume::where('shopping_order_id', $order->id)->count(), 'user_id aktualisieren'],
]
);
}
private function makeShoppingUserSnapshot(UserAbo $userAbo, ShoppingOrder $order, User $targetUser): ShoppingUser
{
$account = $targetUser->account;
$source = $order->shopping_user ?: $userAbo->shopping_user;
$shoppingUser = $source ? $source->replicate() : new ShoppingUser;
$shoppingUser->auth_user_id = $targetUser->id;
$shoppingUser->member_id = $userAbo->member_id;
$shoppingUser->billing_salutation = $account->salutation;
$shoppingUser->billing_company = $account->company;
$shoppingUser->billing_firstname = $account->first_name;
$shoppingUser->billing_lastname = $account->last_name;
$shoppingUser->billing_address = $account->address;
$shoppingUser->billing_address_2 = $account->address_2;
$shoppingUser->billing_zipcode = $account->zipcode;
$shoppingUser->billing_city = $account->city;
$shoppingUser->billing_country_id = $account->country_id;
$shoppingUser->billing_phone = $account->phone;
$shoppingUser->billing_email = $targetUser->email;
$shoppingUser->language = $targetUser->getLocale();
$shoppingUser->is_for = $userAbo->is_for;
$shoppingUser->is_from = $shoppingUser->is_from ?: 'user_order';
$shoppingUser->mode = $order->mode;
if ($account->same_as_billing) {
$shoppingUser->same_as_billing = true;
$shoppingUser->shipping_salutation = $account->salutation;
$shoppingUser->shipping_company = $account->company;
$shoppingUser->shipping_firstname = $account->first_name;
$shoppingUser->shipping_lastname = $account->last_name;
$shoppingUser->shipping_address = $account->address;
$shoppingUser->shipping_address_2 = $account->address_2;
$shoppingUser->shipping_zipcode = $account->zipcode;
$shoppingUser->shipping_city = $account->city;
$shoppingUser->shipping_country_id = $account->country_id;
$shoppingUser->shipping_phone = $account->phone;
$shoppingUser->shipping_postnumber = null;
} else {
$shoppingUser->same_as_billing = false;
$shoppingUser->shipping_salutation = $account->shipping_salutation;
$shoppingUser->shipping_company = $account->shipping_company;
$shoppingUser->shipping_firstname = $account->shipping_firstname;
$shoppingUser->shipping_lastname = $account->shipping_lastname;
$shoppingUser->shipping_address = $account->shipping_address;
$shoppingUser->shipping_address_2 = $account->shipping_address_2;
$shoppingUser->shipping_zipcode = $account->shipping_zipcode;
$shoppingUser->shipping_city = $account->shipping_city;
$shoppingUser->shipping_country_id = $account->shipping_country_id;
$shoppingUser->shipping_phone = $account->shipping_phone;
$shoppingUser->shipping_postnumber = $account->shipping_postnumber;
}
return $shoppingUser;
}
private function resolveShippingCountryId(User $targetUser, ShoppingOrder $order): int
{
$account = $targetUser->account;
$countryId = $account->same_as_billing ? $account->country_id : $account->shipping_country_id;
$shippingCountry = ShippingCountry::where('country_id', $countryId)->first();
return $shippingCountry?->id ?? $order->country_id;
}
/**
* @param list<array{user_id: int|null, month: int|null, year: int|null}> $recalculations
*/
private function recalculateSalesVolumes(array $recalculations): void
{
collect($recalculations)
->filter(fn (array $item) => $item['user_id'] && $item['month'] && $item['year'])
->unique(fn (array $item) => $item['user_id'].'-'.$item['month'].'-'.$item['year'])
->each(fn (array $item) => SalesPointsVolume::reCalculateSalesPointsVolume(
(int) $item['user_id'],
(int) $item['month'],
(int) $item['year']
));
}
private function regenerateDocuments(ShoppingOrder $order): void
{
if (! $order->user_invoice) {
$this->warn('Keine Rechnung gefunden, Dokumente wurden nicht neu gerendert.');
return;
}
$invoiceRepository = new InvoiceRepository($order);
$invoiceRepository->update(['invoice_send_mail' => false]);
$this->info('Rechnung und Lieferschein wurden neu gerendert.');
}
}