Abo Einmalprodukte und Bestätigung abschließen
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2bdc9ada3c
commit
2269ce031f
57 changed files with 3647 additions and 371 deletions
180
tests/Feature/AboOneTimeServiceTest.php
Normal file
180
tests/Feature/AboOneTimeServiceTest.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Requests\Abo\AboOneTimeItemRequest;
|
||||
use App\Models\UserAboOneTimeItem;
|
||||
use App\Services\AboOneTimeService;
|
||||
use App\Services\AboOrderCart;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(Tests\TestCase::class, RefreshDatabase::class);
|
||||
|
||||
describe('AboOneTimeService', function () {
|
||||
beforeEach(function () {
|
||||
makeShopEnv();
|
||||
$this->service = new AboOneTimeService;
|
||||
});
|
||||
|
||||
it('fügt ein erlaubtes Produkt (show_on 2) mit Preis-Snapshot hinzu', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']);
|
||||
|
||||
$message = $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
|
||||
expect($message)->toBeNull();
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first();
|
||||
expect($item)->not->toBeNull()
|
||||
->and($item->qty)->toBe(1)
|
||||
->and((float) $item->price)->toBeGreaterThan(0.0)
|
||||
->and((float) $item->tax_rate)->toBe(19.0)
|
||||
->and((float) $item->price_net)->toBeGreaterThan(0.0);
|
||||
});
|
||||
|
||||
it('erhöht die Menge, wenn dasselbe Produkt erneut hinzugefügt wird', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']);
|
||||
|
||||
$this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
$this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first();
|
||||
expect(UserAboOneTimeItem::where('user_abo_id', $abo->id)->count())->toBe(1)
|
||||
->and($item->qty)->toBe(2);
|
||||
});
|
||||
|
||||
it('lehnt Produkte aus dem Abo-Sortiment (show_on 12) ab', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['12']);
|
||||
|
||||
$message = $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
|
||||
expect($message)->toBe(__('abo.onetime_product_not_allowed'))
|
||||
->and(UserAboOneTimeItem::where('user_abo_id', $abo->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('aktualisiert die Menge eines Einmal-Artikels (geclamped)', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']);
|
||||
$this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first();
|
||||
|
||||
$this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 5]);
|
||||
expect($item->fresh()->qty)->toBe(5);
|
||||
|
||||
$this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 999]);
|
||||
expect($item->fresh()->qty)->toBe(100);
|
||||
});
|
||||
|
||||
it('entfernt einen Einmal-Artikel', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']);
|
||||
$this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first();
|
||||
|
||||
$message = $this->service->handleAction($abo, ['action' => 'remove', 'one_time_item_id' => $item->id]);
|
||||
|
||||
expect($message)->toBeNull()
|
||||
->and(UserAboOneTimeItem::where('user_abo_id', $abo->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('bestätigt den aktuellen Stand der Einmal-Artikel', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']);
|
||||
$this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
|
||||
$message = $this->service->handleAction($abo, ['action' => 'confirm']);
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first();
|
||||
|
||||
expect($message)->toBeNull()
|
||||
->and($item->confirmed_qty)->toBe(1)
|
||||
->and($item->confirmed_at)->not->toBeNull()
|
||||
->and($item->status)->toBe(1)
|
||||
->and(AboOneTimeService::hasUnconfirmedChanges($abo))->toBeFalse()
|
||||
->and(AboOneTimeService::hasConfirmedItems($abo))->toBeTrue();
|
||||
});
|
||||
|
||||
it('verwirft unbestätigte Änderungen und stellt den bestätigten Stand wieder her', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']);
|
||||
$this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
$this->service->handleAction($abo, ['action' => 'confirm']);
|
||||
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first();
|
||||
$this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 4]);
|
||||
|
||||
expect(AboOneTimeService::hasUnconfirmedChanges($abo))->toBeTrue();
|
||||
|
||||
$message = $this->service->handleAction($abo, ['action' => 'discard']);
|
||||
|
||||
expect($message)->toBeNull()
|
||||
->and($item->fresh()->qty)->toBe(1)
|
||||
->and($item->fresh()->status)->toBe(1)
|
||||
->and(AboOneTimeService::hasUnconfirmedChanges($abo))->toBeFalse();
|
||||
});
|
||||
|
||||
it('verhindert das Bearbeiten fremder Einmal-Artikel', function () {
|
||||
$abo = makeMeAbo();
|
||||
$foreignAbo = makeMeAbo();
|
||||
$product = makeProduct(['2']);
|
||||
$this->service->handleAction($foreignAbo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
$foreignItem = UserAboOneTimeItem::where('user_abo_id', $foreignAbo->id)->first();
|
||||
|
||||
$message = $this->service->handleAction($abo, ['action' => 'remove', 'one_time_item_id' => $foreignItem->id]);
|
||||
|
||||
expect($message)->toBe(__('abo.abo_item_not_found'))
|
||||
->and(UserAboOneTimeItem::find($foreignItem->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('liefert eine Fehlermeldung bei ungültiger Aktion', function () {
|
||||
$abo = makeMeAbo();
|
||||
|
||||
expect($this->service->handleAction($abo, ['action' => 'foo']))->toBe(__('abo.onetime_action_invalid'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('AboOrderCart::buildOneTimeSnapshot', function () {
|
||||
beforeEach(fn () => makeShopEnv());
|
||||
|
||||
it('berechnet einen konsistenten Brutto/Netto/Steuer-Snapshot', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2'], 119, 19);
|
||||
|
||||
$snapshot = AboOrderCart::buildOneTimeSnapshot($product, $abo);
|
||||
|
||||
expect($snapshot['price'])->toBe(119.0)
|
||||
->and($snapshot['tax_rate'])->toBe(19.0)
|
||||
->and(round($snapshot['price_net'], 2))->toBe(100.0)
|
||||
->and(round($snapshot['tax'], 2))->toBe(19.0)
|
||||
->and($snapshot['points'])->toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AboOrderCart::getSplitSummary', function () {
|
||||
it('trennt Abo- und Einmal-Artikel-Summen anhand der abo_addon-Option', function () {
|
||||
$yard = Yard::instance(AboOrderCart::INSTANCE);
|
||||
$yard->destroy();
|
||||
|
||||
$aboRow = $yard->add(1, 'Abo-Produkt', 1, 119.0, false, false, ['weight' => 100]);
|
||||
Yard::setTax($aboRow->rowId, 19.0);
|
||||
|
||||
$oneTimeRow = $yard->add(2, 'Einmal-Produkt', 2, 59.5, false, false, ['weight' => 100, 'abo_addon' => true]);
|
||||
Yard::setTax($oneTimeRow->rowId, 19.0);
|
||||
|
||||
$summary = AboOrderCart::getSplitSummary();
|
||||
|
||||
expect($summary['abo']['gross'])->toBe(119.0)
|
||||
->and(round($summary['abo']['net'], 2))->toBe(100.0)
|
||||
->and($summary['one_time']['gross'])->toBe(119.0)
|
||||
->and($summary['has_one_time'])->toBeTrue();
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AboOneTimeItemRequest', function () {
|
||||
it('definiert die erwarteten Validierungsregeln', function () {
|
||||
$rules = (new AboOneTimeItemRequest)->rules();
|
||||
|
||||
expect($rules)->toHaveKeys(['action', 'product_id', 'one_time_item_id', 'qty'])
|
||||
->and($rules['action'])->toContain('required');
|
||||
});
|
||||
});
|
||||
201
tests/Feature/AboOneTimeViewTest.php
Normal file
201
tests/Feature/AboOneTimeViewTest.php
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<?php
|
||||
|
||||
use App\Models\UserAboOneTimeItem;
|
||||
use App\Services\AboOrderCart;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(Tests\TestCase::class, RefreshDatabase::class);
|
||||
|
||||
describe('Abo Einmal-Produkte Views', function () {
|
||||
beforeEach(fn () => makeShopEnv());
|
||||
|
||||
it('rendert die Einmal-Produktliste mit Position und Zwischensumme', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2'], 119, 19);
|
||||
|
||||
UserAboOneTimeItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $product->id,
|
||||
'qty' => 2,
|
||||
'price' => 119.0,
|
||||
'price_net' => 100.0,
|
||||
'tax_rate' => 19.0,
|
||||
'tax' => 19.0,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
$summary = ['one_time' => ['gross' => 238.0, 'net' => 200.0, 'tax' => 38.0]];
|
||||
|
||||
$html = view('admin.abo._order_onetime_show', [
|
||||
'user_abo' => $abo->fresh(),
|
||||
'summary' => $summary,
|
||||
])->render();
|
||||
|
||||
expect($html)->toContain($product->getLang('name'))
|
||||
->and($html)->toContain(__('abo.onetime_subtotal'))
|
||||
->and($html)->toContain('238,00');
|
||||
});
|
||||
|
||||
it('zeigt bei unbestätigten Einmal-Artikeln die Bestätigungsbuttons', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2'], 119, 19);
|
||||
|
||||
UserAboOneTimeItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $product->id,
|
||||
'qty' => 1,
|
||||
'price' => 119.0,
|
||||
'price_net' => 100.0,
|
||||
'tax_rate' => 19.0,
|
||||
'tax' => 19.0,
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$html = view('admin.abo._order_onetime_show', [
|
||||
'user_abo' => $abo->fresh(),
|
||||
'summary' => ['one_time' => ['gross' => 119.0], 'total_with_shipping' => 219.0],
|
||||
])->render();
|
||||
|
||||
expect($html)->toContain(__('abo.onetime_next_delivery_total'))
|
||||
->and($html)->toContain(__('abo.onetime_legal_notice', [
|
||||
'nextBillingDate' => $abo->next_date,
|
||||
'agb' => '<a href="'.url('/agb').'" target="_blank">'.__('abo.confirm_terms_link').'</a>',
|
||||
'withdrawal' => '<a href="'.asset('download/mivita_widerruf_formular.pdf').'" target="_blank">'.__('abo.confirm_withdrawal_link').'</a>',
|
||||
]))
|
||||
->and($html)->toContain(__('abo.onetime_discard_changes'))
|
||||
->and($html)->toContain(__('abo.onetime_confirm_paid'));
|
||||
});
|
||||
|
||||
it('zeigt bei bestätigten Einmal-Artikeln den Erfolgshinweis', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2'], 119, 19);
|
||||
|
||||
UserAboOneTimeItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $product->id,
|
||||
'qty' => 1,
|
||||
'confirmed_qty' => 1,
|
||||
'confirmed_at' => now(),
|
||||
'price' => 119.0,
|
||||
'price_net' => 100.0,
|
||||
'tax_rate' => 19.0,
|
||||
'tax' => 19.0,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
$html = view('admin.abo._order_onetime_show', [
|
||||
'user_abo' => $abo->fresh(),
|
||||
'summary' => ['one_time' => ['gross' => 119.0], 'total_with_shipping' => 219.0],
|
||||
])->render();
|
||||
|
||||
expect($html)->toContain(__('abo.onetime_confirm_success'))
|
||||
->and($html)->toContain('data-confirmation-state="confirmed"')
|
||||
->and($html)->not->toContain(__('abo.onetime_confirm_paid'));
|
||||
});
|
||||
|
||||
it('zeigt nach einer Änderung an bestätigten Einmal-Artikeln wieder die Bestätigungsbuttons', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2'], 119, 19);
|
||||
|
||||
UserAboOneTimeItem::create([
|
||||
'user_abo_id' => $abo->id,
|
||||
'product_id' => $product->id,
|
||||
'qty' => 3,
|
||||
'confirmed_qty' => 1,
|
||||
'confirmed_at' => now(),
|
||||
'price' => 119.0,
|
||||
'price_net' => 100.0,
|
||||
'tax_rate' => 19.0,
|
||||
'tax' => 19.0,
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$html = view('admin.abo._order_onetime_show', [
|
||||
'user_abo' => $abo->fresh(),
|
||||
'summary' => ['one_time' => ['gross' => 357.0], 'total_with_shipping' => 457.0],
|
||||
])->render();
|
||||
|
||||
expect($html)->toContain('data-confirmation-state="changed"')
|
||||
->and($html)->toContain(__('abo.onetime_discard_changes'))
|
||||
->and($html)->toContain(__('abo.onetime_confirm_paid'))
|
||||
->and($html)->not->toContain(__('abo.onetime_confirm_success'));
|
||||
});
|
||||
|
||||
it('zeigt den Leer-Hinweis, wenn keine Einmal-Produkte vorhanden sind', function () {
|
||||
$abo = makeMeAbo();
|
||||
|
||||
$html = view('admin.abo._order_onetime_show', [
|
||||
'user_abo' => $abo,
|
||||
'summary' => ['one_time' => ['gross' => 0]],
|
||||
])->render();
|
||||
|
||||
expect($html)->toContain(__('abo.onetime_empty'));
|
||||
});
|
||||
|
||||
it('rendert die kombinierte Gesamtsumme mit getrennten Zwischensummen', function () {
|
||||
$yard = Yard::instance(AboOrderCart::INSTANCE);
|
||||
$yard->destroy();
|
||||
|
||||
$aboRow = $yard->add(1, 'Abo-Produkt', 1, 119.0, false, false, ['weight' => 100]);
|
||||
Yard::setTax($aboRow->rowId, 19.0);
|
||||
$oneTimeRow = $yard->add(2, 'Einmal-Produkt', 1, 59.5, false, false, ['weight' => 100, 'abo_addon' => true]);
|
||||
Yard::setTax($oneTimeRow->rowId, 19.0);
|
||||
|
||||
$summary = AboOrderCart::getSplitSummary();
|
||||
|
||||
$html = view('admin.abo._order_combined_summary', ['summary' => $summary])->render();
|
||||
|
||||
expect($html)->toContain(__('abo.combined_summary_hl'))
|
||||
->and($html)->toContain(__('abo.onetime_subtotal'))
|
||||
->and($html)->toContain(__('abo.abo_subtotal'))
|
||||
->and($html)->toContain(__('order.total_sum'));
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
|
||||
it('rendert die Abo-Liste im Split-Modus mit Abo-Zwischensumme und Endsumme', function () {
|
||||
$abo = makeMeAbo();
|
||||
|
||||
$yard = Yard::instance(AboOrderCart::INSTANCE);
|
||||
$yard->destroy();
|
||||
$aboRow = $yard->add(1, 'Abo-Produkt', 1, 119.0, false, false, ['weight' => 100]);
|
||||
Yard::setTax($aboRow->rowId, 19.0);
|
||||
|
||||
$summary = AboOrderCart::getSplitSummary();
|
||||
|
||||
$html = view('admin.abo._order_abo_show', [
|
||||
'user_abo' => $abo,
|
||||
'split_mode' => true,
|
||||
'summary' => $summary,
|
||||
'only_show_products' => false,
|
||||
'add_only_mode' => false,
|
||||
])->render();
|
||||
|
||||
expect($html)->toContain(__('abo.abo_subtotal'))
|
||||
->and($html)->toContain(__('abo.abo_next_delivery_total'))
|
||||
->and($html)->not->toContain(__('order.shipping_costs'))
|
||||
->and($html)->not->toContain(__('order.total_sum'));
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
|
||||
it('rendert den Bestätigungsdialog für kostenpflichtige Abo-Erhöhungen', function () {
|
||||
$abo = makeMeAbo();
|
||||
$abo->amount = 1760;
|
||||
$abo->save();
|
||||
|
||||
$html = view('admin.abo._confirm_add_modal', [
|
||||
'user_abo' => $abo,
|
||||
])->render();
|
||||
|
||||
expect($html)->toContain(__('abo.confirm_increase_title'))
|
||||
->and($html)->toContain(__('abo.confirm_increase_info'))
|
||||
->and($html)->toContain(__('abo.confirm_new_total'))
|
||||
->and($html)->toContain(__('abo.confirm_legal_notice', [
|
||||
'nextBillingDate' => '<span id="confirm-add-next-billing-date"></span>',
|
||||
'agb' => '<a href="'.url('/agb').'" target="_blank">'.__('abo.confirm_terms_link').'</a>',
|
||||
'withdrawal' => '<a href="'.asset('download/mivita_widerruf_formular.pdf').'" target="_blank">'.__('abo.confirm_withdrawal_link').'</a>',
|
||||
]))
|
||||
->and($html)->toContain(__('abo.confirm_add_ok'));
|
||||
});
|
||||
});
|
||||
120
tests/Feature/AboOneTimeWindowTest.php
Normal file
120
tests/Feature/AboOneTimeWindowTest.php
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOneTimeItem;
|
||||
use App\Services\AboHelper;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(Tests\TestCase::class, RefreshDatabase::class);
|
||||
|
||||
afterEach(function () {
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
describe('getOneTimeWindowDays', function () {
|
||||
it('liefert den Standardwert, wenn kein Setting gesetzt ist', function () {
|
||||
expect(AboHelper::getOneTimeWindowDays())->toBe(AboHelper::DEFAULT_ONETIME_WINDOW_DAYS);
|
||||
});
|
||||
|
||||
it('liefert den konfigurierten Wert aus den Settings', function () {
|
||||
Setting::setContentBySlug('abo-onetime-window-days', 6, 'int');
|
||||
|
||||
expect(AboHelper::getOneTimeWindowDays())->toBe(6);
|
||||
});
|
||||
|
||||
it('fällt bei 0 oder ungültigem Wert auf den Standard zurück', function () {
|
||||
Setting::setContentBySlug('abo-onetime-window-days', 0, 'int');
|
||||
|
||||
expect(AboHelper::getOneTimeWindowDays())->toBe(AboHelper::DEFAULT_ONETIME_WINDOW_DAYS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOneTimeWindowOpen', function () {
|
||||
beforeEach(function () {
|
||||
Carbon::setTestNow(Carbon::parse('2026-06-05 10:00:00', 'Europe/Berlin'));
|
||||
});
|
||||
|
||||
it('ist offen, wenn die Ausführung innerhalb des Fensters liegt', function () {
|
||||
$abo = new UserAbo;
|
||||
$abo->next_date = '2026-06-09'; // 4 Tage entfernt, Default-Fenster = 4
|
||||
|
||||
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeTrue();
|
||||
});
|
||||
|
||||
it('ist offen am Tag der Ausführung', function () {
|
||||
$abo = new UserAbo;
|
||||
$abo->next_date = '2026-06-05';
|
||||
|
||||
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeTrue();
|
||||
});
|
||||
|
||||
it('ist geschlossen, wenn die Ausführung weiter als das Fenster entfernt ist', function () {
|
||||
$abo = new UserAbo;
|
||||
$abo->next_date = '2026-06-10'; // 5 Tage entfernt, Fenster = 4
|
||||
|
||||
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeFalse();
|
||||
});
|
||||
|
||||
it('berücksichtigt ein konfiguriertes größeres Fenster', function () {
|
||||
Setting::setContentBySlug('abo-onetime-window-days', 7, 'int');
|
||||
|
||||
$abo = new UserAbo;
|
||||
$abo->next_date = '2026-06-10'; // 5 Tage entfernt, Fenster = 7
|
||||
|
||||
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeTrue();
|
||||
});
|
||||
|
||||
it('ist geschlossen, wenn das Ausführungsdatum in der Vergangenheit liegt', function () {
|
||||
$abo = new UserAbo;
|
||||
$abo->next_date = '2026-06-04';
|
||||
|
||||
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeFalse();
|
||||
});
|
||||
|
||||
it('ist geschlossen, wenn kein next_date gesetzt ist', function () {
|
||||
$abo = new UserAbo;
|
||||
$abo->next_date = null;
|
||||
|
||||
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UserAboOneTimeItem Model', function () {
|
||||
it('definiert die Beziehung one_time_items auf UserAbo', function () {
|
||||
$relation = (new UserAbo)->one_time_items();
|
||||
|
||||
expect($relation)->toBeInstanceOf(Illuminate\Database\Eloquent\Relations\HasMany::class)
|
||||
->and($relation->getRelated())->toBeInstanceOf(UserAboOneTimeItem::class);
|
||||
});
|
||||
|
||||
it('castet die Snapshot-Felder korrekt', function () {
|
||||
$item = new UserAboOneTimeItem([
|
||||
'qty' => '3',
|
||||
'price' => '12.50',
|
||||
'tax_rate' => '19.00',
|
||||
]);
|
||||
|
||||
expect($item->qty)->toBe(3)
|
||||
->and($item->price)->toBe(12.5)
|
||||
->and($item->tax_rate)->toBe(19.0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShoppingOrderItem is_abo_addon', function () {
|
||||
it('grenzt Abo- und Einmal-Positionen über Scopes ab', function () {
|
||||
$aboSql = ShoppingOrderItem::query()->aboItems()->toSql();
|
||||
$addonSql = ShoppingOrderItem::query()->addonItems()->toSql();
|
||||
|
||||
expect($aboSql)->toContain('is_abo_addon')
|
||||
->and($addonSql)->toContain('is_abo_addon');
|
||||
});
|
||||
|
||||
it('castet is_abo_addon als bool', function () {
|
||||
$item = new ShoppingOrderItem(['is_abo_addon' => 1]);
|
||||
|
||||
expect($item->is_abo_addon)->toBeTrue();
|
||||
});
|
||||
});
|
||||
232
tests/Feature/CartMaxWeightTest.php
Normal file
232
tests/Feature/CartMaxWeightTest.php
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Shipping;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShippingPrice;
|
||||
use App\Models\UserAboItem;
|
||||
use App\Models\UserAboOneTimeItem;
|
||||
use App\Services\AboOneTimeService;
|
||||
use App\Services\AboOrderCart;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
uses(Tests\TestCase::class, RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* Versandland mit zwei Gewichtsstufen: 1–1000 g (4,90 €) und 1001–2000 g (9,90 €).
|
||||
* Maximalgewicht = 2000 g.
|
||||
*/
|
||||
function makeShopEnvWithWeightTiers(): ShippingCountry
|
||||
{
|
||||
$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]);
|
||||
|
||||
ShippingPrice::create([
|
||||
'shipping_id' => $shipping->id, 'price' => 4.90, 'price_comp' => 0, 'num_comp' => 0,
|
||||
'tax_rate' => 19, 'weight_from' => 1, 'weight_to' => 1000,
|
||||
]);
|
||||
ShippingPrice::create([
|
||||
'shipping_id' => $shipping->id, 'price' => 9.90, 'price_comp' => 0, 'num_comp' => 0,
|
||||
'tax_rate' => 19, 'weight_from' => 1001, 'weight_to' => 2000,
|
||||
]);
|
||||
|
||||
return ShippingCountry::create(['shipping_id' => $shipping->id, 'country_id' => $country->id]);
|
||||
}
|
||||
|
||||
function makeShopEnvWithCompWeightTiers(): ShippingCountry
|
||||
{
|
||||
$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]);
|
||||
|
||||
ShippingPrice::create([
|
||||
'shipping_id' => $shipping->id, 'price' => 4.90, 'price_comp' => 4.90, 'num_comp' => 0,
|
||||
'tax_rate' => 19, 'weight_from' => 1, 'weight_to' => 1000,
|
||||
]);
|
||||
ShippingPrice::create([
|
||||
'shipping_id' => $shipping->id, 'price' => 9.90, 'price_comp' => 9.90, 'num_comp' => 2,
|
||||
'tax_rate' => 19, 'weight_from' => 1001, 'weight_to' => 2000,
|
||||
]);
|
||||
|
||||
return ShippingCountry::create(['shipping_id' => $shipping->id, 'country_id' => $country->id]);
|
||||
}
|
||||
|
||||
describe('Yard Maximalgewicht', function () {
|
||||
beforeEach(function () {
|
||||
$this->shippingCountry = makeShopEnvWithWeightTiers();
|
||||
});
|
||||
|
||||
it('liefert das höchste weight_to als Maximalgewicht', function () {
|
||||
$yard = Yard::instance('test-maxweight');
|
||||
$yard->destroy();
|
||||
$yard->setShippingCountryWithPrice($this->shippingCountry->id);
|
||||
|
||||
expect($yard->getMaxWeight())->toBe(2000);
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
|
||||
it('erkennt das Überschreiten des Maximalgewichts', function () {
|
||||
$yard = Yard::instance('test-maxweight');
|
||||
$yard->destroy();
|
||||
$yard->setShippingCountryWithPrice($this->shippingCountry->id);
|
||||
|
||||
$yard->add(1, 'A', 1, 10.0, false, false, ['weight' => 1500]);
|
||||
|
||||
expect($yard->exceedsMaxWeight(0))->toBeFalse()
|
||||
->and($yard->exceedsMaxWeight(500))->toBeFalse()
|
||||
->and($yard->exceedsMaxWeight(501))->toBeTrue();
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
|
||||
it('nutzt bei Überschreitung die teuerste Stufe statt der günstigsten', function () {
|
||||
$yard = Yard::instance('test-maxweight');
|
||||
$yard->destroy();
|
||||
$yard->setShippingCountryWithPrice($this->shippingCountry->id);
|
||||
|
||||
// Gewicht 3000 g liegt über der höchsten Stufe (2000 g)
|
||||
$yard->add(1, 'A', 1, 10.0, false, false, ['weight' => 3000]);
|
||||
$yard->reCalculateShippingPrice();
|
||||
|
||||
expect((float) $yard->getShippingPrice())->toBe(9.90);
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AboOrderCart Maximalgewicht', function () {
|
||||
beforeEach(function () {
|
||||
makeShopEnvWithWeightTiers();
|
||||
$this->service = new AboOneTimeService;
|
||||
});
|
||||
|
||||
it('berechnet das kombinierte Gewicht aus Einmal-Artikeln', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']); // weight 500
|
||||
|
||||
$this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
|
||||
expect(AboOrderCart::combinedWeight($abo->fresh()))->toBe(500);
|
||||
});
|
||||
|
||||
it('stoppt das Hinzufügen, wenn das Maximalgewicht überschritten würde', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']); // weight 500, max 2000 => max 4 Stück
|
||||
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$message = $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
expect($message)->toBeNull();
|
||||
}
|
||||
|
||||
// 5. Stück => 2500 g > 2000 g
|
||||
$message = $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
|
||||
expect($message)->toBe(__('msg.cart_max_weight_reached'))
|
||||
->and(UserAboOneTimeItem::where('user_abo_id', $abo->id)->first()->qty)->toBe(4);
|
||||
});
|
||||
|
||||
it('stoppt das Hochsetzen der Menge bei Überschreitung, erlaubt aber gültige Mengen', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']); // weight 500
|
||||
$this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first();
|
||||
|
||||
// qty 4 => 2000 g (ok)
|
||||
$okMessage = $this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 4]);
|
||||
expect($okMessage)->toBeNull()
|
||||
->and($item->fresh()->qty)->toBe(4);
|
||||
|
||||
// qty 5 => 2500 g (zu schwer)
|
||||
$failMessage = $this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 5]);
|
||||
expect($failMessage)->toBe(__('msg.cart_max_weight_reached'))
|
||||
->and($item->fresh()->qty)->toBe(4);
|
||||
});
|
||||
|
||||
it('erlaubt das Reduzieren der Menge immer', function () {
|
||||
$abo = makeMeAbo();
|
||||
$product = makeProduct(['2']);
|
||||
$this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]);
|
||||
$item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first();
|
||||
$this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 4]);
|
||||
|
||||
$message = $this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 2]);
|
||||
|
||||
expect($message)->toBeNull()
|
||||
->and($item->fresh()->qty)->toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AboOrderCart Kompensationsprodukte', function () {
|
||||
beforeEach(function () {
|
||||
$this->shippingCountry = makeShopEnvWithCompWeightTiers();
|
||||
Setting::setContentBySlug('is_comp_me_abo', true, 'bool');
|
||||
});
|
||||
|
||||
it('erhöht die Kompensationsprodukte, wenn die höhere Versandstufe mehr verlangt', function () {
|
||||
$abo = makeMeAbo();
|
||||
$compProduct = makeProduct(['12']);
|
||||
DB::table('products')->where('id', $compProduct->id)->update(['shipping_addon' => true, 'pos' => 100]);
|
||||
|
||||
$yard = Yard::instance(AboOrderCart::INSTANCE);
|
||||
$yard->destroy();
|
||||
$yard->setShippingCountryWithPrice($this->shippingCountry->id, 'abo-me');
|
||||
$yard->add(1, 'Abo-Produkt', 1, 10.0, false, false, ['weight' => 1500]);
|
||||
$yard->reCalculateShippingPrice();
|
||||
|
||||
AboOrderCart::checkNumOfCompProducts($abo);
|
||||
|
||||
expect(UserAboItem::where('user_abo_id', $abo->id)->where('comp', '>', 0)->count())->toBe(2)
|
||||
->and(UserAboItem::where('user_abo_id', $abo->id)->where('comp', '>', 0)->pluck('comp')->all())->toBe([1, 2]);
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
|
||||
it('entfernt Kompensationsprodukte wieder, wenn keine mehr benötigt werden', function () {
|
||||
$abo = makeMeAbo();
|
||||
$compProduct = makeProduct(['12']);
|
||||
DB::table('products')->where('id', $compProduct->id)->update(['shipping_addon' => true, 'pos' => 100]);
|
||||
|
||||
UserAboItem::create(['user_abo_id' => $abo->id, 'product_id' => $compProduct->id, 'comp' => 1, 'qty' => 1, 'status' => 1]);
|
||||
UserAboItem::create(['user_abo_id' => $abo->id, 'product_id' => $compProduct->id, 'comp' => 2, 'qty' => 1, 'status' => 1]);
|
||||
|
||||
$yard = Yard::instance(AboOrderCart::INSTANCE);
|
||||
$yard->destroy();
|
||||
$yard->setShippingCountryWithPrice($this->shippingCountry->id, 'abo-me');
|
||||
$yard->add(1, 'Abo-Produkt', 1, 10.0, false, false, ['weight' => 500]);
|
||||
$yard->reCalculateShippingPrice();
|
||||
|
||||
AboOrderCart::checkNumOfCompProducts($abo);
|
||||
|
||||
expect(UserAboItem::where('user_abo_id', $abo->id)->where('comp', '>', 0)->count())->toBe(0);
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
|
||||
it('zeigt einen Hinweis für Kompensationsprodukte aus Zusatzprodukten', function () {
|
||||
$compProduct = makeProduct(['12']);
|
||||
|
||||
$yard = Yard::instance(AboOrderCart::INSTANCE);
|
||||
$yard->destroy();
|
||||
$yard->setShippingCountryWithPrice($this->shippingCountry->id, 'abo-me');
|
||||
$yard->add(1, 'Abo-Produkt', 1, 10.0, false, false, ['weight' => 1500]);
|
||||
$yard->reCalculateShippingPrice();
|
||||
|
||||
$html = view('user.order.comp_product', [
|
||||
'comp_products' => collect([$compProduct]),
|
||||
'cart_instance' => AboOrderCart::INSTANCE,
|
||||
'base_comp_count' => 0,
|
||||
])->render();
|
||||
|
||||
expect($html)->toContain(__('order.comp_required_by_additional_products'));
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue