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>, * 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 $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 $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 $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 $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; } }