mivita/tests/Feature/AboExternalCompletionTest.php
Kevin 4709dfde55 Abo: Bestellung extern als erledigt markieren (ohne Rechnung/Inkasso)
Neue Admin-Funktion fuer angehaltene Abo-Bestellungen, die der Kunde
ausserhalb des Systems geliefert und verrechnet hat.

Aktion (AboExternalCompletionService):
- Punkte gutschreiben wie bei eingegangener Zahlung (SalesPointsVolume,
  idempotent gegen Doppelbuchung)
- UserAboOrder auf neuen gruenen Status 8 "extern erledigt"; paid bleibt false
- Bestellung: shipped=2 (Lieferung bestaetigt), keine Rechnung/kein Lieferschein
- Blockade aufheben: Abo zurueck auf status=2 (okay), aktiv, next_date neu,
  damit der naechste Termin regulaer laeuft
- wie bei regulaerer Ausfuehrung: IncentiveTracker + AboOneTimeService-Purge

Einstiegspunkte: Button + Bestaetigungs-Modal auf der Kunden-Bestelldetailseite
(SalesController) und auf der Abo-Detailseite (Admin\AboController@externDone,
Route admin_abos_extern_done). Status 8 (gruen) in UserAbo/UserAboOrder, Labels
de/en.

Tests: tests/Feature/AboExternalCompletionTest (Happy Path, Idempotenz,
bezahlt-/Nicht-Abo-Guards, Abo-Detail-Einstieg) - 5 gruen.

Zusaetzlich: docs/2026-06-11-test-suite-vorbestehende-fehler.md - Diagnose der
93 vorbestehenden (von master geerbten) Test-Suite-Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 08:11:45 +00:00

219 lines
7.4 KiB
PHP

<?php
use App\Models\Country;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
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\Services\AboExternalCompletionService;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function heldAboSeed(): array
{
$country = Country::create([
'code' => 'DE',
'phone' => '49',
'en' => 'Germany',
'de' => 'Deutschland',
'es' => 'Alemania',
'fr' => 'Allemagne',
'it' => 'Germania',
'ru' => 'Германия',
'eu_country' => true,
]);
$account = UserAccount::create([
'first_name' => 'Klaus',
'last_name' => 'Kunde',
'address' => 'Hauptstrasse 1',
'zipcode' => '10115',
'city' => 'Berlin',
'country_id' => $country->id,
]);
$user = User::forceCreate([
'account_id' => $account->id,
'email' => 'klaus@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $user->id,
'billing_firstname' => 'Klaus',
'billing_lastname' => 'Kunde',
'billing_address' => 'Hauptstrasse 1',
'billing_zipcode' => '10115',
'billing_city' => 'Berlin',
'billing_country_id' => $country->id,
'billing_email' => $user->email,
'same_as_billing' => true,
'shipping_firstname' => 'Klaus',
'shipping_lastname' => 'Kunde',
'shipping_address' => 'Hauptstrasse 1',
'shipping_zipcode' => '10115',
'shipping_city' => 'Berlin',
'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' => 654321,
'clearingtype' => 'cc',
'active' => true,
'status' => 3, // abo_hold (Blockade)
'abo_interval' => 5,
'next_date' => now()->subDays(2), // ueberfaellig
]);
$order = ShoppingOrder::create([
'shopping_user_id' => $shoppingUser->id,
'auth_user_id' => $user->id,
'country_id' => $country->id,
'language' => 'de',
'user_shop_id' => 1,
'payment_for' => 6,
'total' => 50.00,
'subtotal' => 50.00,
'points' => 25,
'paid' => false,
'shipped' => 0,
'is_abo' => true,
'abo_interval' => 5,
'txaction' => 'error',
'mode' => 'live',
]);
$aboOrder = UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $order->id,
'status' => 3, // abo_hold
'paid' => false,
]);
return [$userAbo, $order, $aboOrder, $user];
}
it('markiert eine angehaltene Abo-Bestellung als extern erledigt, schreibt Punkte gut und hebt die Blockade auf', function () {
[$userAbo, $order, $aboOrder, $user] = heldAboSeed();
$result = app(AboExternalCompletionService::class)->complete($order);
expect($result['success'])->toBeTrue();
$order->refresh();
$aboOrder->refresh();
$userAbo->refresh();
// Bestellung bleibt unbezahlt, keine Rechnung
expect((bool) $order->paid)->toBeFalse()
->and((int) $order->shipped)->toBe(2)
->and($order->isInvoice())->toBeFalse();
// Abo-Bestellung: gruener Status 8, weiterhin unbezahlt
expect((int) $aboOrder->status)->toBe(8)
->and((bool) $aboOrder->paid)->toBeFalse();
// Blockade aufgehoben: zurueck auf okay, aktiv, next_date in der Zukunft
expect((int) $userAbo->status)->toBe(2)
->and($userAbo->active)->toBeTrue();
expect(\Carbon\Carbon::parse($userAbo->getRawOriginal('next_date'))->isFuture())->toBeTrue();
// Punkte gutgeschrieben - mit heutigem Datum (Markierungsdatum), nicht dem
// vergangenen Bestelldatum, damit sie in der laufenden Monatsauswertung landen.
$volume = UserSalesVolume::where('shopping_order_id', $order->id)->first();
expect($volume)->not->toBeNull()
->and((float) $volume->points)->toBe(25.0)
->and((int) $volume->user_id)->toBe($user->id)
->and((int) $volume->month)->toBe((int) date('m'))
->and((int) $volume->year)->toBe((int) date('Y'))
->and($volume->date)->toBe(date('d.m.Y'));
});
it('trackt Incentive-Punkte mit dem Bestelldatum (Ausfuehrungsmonat), nicht mit heute', function () {
\Carbon\Carbon::setTestNow('2026-06-15');
[$userAbo, $order, , $user] = heldAboSeed();
// Die vorgesehene Ausfuehrung lag im April; das Incentive-Fenster deckt nur
// April ab (nicht Juni/heute). Wuerde der Tracker "heute" verwenden, faende
// er das Incentive nicht und legte keinen IncentiveNewAbo-Eintrag an.
$executionDate = \Carbon\Carbon::parse('2026-04-10');
ShoppingOrder::where('id', $order->id)->update(['created_at' => $executionDate]);
Incentive::factory()->active()->create([
'qualification_start' => '2026-04-01',
'qualification_end' => '2026-04-30',
'calculation_end' => '2026-05-31',
]);
app(AboExternalCompletionService::class)->complete($order->fresh());
// Incentive-Abo wurde dem April-Fenster zugeordnet, activated_at = Bestelldatum.
$newAbo = IncentiveNewAbo::where('user_abo_id', $userAbo->id)->first();
expect($newAbo)->not->toBeNull()
->and($newAbo->activated_at->toDateString())->toBe('2026-04-10');
// Reguläre Provisionspunkte hingegen mit heutigem Datum (Juni).
$volume = UserSalesVolume::where('shopping_order_id', $order->id)->first();
expect((int) $volume->month)->toBe(6)
->and((int) $volume->year)->toBe(2026);
\Carbon\Carbon::setTestNow();
});
it('ist idempotent und schreibt Punkte nicht doppelt gut', function () {
[, $order] = heldAboSeed();
$service = app(AboExternalCompletionService::class);
$first = $service->complete($order);
$second = $service->complete($order->fresh());
expect($first['success'])->toBeTrue()
->and($second['success'])->toBeFalse();
expect(UserSalesVolume::where('shopping_order_id', $order->id)->count())->toBe(1);
});
it('verweigert die Aktion fuer eine bereits bezahlte Abo-Bestellung', function () {
[, $order, $aboOrder] = heldAboSeed();
$aboOrder->update(['paid' => true]);
$result = app(AboExternalCompletionService::class)->complete($order->fresh());
expect($result['success'])->toBeFalse();
expect(UserSalesVolume::where('shopping_order_id', $order->id)->exists())->toBeFalse();
});
it('verweigert die Aktion fuer eine Nicht-Abo-Bestellung', function () {
[, $order] = heldAboSeed();
$order->update(['is_abo' => false]);
$result = app(AboExternalCompletionService::class)->complete($order->fresh());
expect($result['success'])->toBeFalse();
});
it('erledigt ueber die Abo-Detailseite die juengste angehaltene Bestellung', function () {
[$userAbo, $order] = heldAboSeed();
$result = app(AboExternalCompletionService::class)->completeForAbo($userAbo);
expect($result['success'])->toBeTrue();
expect((int) UserAboOrder::where('shopping_order_id', $order->id)->value('status'))->toBe(8);
expect((int) $userAbo->fresh()->status)->toBe(2);
});