Abo Einmalprodukte: Phase 4 - Ausfuehrung, Purge & User-Retry
- UserMakeOrder: bestaetigte Einmal-Artikel in den Yard, is_abo_addon auf ShoppingOrderItem; amount bleibt reiner Abo-Betrag (Reihenfolge) - AboOneTimeService::purgeAfterExecution: loescht alle Einmal-Artikel und rechnet Comp-Produkte neu - nur im Erfolgszweig (Cron + Retry) - User-Retry in Sales Center und Portal mit Berechtigungspruefung, gemeinsames Confirm-Modal; Admin-Retry unveraendert - Tests: AboMakeOrderOneTimeTest, AboUserRetryTest; Plan-Doku Phase 4 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8288ea59ac
commit
ee04146217
14 changed files with 536 additions and 19 deletions
269
tests/Feature/AboMakeOrderOneTimeTest.php
Normal file
269
tests/Feature/AboMakeOrderOneTimeTest.php
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
<?php
|
||||
|
||||
use App\Cron\UserMakeOrder;
|
||||
use App\Models\Country;
|
||||
use App\Models\Shipping;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboItem;
|
||||
use App\Models\UserAboOneTimeItem;
|
||||
use App\Models\UserAccount;
|
||||
use App\Models\UserShop;
|
||||
use App\Services\AboOneTimeService;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(Tests\TestCase::class, RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* Baut ein vollständiges Berater-Abo ('me') inklusive Stammkunde, Referenz-Bestellung,
|
||||
* Versandland, einem regulären Abo-Artikel und einem verbindlich bestätigten Einmal-Artikel.
|
||||
*
|
||||
* @return array{abo: UserAbo, aboProductId: int, oneTimeProductId: int}
|
||||
*/
|
||||
function makeMakeOrderFixture(): array
|
||||
{
|
||||
$country = Country::create([
|
||||
'code' => 'DE', 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland',
|
||||
'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
|
||||
]);
|
||||
|
||||
$shipping = Shipping::create(['name' => 'Standard', 'active' => true]);
|
||||
ShippingCountry::create(['shipping_id' => $shipping->id, 'country_id' => $country->id]);
|
||||
|
||||
$account = UserAccount::create([
|
||||
'salutation' => 'Herr',
|
||||
'first_name' => 'Max',
|
||||
'last_name' => 'Muster',
|
||||
'address' => 'Musterstr. 1',
|
||||
'zipcode' => '12345',
|
||||
'city' => 'Musterstadt',
|
||||
'country_id' => $country->id,
|
||||
'phone' => '123456',
|
||||
'same_as_billing' => 1,
|
||||
'language' => 'de',
|
||||
]);
|
||||
|
||||
$user = User::forceCreate([
|
||||
'email' => 'consultant-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'account_id' => $account->id,
|
||||
]);
|
||||
|
||||
$userShop = UserShop::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => 'TS'.substr(uniqid('', true), 0, 8),
|
||||
'slug' => 'ts-'.uniqid(),
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$shoppingUser = ShoppingUser::create([
|
||||
'auth_user_id' => $user->id,
|
||||
'member_id' => $user->id,
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
'billing_email' => $user->email,
|
||||
'shipping_email' => $user->email,
|
||||
'billing_firstname' => 'Max',
|
||||
'billing_lastname' => 'Muster',
|
||||
'shipping_firstname' => 'Max',
|
||||
'shipping_lastname' => 'Muster',
|
||||
'same_as_billing' => 1,
|
||||
'is_for' => 'me',
|
||||
'is_from' => 'user_order',
|
||||
]);
|
||||
|
||||
ShoppingOrder::create([
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'auth_user_id' => $user->id,
|
||||
'member_id' => $user->id,
|
||||
'country_id' => $country->id,
|
||||
'user_shop_id' => $userShop->id,
|
||||
'payment_for' => 3,
|
||||
'total' => 100,
|
||||
'subtotal' => 90,
|
||||
'total_shipping' => 100,
|
||||
'paid' => true,
|
||||
'is_abo' => true,
|
||||
'txaction' => 'paid',
|
||||
'mode' => 'test',
|
||||
]);
|
||||
|
||||
$abo = UserAbo::create([
|
||||
'user_id' => $user->id,
|
||||
'member_id' => $user->id,
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'is_for' => 'me',
|
||||
'email' => $user->email,
|
||||
'payone_userid' => 900200,
|
||||
'clearingtype' => 'cc',
|
||||
'active' => true,
|
||||
'status' => 2,
|
||||
'abo_interval' => 5,
|
||||
'next_date' => now()->toDateString(),
|
||||
]);
|
||||
|
||||
$aboProduct = makeProduct(['12'], 119, 19);
|
||||
$oneTimeProduct = makeProduct(['2'], 50, 19);
|
||||
|
||||
UserAboItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $aboProduct->id,
|
||||
'qty' => 1,
|
||||
'comp' => 0,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
UserAboOneTimeItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $oneTimeProduct->id,
|
||||
'qty' => 2,
|
||||
'confirmed_qty' => 2,
|
||||
'confirmed_at' => now(),
|
||||
'price' => 50.0,
|
||||
'price_net' => 42.017,
|
||||
'tax_rate' => 19.0,
|
||||
'tax' => 7.983,
|
||||
'points' => 7,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
return [
|
||||
'abo' => $abo->fresh(),
|
||||
'aboProductId' => $aboProduct->id,
|
||||
'oneTimeProductId' => $oneTimeProduct->id,
|
||||
];
|
||||
}
|
||||
|
||||
it('schreibt verbindlich bestätigte Einmal-Artikel mit is_abo_addon in die Bestellung', function () {
|
||||
$fixture = makeMakeOrderFixture();
|
||||
|
||||
$userMakeOrder = new UserMakeOrder($fixture['abo']);
|
||||
$userMakeOrder->createShoppingUser();
|
||||
$order = $userMakeOrder->makeShoppingOrder();
|
||||
|
||||
expect($order)->not->toBeFalse();
|
||||
|
||||
$aboItem = $order->shopping_order_items()
|
||||
->where('product_id', $fixture['aboProductId'])
|
||||
->first();
|
||||
$addonItem = $order->shopping_order_items()
|
||||
->where('product_id', $fixture['oneTimeProductId'])
|
||||
->first();
|
||||
|
||||
expect($aboItem)->not->toBeNull()
|
||||
->and((bool) $aboItem->is_abo_addon)->toBeFalse()
|
||||
->and($addonItem)->not->toBeNull()
|
||||
->and((bool) $addonItem->is_abo_addon)->toBeTrue()
|
||||
->and((int) $addonItem->qty)->toBe(2);
|
||||
});
|
||||
|
||||
it('lässt user_abos.amount der reine Abo-Betrag (ohne Einmal-Artikel)', function () {
|
||||
$fixture = makeMakeOrderFixture();
|
||||
|
||||
$userMakeOrder = new UserMakeOrder($fixture['abo']);
|
||||
$userMakeOrder->createShoppingUser();
|
||||
$order = $userMakeOrder->makeShoppingOrder();
|
||||
|
||||
$pureAboAmount = (float) $fixture['abo']->fresh()->amount; // Cent, nur Abo
|
||||
$combinedTotal = (float) $order->total_shipping * 100; // Cent, inkl. Einmal-Artikel
|
||||
|
||||
// Der kombinierte Abbuchungsbetrag enthält die Einmal-Artikel, der gespeicherte
|
||||
// Abo-Betrag jedoch nicht -> kombiniert ist größer.
|
||||
expect($combinedTotal)->toBeGreaterThan($pureAboAmount);
|
||||
});
|
||||
|
||||
it('nimmt nur bestätigte Einmal-Artikel auf, offene Entwürfe nicht', function () {
|
||||
$fixture = makeMakeOrderFixture();
|
||||
$abo = $fixture['abo'];
|
||||
|
||||
// Zusätzlicher, NICHT bestätigter Einmal-Artikel
|
||||
$pendingProduct = makeProduct(['2'], 30, 19);
|
||||
UserAboOneTimeItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $pendingProduct->id,
|
||||
'qty' => 1,
|
||||
'price' => 30.0,
|
||||
'tax_rate' => 19.0,
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$userMakeOrder = new UserMakeOrder($abo);
|
||||
$userMakeOrder->createShoppingUser();
|
||||
$order = $userMakeOrder->makeShoppingOrder();
|
||||
|
||||
expect($order->shopping_order_items()->where('product_id', $pendingProduct->id)->exists())->toBeFalse()
|
||||
->and($order->shopping_order_items()->where('product_id', $fixture['oneTimeProductId'])->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('entfernt beim Purge ALLE Einmal-Artikel (bestätigt, offen und soft-deleted)', function () {
|
||||
$fixture = makeMakeOrderFixture();
|
||||
$abo = $fixture['abo'];
|
||||
|
||||
// zusätzlich ein offener und ein soft-deleted Artikel
|
||||
$pending = makeProduct(['2'], 30, 19);
|
||||
UserAboOneTimeItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $pending->id,
|
||||
'qty' => 1,
|
||||
'price' => 30.0,
|
||||
'tax_rate' => 19.0,
|
||||
'status' => 0,
|
||||
]);
|
||||
$trashed = makeProduct(['2'], 20, 19);
|
||||
$trashedItem = UserAboOneTimeItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $trashed->id,
|
||||
'qty' => 1,
|
||||
'confirmed_qty' => 1,
|
||||
'confirmed_at' => now(),
|
||||
'price' => 20.0,
|
||||
'tax_rate' => 19.0,
|
||||
'status' => 1,
|
||||
]);
|
||||
$trashedItem->delete();
|
||||
|
||||
expect(UserAboOneTimeItem::withTrashed()->where('user_abo_id', $abo->id)->count())->toBe(3);
|
||||
|
||||
AboOneTimeService::purgeAfterExecution($abo);
|
||||
|
||||
expect(UserAboOneTimeItem::withTrashed()->where('user_abo_id', $abo->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('reduziert nach dem Purge nicht mehr benötigte Kompensationsprodukte', function () {
|
||||
$fixture = makeMakeOrderFixture();
|
||||
$abo = $fixture['abo'];
|
||||
|
||||
// Ein Comp-Abo-Artikel, der nach Wegfall der Einmal-Artikel nicht mehr nötig ist.
|
||||
$compProduct = makeProduct(['12'], 0, 0);
|
||||
UserAboItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $compProduct->id,
|
||||
'comp' => 1,
|
||||
'qty' => 1,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
expect(UserAboItem::where('user_abo_id', $abo->id)->where('comp', '>', 0)->count())->toBe(1);
|
||||
|
||||
AboOneTimeService::purgeAfterExecution($abo);
|
||||
|
||||
expect(UserAboItem::where('user_abo_id', $abo->id)->where('comp', '>', 0)->count())->toBe(0)
|
||||
->and(UserAboOneTimeItem::withTrashed()->where('user_abo_id', $abo->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('entfernt durch reine Bestellerstellung KEINE Einmal-Artikel (behalten bei Fehler)', function () {
|
||||
$fixture = makeMakeOrderFixture();
|
||||
$abo = $fixture['abo'];
|
||||
|
||||
$userMakeOrder = new UserMakeOrder($abo);
|
||||
$userMakeOrder->createShoppingUser();
|
||||
$userMakeOrder->makeShoppingOrder();
|
||||
|
||||
// makeShoppingOrder läuft sowohl im Erfolgs- als auch im Fehlerfall; der Purge
|
||||
// erfolgt ausschließlich separat im Erfolgszweig. Daher bleiben die Artikel hier erhalten.
|
||||
expect(UserAboOneTimeItem::where('user_abo_id', $abo->id)->count())->toBe(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue