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>
252 lines
11 KiB
PHP
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.');
|
|
}
|
|
}
|