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>
This commit is contained in:
parent
2b2847add1
commit
9b7665c59a
5 changed files with 1156 additions and 1 deletions
429
app/Console/Commands/CreateAboReplacementOrder.php
Normal file
429
app/Console/Commands/CreateAboReplacementOrder.php
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShippingPrice;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserInvoice;
|
||||
use App\Repositories\InvoiceRepository;
|
||||
use App\Services\BusinessPlan\SalesPointsVolume;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateAboReplacementOrder extends Command
|
||||
{
|
||||
protected $signature = 'abo:create-replacement-order
|
||||
{--source-order=55435 : Stornierte Original-shopping_orders.id}
|
||||
{--abo=3 : user_abos.id fuer die neue UserAboOrder-Verknuepfung}
|
||||
{--user=455 : Ziel users.id}
|
||||
{--invoice-date= : Rechnungsdatum der Ersatzrechnung (d.m.Y oder Y-m-d, Standard: heute)}
|
||||
{--fix : Ersatzbestellung erstellen (ohne: nur Vorschau)}
|
||||
{--force : Mit --fix ohne Rueckfrage}
|
||||
{--send-mail : Ersatzrechnung per Mail versenden}
|
||||
{--no-invoice : Nur Ersatzbestellung ohne Rechnung/SalesVolume erstellen (nur Test/Diagnose)}';
|
||||
|
||||
protected $description = 'Erstellt nach einer Stornorechnung eine korrekt berechnete Ersatzbestellung ohne neue Payone-Zahlung';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$sourceOrder = ShoppingOrder::with([
|
||||
'shopping_user',
|
||||
'shopping_order_items.product',
|
||||
'shopping_payments.payment_transactions',
|
||||
'user_invoice',
|
||||
'user_cancellation_invoice',
|
||||
])->find((int) $this->option('source-order'));
|
||||
|
||||
if (! $sourceOrder) {
|
||||
$this->error('Originalbestellung wurde nicht gefunden.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$userAbo = UserAbo::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;
|
||||
}
|
||||
|
||||
if (! $sourceOrder->isCancellationInvoice()) {
|
||||
$this->error('Abbruch: Zur Originalbestellung existiert noch keine Stornorechnung.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($existingReplacement = $this->findExistingReplacement($sourceOrder)) {
|
||||
$this->error("Abbruch: Es existiert bereits eine Ersatzbestellung #{$existingReplacement->id}.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$calculation = $this->calculateReplacement($sourceOrder, $userAbo);
|
||||
$invoiceDate = $this->resolveInvoiceDate();
|
||||
|
||||
$this->printPreview($sourceOrder, $calculation, $invoiceDate);
|
||||
|
||||
if (! $this->option('fix')) {
|
||||
$this->warn('Trockenlauf. Nutze --fix zur Erstellung der Ersatzbestellung.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $this->option('force') && ! $this->confirm('Ersatzbestellung und Ersatzrechnung wirklich erstellen?')) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$replacementOrder = null;
|
||||
$replacementInvoice = null;
|
||||
|
||||
DB::transaction(function () use ($sourceOrder, $userAbo, $targetUser, $calculation, $invoiceDate, &$replacementOrder, &$replacementInvoice) {
|
||||
$replacementOrder = $this->createReplacementOrder($sourceOrder, $targetUser, $calculation);
|
||||
$this->createReplacementItems($sourceOrder, $replacementOrder, $calculation);
|
||||
$this->createReplacementPayment($sourceOrder, $replacementOrder, $calculation, $invoiceDate);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $replacementOrder->id,
|
||||
'status' => 2,
|
||||
'paid' => true,
|
||||
]);
|
||||
|
||||
if (! $this->option('no-invoice')) {
|
||||
$replacementInvoice = $this->createInvoiceAndSalesVolume($replacementOrder, $invoiceDate);
|
||||
}
|
||||
});
|
||||
|
||||
if (! $replacementOrder) {
|
||||
$this->error('Ersatzbestellung konnte nicht erstellt werden.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("Ersatzbestellung erstellt: #{$replacementOrder->id}");
|
||||
if ($replacementInvoice) {
|
||||
$this->info("Ersatzrechnung erstellt: {$replacementInvoice->full_number}");
|
||||
} else {
|
||||
$this->warn('Keine Ersatzrechnung erstellt (--no-invoice).');
|
||||
}
|
||||
|
||||
$difference = round((float) $sourceOrder->total_shipping - $calculation['total_shipping'], 2);
|
||||
if ($difference > 0) {
|
||||
$this->warn('Zu erstattender Differenzbetrag: '.number_format($difference, 2, ',', '.').' EUR');
|
||||
} elseif ($difference < 0) {
|
||||
$this->warn('Nachzuberechnender Differenzbetrag: '.number_format(abs($difference), 2, ',', '.').' EUR');
|
||||
} else {
|
||||
$this->info('Keine Zahlungsdifferenz.');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function findExistingReplacement(ShoppingOrder $sourceOrder): ?ShoppingOrder
|
||||
{
|
||||
return ShoppingOrder::query()
|
||||
->where('customer_order_source_comment', 'like', '%replacement_for_order:'.$sourceOrder->id.'%')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* items: list<array<string, mixed>>,
|
||||
* subtotal: float,
|
||||
* shipping: float,
|
||||
* shipping_net: float,
|
||||
* subtotal_ws: float,
|
||||
* tax: float,
|
||||
* total: float,
|
||||
* total_shipping: float,
|
||||
* points: float,
|
||||
* weight: int,
|
||||
* shipping_price_id: int|null
|
||||
* }
|
||||
*/
|
||||
private function calculateReplacement(ShoppingOrder $sourceOrder, UserAbo $userAbo): array
|
||||
{
|
||||
$items = [];
|
||||
$subtotal = 0.0;
|
||||
$points = 0.0;
|
||||
$weight = 0;
|
||||
|
||||
foreach ($sourceOrder->shopping_order_items as $item) {
|
||||
$qty = (int) $item->qty;
|
||||
$unitNet = round((float) ($item->price_net ?: $item->price), 2);
|
||||
$lineNet = round($unitNet * $qty, 2);
|
||||
$linePoints = (float) $item->points * $qty;
|
||||
$lineWeight = (int) ($item->product?->weight ?? 0) * $qty;
|
||||
|
||||
$items[] = [
|
||||
'source_item_id' => $item->id,
|
||||
'row_id' => $item->row_id,
|
||||
'product_id' => $item->product_id,
|
||||
'homeparty_id' => $item->homeparty_id,
|
||||
'shopping_collect_order_id' => $item->shopping_collect_order_id,
|
||||
'comp' => $item->comp,
|
||||
'qty' => $qty,
|
||||
'price' => $unitNet,
|
||||
'price_net' => $unitNet,
|
||||
'tax_rate' => 0.0,
|
||||
'tax' => 0.0,
|
||||
'price_vk_net' => $item->price_vk_net,
|
||||
'discount' => $item->discount,
|
||||
'points' => $item->points,
|
||||
'slug' => $item->slug,
|
||||
];
|
||||
|
||||
$subtotal += $lineNet;
|
||||
$points += $linePoints;
|
||||
$weight += $lineWeight;
|
||||
}
|
||||
|
||||
$shipping = $this->resolveShipping($sourceOrder, $userAbo, $weight);
|
||||
$shippingNet = round($shipping['price'] / ((100 + $shipping['tax_rate']) / 100), 2);
|
||||
$subtotal = round($subtotal, 2);
|
||||
$subtotalWithShipping = round($subtotal + $shippingNet, 2);
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'subtotal' => $subtotal,
|
||||
'shipping' => round($shipping['price'], 2),
|
||||
'shipping_net' => $shippingNet,
|
||||
'subtotal_ws' => $subtotalWithShipping,
|
||||
'tax' => 0.0,
|
||||
'total' => $subtotal,
|
||||
'total_shipping' => $subtotalWithShipping,
|
||||
'points' => round($points, 2),
|
||||
'weight' => $weight,
|
||||
'shipping_price_id' => $shipping['shipping_price_id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{price: float, tax_rate: float, shipping_price_id: int|null}
|
||||
*/
|
||||
private function resolveShipping(ShoppingOrder $sourceOrder, UserAbo $userAbo, int $weight): array
|
||||
{
|
||||
$shippingCountry = ShippingCountry::with('shipping.shipping_prices')->findOrFail($sourceOrder->country_id);
|
||||
$shippingPrices = $shippingCountry->shipping?->shipping_prices ?? collect();
|
||||
|
||||
$shippingPrice = $shippingPrices
|
||||
->first(fn (ShippingPrice $price) => $weight >= (int) $price->weight_from && $weight <= (int) $price->weight_to);
|
||||
|
||||
if (! $shippingPrice) {
|
||||
$shippingPrice = $shippingPrices->sortByDesc('weight_to')->first();
|
||||
}
|
||||
|
||||
if (! $shippingPrice) {
|
||||
throw new \RuntimeException('Keine Versandpreisstufe fuer das Versandland gefunden.');
|
||||
}
|
||||
|
||||
$useCompPrice = $sourceOrder->is_abo && $userAbo->is_for === 'me';
|
||||
|
||||
return [
|
||||
'price' => (float) ($useCompPrice ? $shippingPrice->price_comp : $shippingPrice->price),
|
||||
'tax_rate' => (float) $shippingPrice->tax_rate,
|
||||
'shipping_price_id' => $shippingPrice->id,
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveInvoiceDate(): string
|
||||
{
|
||||
if ($this->option('invoice-date')) {
|
||||
return Carbon::parse($this->option('invoice-date'))->format('d.m.Y');
|
||||
}
|
||||
|
||||
return now()->format('d.m.Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $calculation
|
||||
*/
|
||||
private function printPreview(ShoppingOrder $sourceOrder, array $calculation, string $invoiceDate): void
|
||||
{
|
||||
$difference = round((float) $sourceOrder->total_shipping - (float) $calculation['total_shipping'], 2);
|
||||
|
||||
$this->table(
|
||||
['Feld', 'Stornierte Originalorder', 'Ersatzorder'],
|
||||
[
|
||||
['shopping_order_id', $sourceOrder->id, 'neu'],
|
||||
['Rechnungsdatum', $sourceOrder->user_invoice?->date ?? '-', $invoiceDate],
|
||||
['Positionen', $sourceOrder->shopping_order_items->count(), count($calculation['items'])],
|
||||
['Gewicht', (int) $sourceOrder->weight.' g', $calculation['weight'].' g'],
|
||||
['Artikel netto', number_format((float) $sourceOrder->subtotal, 2, ',', '.').' EUR', number_format($calculation['subtotal'], 2, ',', '.').' EUR'],
|
||||
['Porto brutto', number_format((float) $sourceOrder->shipping, 2, ',', '.').' EUR', number_format($calculation['shipping'], 2, ',', '.').' EUR'],
|
||||
['Porto netto', number_format((float) $sourceOrder->shipping_net, 2, ',', '.').' EUR', number_format($calculation['shipping_net'], 2, ',', '.').' EUR'],
|
||||
['MwSt.', number_format((float) $sourceOrder->tax, 2, ',', '.').' EUR', '0,00 EUR'],
|
||||
['Gesamt', number_format((float) $sourceOrder->total_shipping, 2, ',', '.').' EUR', number_format($calculation['total_shipping'], 2, ',', '.').' EUR'],
|
||||
['Differenz', '-', number_format($difference, 2, ',', '.').' EUR'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $calculation
|
||||
*/
|
||||
private function createReplacementOrder(ShoppingOrder $sourceOrder, User $targetUser, array $calculation): ShoppingOrder
|
||||
{
|
||||
return ShoppingOrder::create([
|
||||
'shopping_user_id' => $sourceOrder->shopping_user_id,
|
||||
'auth_user_id' => $targetUser->id,
|
||||
'member_id' => $sourceOrder->member_id,
|
||||
'homeparty_id' => $sourceOrder->homeparty_id,
|
||||
'payment_for' => $sourceOrder->payment_for,
|
||||
'country_id' => $sourceOrder->country_id,
|
||||
'language' => $sourceOrder->language ?: $targetUser->getLocale(),
|
||||
'user_shop_id' => $sourceOrder->user_shop_id,
|
||||
'total' => $calculation['total'],
|
||||
'subtotal' => $calculation['subtotal'],
|
||||
'shipping' => $calculation['shipping'],
|
||||
'shipping_net' => $calculation['shipping_net'],
|
||||
'subtotal_ws' => $calculation['subtotal_ws'],
|
||||
'tax' => $calculation['tax'],
|
||||
'tax_split' => null,
|
||||
'net_split' => null,
|
||||
'total_shipping' => $calculation['total_shipping'],
|
||||
'points' => $calculation['points'],
|
||||
'weight' => $calculation['weight'],
|
||||
'paid' => true,
|
||||
'shipped' => 0,
|
||||
'is_abo' => $sourceOrder->is_abo,
|
||||
'abo_interval' => $sourceOrder->abo_interval,
|
||||
'txaction' => 'extern_paid',
|
||||
'mode' => $sourceOrder->mode,
|
||||
'customer_order_source' => 'other',
|
||||
'customer_order_source_comment' => 'replacement_for_order:'.$sourceOrder->id.'; stornierte Rechnung: '.($sourceOrder->user_invoice?->full_number ?? '-'),
|
||||
'api_notice' => [
|
||||
'replacement_for_order_id' => $sourceOrder->id,
|
||||
'replacement_reason' => 'Korrektur nach falscher User-/Steuer-/Porto-Zuordnung',
|
||||
'shipping_price_id' => $calculation['shipping_price_id'],
|
||||
],
|
||||
'api_status' => $sourceOrder->api_status,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $calculation
|
||||
*/
|
||||
private function createReplacementItems(ShoppingOrder $sourceOrder, ShoppingOrder $replacementOrder, array $calculation): void
|
||||
{
|
||||
foreach ($calculation['items'] as $item) {
|
||||
ShoppingOrderItem::create([
|
||||
'shopping_order_id' => $replacementOrder->id,
|
||||
'row_id' => $item['row_id'] ?: 'replacement-'.$sourceOrder->id.'-'.$item['source_item_id'],
|
||||
'product_id' => $item['product_id'],
|
||||
'homeparty_id' => $item['homeparty_id'],
|
||||
'shopping_collect_order_id' => $item['shopping_collect_order_id'],
|
||||
'comp' => $item['comp'],
|
||||
'qty' => $item['qty'],
|
||||
'price' => $item['price'],
|
||||
'price_net' => $item['price_net'],
|
||||
'tax_rate' => $item['tax_rate'],
|
||||
'tax' => $item['tax'],
|
||||
'price_vk_net' => $item['price_vk_net'],
|
||||
'discount' => $item['discount'],
|
||||
'points' => $item['points'],
|
||||
'slug' => $item['slug'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $calculation
|
||||
*/
|
||||
private function createReplacementPayment(ShoppingOrder $sourceOrder, ShoppingOrder $replacementOrder, array $calculation, string $invoiceDate): ShoppingPayment
|
||||
{
|
||||
$sourcePayment = $sourceOrder->shopping_payments->last();
|
||||
|
||||
$paymentData = [
|
||||
'shopping_order_id' => $replacementOrder->id,
|
||||
'clearingtype' => $sourcePayment?->clearingtype ?? 'cc',
|
||||
'wallettype' => $sourcePayment?->wallettype,
|
||||
'onlinebanktransfertype' => $sourcePayment?->onlinebanktransfertype,
|
||||
'reference' => 'replacement-for-'.$sourceOrder->id,
|
||||
'amount' => (int) round($calculation['total_shipping'] * 100),
|
||||
'currency' => $sourcePayment?->currency ?? 'EUR',
|
||||
];
|
||||
|
||||
foreach (['carddata', 'mode', 'is_abo', 'abo_interval', 'identifier'] as $column) {
|
||||
if (Schema::hasColumn('shopping_payments', $column)) {
|
||||
$paymentData[$column] = match ($column) {
|
||||
'carddata' => $sourcePayment?->carddata,
|
||||
'mode' => $sourceOrder->mode,
|
||||
'is_abo' => $sourceOrder->is_abo,
|
||||
'abo_interval' => $sourceOrder->abo_interval,
|
||||
'identifier' => $sourcePayment?->identifier,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$payment = ShoppingPayment::create($paymentData);
|
||||
|
||||
if (Schema::hasColumn('shopping_payments', 'txaction')) {
|
||||
$payment->txaction = 'extern_paid';
|
||||
}
|
||||
if (Schema::hasColumn('shopping_payments', 'status')) {
|
||||
$payment->status = 'APPROVED';
|
||||
}
|
||||
|
||||
$payment->save();
|
||||
|
||||
$transactionData = [
|
||||
'shopping_payment_id' => $payment->id,
|
||||
'request' => 'replacement',
|
||||
'txid' => 0,
|
||||
'userid' => 0,
|
||||
'status' => 'APPROVED',
|
||||
'txaction' => 'extern_paid',
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('payment_transactions', 'transmitted_data')) {
|
||||
$transactionData['transmitted_data'] = [
|
||||
'source_order_id' => $sourceOrder->id,
|
||||
'source_payment_id' => $sourcePayment?->id,
|
||||
'invoice_date' => $invoiceDate,
|
||||
'note' => 'Technischer Ersatz ohne neue Payone-Abbuchung',
|
||||
];
|
||||
}
|
||||
if (Schema::hasColumn('payment_transactions', 'mode')) {
|
||||
$transactionData['mode'] = $sourceOrder->mode;
|
||||
}
|
||||
|
||||
PaymentTransaction::create($transactionData);
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
private function createInvoiceAndSalesVolume(ShoppingOrder $replacementOrder, string $invoiceDate): UserInvoice
|
||||
{
|
||||
$userSalesVolume = SalesPointsVolume::User($replacementOrder);
|
||||
$invoiceRepository = new InvoiceRepository($replacementOrder->fresh(['shopping_order_items', 'shopping_payments.payment_transactions', 'shopping_user', 'auth_user.account']));
|
||||
$userInvoice = $invoiceRepository->create([
|
||||
'invoice_date' => $invoiceDate,
|
||||
'invoice_send_mail' => $this->option('send-mail'),
|
||||
]);
|
||||
|
||||
$userSalesVolume->user_invoice_id = $userInvoice->id;
|
||||
$userSalesVolume->save();
|
||||
|
||||
IncentiveTracker::trackSalesVolume($userSalesVolume);
|
||||
|
||||
return $userInvoice;
|
||||
}
|
||||
}
|
||||
252
app/Console/Commands/RepairAboOrderUserAssignment.php
Normal file
252
app/Console/Commands/RepairAboOrderUserAssignment.php
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<?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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ class Kernel extends ConsoleKernel
|
|||
// Abo-Einmalprodukte: Reminder-Mail vor der Ausführung (Einmalprodukte-Fenster).
|
||||
// BEWUSST INAKTIV bis zum Abschluss des Review-Gates (Feature aktuell nur VIP
|
||||
// im Sales Center, Portal ausgeblendet). Nach Freigabe einkommentieren:
|
||||
// $schedule->command('abo:send-execution-reminder')->dailyAt('06:00');
|
||||
$schedule->command('abo:send-execution-reminder')->dailyAt('06:00');
|
||||
|
||||
// Abo-Chart-Snapshots: vergangene Monate einfrieren (nach allen Abo-Jobs)
|
||||
$schedule->command('abo:store-chart-snapshots')->dailyAt('04:30');
|
||||
|
|
|
|||
295
tests/Feature/CreateAboReplacementOrderTest.php
Normal file
295
tests/Feature/CreateAboReplacementOrderTest.php
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\Shipping;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShippingPrice;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserAccount;
|
||||
use App\Models\UserInvoice;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function createAboReplacementOrderSeed(): array
|
||||
{
|
||||
$country = Country::create([
|
||||
'code' => 'AT',
|
||||
'phone' => '43',
|
||||
'en' => 'Austria',
|
||||
'de' => 'Österreich',
|
||||
'es' => 'Austria',
|
||||
'fr' => 'Autriche',
|
||||
'it' => 'Austria',
|
||||
'ru' => 'Австрия',
|
||||
'eu_country' => true,
|
||||
]);
|
||||
|
||||
$shipping = Shipping::create([
|
||||
'name' => 'Österreich',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$shippingCountry = ShippingCountry::create([
|
||||
'shipping_id' => $shipping->id,
|
||||
'country_id' => $country->id,
|
||||
]);
|
||||
|
||||
ShippingPrice::create([
|
||||
'shipping_id' => $shipping->id,
|
||||
'price' => 12.90,
|
||||
'price_comp' => 37.00,
|
||||
'tax_rate' => 19.00,
|
||||
'weight_from' => 1,
|
||||
'weight_to' => 20000,
|
||||
]);
|
||||
|
||||
$account = UserAccount::create([
|
||||
'first_name' => 'Marlene',
|
||||
'last_name' => 'Mathis',
|
||||
'address' => 'Oberdorferstrasse 11b',
|
||||
'zipcode' => '6850',
|
||||
'city' => 'Dornbirn',
|
||||
'country_id' => $country->id,
|
||||
'same_as_billing' => false,
|
||||
'shipping_firstname' => 'Marlene',
|
||||
'shipping_lastname' => 'Mathis',
|
||||
'shipping_address' => 'Oberdorferstrasse 11b',
|
||||
'shipping_zipcode' => '6850',
|
||||
'shipping_city' => 'Dornbirn',
|
||||
'shipping_country_id' => $country->id,
|
||||
'reverse_charge' => true,
|
||||
'reverse_charge_valid' => now(),
|
||||
'reverse_charge_code' => 'AT',
|
||||
'tax_identification_number' => 'ATU64282555',
|
||||
'taxable_sales' => 1,
|
||||
]);
|
||||
|
||||
$user = User::forceCreate([
|
||||
'account_id' => $account->id,
|
||||
'email' => 'marlene@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
]);
|
||||
|
||||
$shoppingUser = ShoppingUser::create([
|
||||
'auth_user_id' => $user->id,
|
||||
'billing_firstname' => 'Marlene',
|
||||
'billing_lastname' => 'Mathis',
|
||||
'billing_address' => 'Oberdorferstrasse 11b',
|
||||
'billing_zipcode' => '6850',
|
||||
'billing_city' => 'Dornbirn',
|
||||
'billing_country_id' => $country->id,
|
||||
'billing_email' => $user->email,
|
||||
'same_as_billing' => false,
|
||||
'shipping_firstname' => 'Marlene',
|
||||
'shipping_lastname' => 'Mathis',
|
||||
'shipping_address' => 'Oberdorferstrasse 11b',
|
||||
'shipping_zipcode' => '6850',
|
||||
'shipping_city' => 'Dornbirn',
|
||||
'shipping_country_id' => $country->id,
|
||||
'is_for' => 'me',
|
||||
'is_from' => 'user_order',
|
||||
'mode' => 'live',
|
||||
]);
|
||||
|
||||
$userAbo = UserAbo::create([
|
||||
'user_id' => $user->id,
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'is_for' => 'me',
|
||||
'email' => $user->email,
|
||||
'payone_userid' => 123456,
|
||||
'clearingtype' => 'cc',
|
||||
'active' => true,
|
||||
'status' => 2,
|
||||
'abo_interval' => 5,
|
||||
'next_date' => now()->addDays(3),
|
||||
]);
|
||||
|
||||
$productA = makeProduct(['2'], 119, 19);
|
||||
$productB = makeProduct(['2'], 10.70, 7);
|
||||
|
||||
$sourceOrder = ShoppingOrder::create([
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'auth_user_id' => $user->id,
|
||||
'country_id' => $shippingCountry->id,
|
||||
'language' => 'de',
|
||||
'user_shop_id' => 1,
|
||||
'payment_for' => 3,
|
||||
'total' => 248.70,
|
||||
'subtotal' => 210.00,
|
||||
'shipping' => 39.90,
|
||||
'shipping_net' => 33.53,
|
||||
'subtotal_ws' => 243.53,
|
||||
'tax' => 45.07,
|
||||
'tax_split' => ['19' => '44.37', '7' => '0.70'],
|
||||
'net_split' => ['19' => '233.53', '7' => '10.00'],
|
||||
'total_shipping' => 288.60,
|
||||
'points' => 20,
|
||||
'weight' => 18100,
|
||||
'paid' => true,
|
||||
'shipped' => 10,
|
||||
'is_abo' => true,
|
||||
'abo_interval' => 5,
|
||||
'txaction' => 'cancelled',
|
||||
'mode' => 'live',
|
||||
]);
|
||||
|
||||
ShoppingOrderItem::create([
|
||||
'shopping_order_id' => $sourceOrder->id,
|
||||
'row_id' => 'row-a',
|
||||
'product_id' => $productA->id,
|
||||
'qty' => 2,
|
||||
'price' => 119.00,
|
||||
'price_net' => 100.00,
|
||||
'tax_rate' => 19.00,
|
||||
'tax' => 19.00,
|
||||
'price_vk_net' => 100.00,
|
||||
'discount' => 30.00,
|
||||
'points' => 10,
|
||||
'slug' => $productA->slug,
|
||||
]);
|
||||
|
||||
ShoppingOrderItem::create([
|
||||
'shopping_order_id' => $sourceOrder->id,
|
||||
'row_id' => 'row-b',
|
||||
'product_id' => $productB->id,
|
||||
'qty' => 1,
|
||||
'price' => 10.70,
|
||||
'price_net' => 10.00,
|
||||
'tax_rate' => 7.00,
|
||||
'tax' => 0.70,
|
||||
'price_vk_net' => 10.00,
|
||||
'discount' => 30.00,
|
||||
'points' => 0,
|
||||
'slug' => $productB->slug,
|
||||
]);
|
||||
|
||||
$sourcePayment = ShoppingPayment::create([
|
||||
'shopping_order_id' => $sourceOrder->id,
|
||||
'clearingtype' => 'cc',
|
||||
'reference' => 'source-payment',
|
||||
'amount' => 28860,
|
||||
'currency' => 'EUR',
|
||||
'mode' => 'live',
|
||||
'is_abo' => true,
|
||||
'abo_interval' => 5,
|
||||
]);
|
||||
$sourcePayment->txaction = 'paid';
|
||||
$sourcePayment->status = 'APPROVED';
|
||||
$sourcePayment->save();
|
||||
|
||||
PaymentTransaction::create([
|
||||
'shopping_payment_id' => $sourcePayment->id,
|
||||
'request' => 'authorization',
|
||||
'txid' => 111,
|
||||
'userid' => 222,
|
||||
'status' => 'APPROVED',
|
||||
'txaction' => 'paid',
|
||||
'mode' => 'live',
|
||||
]);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $sourceOrder->id,
|
||||
'status' => 2,
|
||||
'paid' => true,
|
||||
]);
|
||||
|
||||
$originalInvoice = UserInvoice::create([
|
||||
'shopping_order_id' => $sourceOrder->id,
|
||||
'year' => now()->year,
|
||||
'month' => now()->month,
|
||||
'date' => now(),
|
||||
'full_number' => '202600001',
|
||||
'number' => 1,
|
||||
'filename' => 'original.pdf',
|
||||
'dir' => 'invoice/2026/06/',
|
||||
'delivery_filename' => 'delivery.pdf',
|
||||
'delivery_dir' => 'delivery/2026/06/',
|
||||
'disk' => 'public',
|
||||
'cancellation' => true,
|
||||
'cancellation_id' => 2,
|
||||
'cancellation_date' => now(),
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
$cancellationInvoice = UserInvoice::create([
|
||||
'shopping_order_id' => $sourceOrder->id,
|
||||
'year' => now()->year,
|
||||
'month' => now()->month,
|
||||
'date' => now(),
|
||||
'full_number' => '202600002',
|
||||
'number' => 2,
|
||||
'filename' => 'cancellation.pdf',
|
||||
'dir' => 'invoice/2026/06/',
|
||||
'delivery_filename' => 'cancellation-delivery.pdf',
|
||||
'delivery_dir' => 'delivery/2026/06/',
|
||||
'disk' => 'public',
|
||||
'cancellation' => true,
|
||||
'status' => 11,
|
||||
]);
|
||||
|
||||
$originalInvoice->cancellation_id = $cancellationInvoice->id;
|
||||
$originalInvoice->save();
|
||||
|
||||
return [$sourceOrder, $userAbo, $user];
|
||||
}
|
||||
|
||||
it('zeigt die Ersatzbestellung im Trockenlauf ohne Aenderung', function () {
|
||||
[$sourceOrder, $userAbo, $user] = createAboReplacementOrderSeed();
|
||||
|
||||
$exit = Artisan::call('abo:create-replacement-order', [
|
||||
'--source-order' => (string) $sourceOrder->id,
|
||||
'--abo' => (string) $userAbo->id,
|
||||
'--user' => (string) $user->id,
|
||||
]);
|
||||
|
||||
expect($exit)->toBe(0);
|
||||
expect(ShoppingOrder::where('customer_order_source_comment', 'like', '%replacement_for_order:%')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('erstellt eine steuerfreie Ersatzbestellung ohne neue Zahlung auszuloesen', function () {
|
||||
[$sourceOrder, $userAbo, $user] = createAboReplacementOrderSeed();
|
||||
|
||||
$exit = Artisan::call('abo:create-replacement-order', [
|
||||
'--fix' => true,
|
||||
'--force' => true,
|
||||
'--no-invoice' => true,
|
||||
'--source-order' => (string) $sourceOrder->id,
|
||||
'--abo' => (string) $userAbo->id,
|
||||
'--user' => (string) $user->id,
|
||||
]);
|
||||
|
||||
expect($exit)->toBe(0);
|
||||
|
||||
$replacement = ShoppingOrder::where('customer_order_source_comment', 'like', '%replacement_for_order:'.$sourceOrder->id.'%')->first();
|
||||
|
||||
expect($replacement)->not->toBeNull()
|
||||
->and((float) $replacement->subtotal)->toBe(210.00)
|
||||
->and((float) $replacement->shipping)->toBe(37.00)
|
||||
->and((float) $replacement->shipping_net)->toBe(31.09)
|
||||
->and((float) $replacement->tax)->toBe(0.00)
|
||||
->and((float) $replacement->total_shipping)->toBe(241.09)
|
||||
->and($replacement->tax_split)->toBeNull()
|
||||
->and($replacement->net_split)->toBeNull()
|
||||
->and($replacement->txaction)->toBe('extern_paid')
|
||||
->and($replacement->shopping_order_items()->count())->toBe(2)
|
||||
->and(UserAboOrder::where('shopping_order_id', $replacement->id)->where('user_abo_id', $userAbo->id)->exists())->toBeTrue()
|
||||
->and(ShoppingPayment::where('shopping_order_id', $replacement->id)->value('amount'))->toBe(24109);
|
||||
|
||||
$replacement->shopping_order_items->each(function (ShoppingOrderItem $item) {
|
||||
expect((float) $item->tax_rate)->toBe(0.00)
|
||||
->and((float) $item->tax)->toBe(0.00)
|
||||
->and((float) $item->price)->toBe((float) $item->price_net);
|
||||
});
|
||||
});
|
||||
179
tests/Feature/RepairAboOrderUserAssignmentTest.php
Normal file
179
tests/Feature/RepairAboOrderUserAssignmentTest.php
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserAccount;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function repairAboOrderUserAssignmentMakeUser(string $email, array $accountData): User
|
||||
{
|
||||
$country = Country::firstOrFail();
|
||||
$account = UserAccount::create(array_merge([
|
||||
'salutation' => 'mr',
|
||||
'first_name' => 'Vorname',
|
||||
'last_name' => 'Nachname',
|
||||
'address' => 'Alte Strasse 1',
|
||||
'zipcode' => '10000',
|
||||
'city' => 'Berlin',
|
||||
'country_id' => $country->id,
|
||||
'same_as_billing' => true,
|
||||
], $accountData));
|
||||
|
||||
return User::forceCreate([
|
||||
'account_id' => $account->id,
|
||||
'email' => $email,
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
]);
|
||||
}
|
||||
|
||||
function repairAboOrderUserAssignmentSeed(): array
|
||||
{
|
||||
makeShopEnv();
|
||||
|
||||
$oldUser = repairAboOrderUserAssignmentMakeUser('old@example.com', [
|
||||
'first_name' => 'Alter',
|
||||
'last_name' => 'User',
|
||||
'address' => 'Falsche Strasse 1',
|
||||
'zipcode' => '11111',
|
||||
'city' => 'Altstadt',
|
||||
]);
|
||||
|
||||
$targetUser = repairAboOrderUserAssignmentMakeUser('target@example.com', [
|
||||
'first_name' => 'Richtiger',
|
||||
'last_name' => 'User',
|
||||
'address' => 'Richtige Strasse 455',
|
||||
'zipcode' => '45500',
|
||||
'city' => 'Zielstadt',
|
||||
]);
|
||||
|
||||
$wrongShoppingUser = ShoppingUser::create([
|
||||
'auth_user_id' => $oldUser->id,
|
||||
'billing_firstname' => 'Alter',
|
||||
'billing_lastname' => 'User',
|
||||
'billing_address' => 'Falsche Strasse 1',
|
||||
'billing_zipcode' => '11111',
|
||||
'billing_city' => 'Altstadt',
|
||||
'billing_country_id' => Country::firstOrFail()->id,
|
||||
'billing_email' => $oldUser->email,
|
||||
'same_as_billing' => true,
|
||||
'shipping_country_id' => Country::firstOrFail()->id,
|
||||
'is_for' => 'me',
|
||||
'is_from' => 'user_order',
|
||||
'mode' => 'live',
|
||||
]);
|
||||
|
||||
$userAbo = UserAbo::create([
|
||||
'user_id' => $oldUser->id,
|
||||
'shopping_user_id' => $wrongShoppingUser->id,
|
||||
'is_for' => 'me',
|
||||
'email' => $oldUser->email,
|
||||
'payone_userid' => 900100,
|
||||
'clearingtype' => 'cc',
|
||||
'active' => true,
|
||||
'status' => 2,
|
||||
'abo_interval' => 5,
|
||||
'next_date' => now()->addDays(3),
|
||||
]);
|
||||
|
||||
$order = ShoppingOrder::create([
|
||||
'shopping_user_id' => $wrongShoppingUser->id,
|
||||
'auth_user_id' => $oldUser->id,
|
||||
'country_id' => 1,
|
||||
'language' => 'de',
|
||||
'user_shop_id' => 1,
|
||||
'payment_for' => 3,
|
||||
'total' => 100,
|
||||
'subtotal' => 90,
|
||||
'shipping' => 10,
|
||||
'shipping_net' => 8.4,
|
||||
'subtotal_ws' => 98.4,
|
||||
'tax' => 11.6,
|
||||
'total_shipping' => 100,
|
||||
'points' => 10,
|
||||
'is_abo' => true,
|
||||
'abo_interval' => 5,
|
||||
'paid' => true,
|
||||
'txaction' => 'paid',
|
||||
'mode' => 'live',
|
||||
]);
|
||||
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $order->id,
|
||||
'status' => 2,
|
||||
'paid' => true,
|
||||
]);
|
||||
|
||||
UserSalesVolume::create([
|
||||
'user_id' => $oldUser->id,
|
||||
'shopping_order_id' => $order->id,
|
||||
'month' => now()->month,
|
||||
'year' => now()->year,
|
||||
'date' => now(),
|
||||
'points' => 10,
|
||||
'total_net' => 90,
|
||||
'status_points' => 1,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
return [$oldUser, $targetUser, $userAbo, $order, $wrongShoppingUser];
|
||||
}
|
||||
|
||||
it('zeigt die betroffene Abo-Bestellung im Trockenlauf ohne Aenderung', function () {
|
||||
[$oldUser, $targetUser, $userAbo, $order, $wrongShoppingUser] = repairAboOrderUserAssignmentSeed();
|
||||
|
||||
$exit = Artisan::call('abo:repair-order-user', [
|
||||
'--abo' => (string) $userAbo->id,
|
||||
'--user' => (string) $targetUser->id,
|
||||
'--order' => (string) $order->id,
|
||||
'--old-user' => (string) $oldUser->id,
|
||||
'--no-documents' => true,
|
||||
]);
|
||||
|
||||
expect($exit)->toBe(0);
|
||||
expect($order->fresh()->auth_user_id)->toBe($oldUser->id);
|
||||
expect($order->fresh()->shopping_user_id)->toBe($wrongShoppingUser->id);
|
||||
});
|
||||
|
||||
it('korrigiert die letzte Abo-Bestellung auf den Ziel-User', function () {
|
||||
[$oldUser, $targetUser, $userAbo, $order, $wrongShoppingUser] = repairAboOrderUserAssignmentSeed();
|
||||
|
||||
$exit = Artisan::call('abo:repair-order-user', [
|
||||
'--fix' => true,
|
||||
'--force' => true,
|
||||
'--abo' => (string) $userAbo->id,
|
||||
'--user' => (string) $targetUser->id,
|
||||
'--order' => (string) $order->id,
|
||||
'--old-user' => (string) $oldUser->id,
|
||||
'--no-documents' => true,
|
||||
]);
|
||||
|
||||
expect($exit)->toBe(0);
|
||||
|
||||
$order->refresh();
|
||||
$userAbo->refresh();
|
||||
|
||||
expect($userAbo->user_id)->toBe($targetUser->id)
|
||||
->and($userAbo->email)->toBe($targetUser->email)
|
||||
->and($order->auth_user_id)->toBe($targetUser->id)
|
||||
->and($order->shopping_user_id)->not->toBe($wrongShoppingUser->id)
|
||||
->and(UserSalesVolume::where('shopping_order_id', $order->id)->first()->user_id)->toBe($targetUser->id);
|
||||
|
||||
$newShoppingUser = $order->shopping_user;
|
||||
|
||||
expect($newShoppingUser->auth_user_id)->toBe($targetUser->id)
|
||||
->and($newShoppingUser->billing_firstname)->toBe('Richtiger')
|
||||
->and($newShoppingUser->billing_lastname)->toBe('User')
|
||||
->and($newShoppingUser->billing_address)->toBe('Richtige Strasse 455')
|
||||
->and($newShoppingUser->billing_email)->toBe($targetUser->email);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue