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:
Kevin 2026-06-25 13:37:39 +00:00
parent 2b2847add1
commit 9b7665c59a
5 changed files with 1156 additions and 1 deletions

View 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);
});
});

View 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);
});