Abo: extern-erledigt für gelöschte Abos + Lieferhistorie-Umzug (Ticket Kunz Mai-Punkte)

- AboExternalCompletionService: Bestellungen gelöschter/ersetzter Abos können
  nachträglich extern erledigt werden (Punkte + Status 8 + Versandstatus);
  das gelöschte Abo wird dabei nicht reaktiviert
- IncentiveTracker::trackSalesVolume: Abo-Zuordnung inkl. gelöschter Abos
  (withTrashed) - nachgebuchte Umsätze bereits getrackter Abos zählen weiter
- Neue Wartungs-Commands: abo:extern-done {shopping_order_id} und
  abo:move-orders {from_abo} {to_abo} (--dry-run, Guards: gleicher Kunde)
- Latent datumsabhängigen Alt-Test in AboExternalCompletionTest repariert
  (Buchung nutzt date(), nicht Carbon::setTestNow)

Fall Kunz (dev/2026-08-12-intensiv-tickets): Dannenhauers gelieferte, aber am
17.06. beim Nachtragen vergessene Mai-Order 53274 → abo:extern-done (+54 P);
Bockhackers Lieferhistorie von gelöschtem Abo 197 → 496 umgehängt (Zähler 4).
Bockhackers 24 P existieren bereits (Juni-Buchung der Mai-Lieferung).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-08-21 12:06:29 +00:00
parent 867dc21b57
commit f6faad9ae7
8 changed files with 433 additions and 17 deletions

View file

@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands;
use App\Models\ShoppingOrder;
use App\Services\AboExternalCompletionService;
use Illuminate\Console\Command;
/**
* Wartungs-Command: eine angehaltene Abo-Bestellung als "extern erledigt" markieren
* (Punkte gutschreiben, Lieferung zaehlt, Bestellung bleibt unbezahlt).
*
* Entspricht dem Admin-Button "extern erledigt", funktioniert aber auch fuer
* Bestellungen geloeschter/ersetzter Abos, die im UI nicht mehr erreichbar sind.
*/
class AboExternDoneOrder extends Command
{
protected $signature = 'abo:extern-done {shopping_order_id : ID der Abo-Bestellung}';
protected $description = 'Angehaltene Abo-Bestellung als extern erledigt markieren (Punkte + Lieferzähler, auch bei gelöschtem Abo)';
public function handle(AboExternalCompletionService $service): int
{
$order = ShoppingOrder::find($this->argument('shopping_order_id'));
if (! $order) {
$this->error('Bestellung nicht gefunden.');
return self::FAILURE;
}
$result = $service->complete($order);
if (! $result['success']) {
$this->error($result['message']);
return self::FAILURE;
}
$this->info("Bestellung {$order->id} extern erledigt: {$result['message']}");
$this->info("Punkte: {$order->points} | Netto: {$order->subtotal} | Berater: ".($order->member_id ?? $order->auth_user_id));
return self::SUCCESS;
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace App\Console\Commands;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use Illuminate\Console\Command;
/**
* Wartungs-Command: Lieferhistorie (user_abo_orders) eines geloeschten/ersetzten
* Abos auf das Nachfolge-Abo desselben Kunden umhaengen.
*
* Hintergrund: Muss ein Abo geloescht und neu angelegt werden (z. B. Wechsel des
* Zahlungsmittels), startet der Lieferzaehler des neuen Abos bei 0 - Mindestlaufzeit
* und "x-te Lieferung" gehen verloren. Dieses Command uebertraegt die Historie.
*/
class AboMoveOrders extends Command
{
protected $signature = 'abo:move-orders
{from_abo : ID des alten (i. d. R. gelöschten) Abos}
{to_abo : ID des Nachfolge-Abos}
{--dry-run : Nur anzeigen, nichts ändern}';
protected $description = 'Lieferhistorie (user_abo_orders) eines ersetzten Abos auf das Nachfolge-Abo umhängen';
public function handle(): int
{
$from = UserAbo::withTrashed()->find($this->argument('from_abo'));
$to = UserAbo::find($this->argument('to_abo'));
if (! $from || ! $to) {
$this->error('Abo nicht gefunden (from: '.($from ? 'ok' : 'fehlt').', to: '.($to ? 'ok' : 'fehlt').').');
return self::FAILURE;
}
if ($from->id === $to->id) {
$this->error('from_abo und to_abo sind identisch.');
return self::FAILURE;
}
$sameCustomer = strcasecmp(trim((string) $from->email), trim((string) $to->email)) === 0
&& (int) $from->member_id === (int) $to->member_id
&& $from->is_for === $to->is_for;
if (! $sameCustomer) {
$this->error("Abos gehören nicht zum selben Kunden (from: {$from->email} / Berater {$from->member_id}, to: {$to->email} / Berater {$to->member_id}). Abbruch.");
return self::FAILURE;
}
$orders = UserAboOrder::where('user_abo_id', $from->id)->orderBy('id')->get();
if ($orders->isEmpty()) {
$this->info("Keine user_abo_orders bei Abo {$from->id} - nichts zu tun.");
return self::SUCCESS;
}
$this->info("Abo {$from->id} ({$from->email}".($from->trashed() ? ', gelöscht' : '').") → Abo {$to->id}: {$orders->count()} Lieferung(en)");
$this->table(
['user_abo_order', 'shopping_order', 'Status', 'bezahlt'],
$orders->map(fn (UserAboOrder $o) => [$o->id, $o->shopping_order_id, $o->status, $o->paid ? 'ja' : 'nein'])->all()
);
if ($this->option('dry-run')) {
$this->info('DRY-RUN - nichts geändert.');
return self::SUCCESS;
}
UserAboOrder::where('user_abo_id', $from->id)->update(['user_abo_id' => $to->id]);
$to->refresh();
$this->info("Umgehängt. Abo {$to->id} zählt jetzt {$to->getCountPaidOrders()} ausgeführte Lieferung(en).");
return self::SUCCESS;
}
}

View file

@ -37,7 +37,9 @@ class AboExternalCompletionService
}
$userAboOrder = UserAboOrder::where('shopping_order_id', $shoppingOrder->id)->first();
$userAbo = $userAboOrder?->user_abo;
// Auch geloeschte Abos zulassen: eine ausstehende Lieferung eines ersetzten/
// geloeschten Abos muss nachtraeglich erledigt werden koennen (Punkte + Zaehler).
$userAbo = $userAboOrder ? $userAboOrder->user_abo()->withTrashed()->first() : null;
if (! $userAboOrder || ! $userAbo) {
return ['success' => false, 'message' => __('abo.extern_done_no_order')];
@ -65,18 +67,24 @@ class AboExternalCompletionService
$shoppingOrder->save();
// Blockade aufheben: Abo zurueck in den regulaeren Zyklus.
$userAbo->update([
'status' => self::STATUS_ABO_OKAY,
'active' => true,
'last_date' => now(),
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
]);
// Geloeschte (ersetzte) Abos bleiben unangetastet - dort werden nur
// Punkte und Lieferstatus nachgezogen, kein neuer Zyklus gestartet.
if (! $userAbo->trashed()) {
$userAbo->update([
'status' => self::STATUS_ABO_OKAY,
'active' => true,
'last_date' => now(),
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
]);
}
});
// Wie bei einer erfolgreichen Ausfuehrung: Incentive tracken und
// Einmal-Artikel entfernen. Beide Aufrufe sind idempotent.
IncentiveTracker::trackAboActivated($shoppingOrder);
AboOneTimeService::purgeAfterExecution($userAbo);
if (! $userAbo->trashed()) {
AboOneTimeService::purgeAfterExecution($userAbo);
}
\Log::channel('abo_order')->info('AboExternalCompletionService: Bestellung extern erledigt', [
'abo_id' => $userAbo->id,

View file

@ -9,6 +9,7 @@ use App\Models\IncentiveParticipant;
use App\Models\IncentivePointsLog;
use App\Models\ShoppingOrder;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserSalesVolume;
use App\User;
use Carbon\Carbon;
@ -197,7 +198,11 @@ class IncentiveTracker
$order = ShoppingOrder::find($user_sales_volume->shopping_order_id);
if ($order) {
$userAboFromOrder = $order->getUserAbo();
// Bewusst inkl. geloeschter Abos: ein bereits getracktes Abo kann
// geloescht/ersetzt worden sein, waehrend Umsaetze nachgebucht werden
// (z. B. extern erledigte Lieferungen) - die zaehlen weiterhin.
$userAboFromOrder = UserAboOrder::where('shopping_order_id', $order->id)
->first()?->user_abo()->withTrashed()->first();
if (! $userAboFromOrder || ! in_array($userAboFromOrder->is_for, ['ot', 'me'], true)) {
$userAboFromOrder = null;
}

View file

@ -124,6 +124,49 @@ Umsetzung:
Verifiziert auf der Live-Kopie (19.08.): 13 August-Bestellungen geprüft, 12 bereits
gebucht, genau 1 nachgebucht (Order 59900, Jasmin Grund +200 P), Juni/Juli unberührt.
## Nachtrag 2026-08-21: Ticket Kunz Teil 2 – Mai-Punkte & Bockhacker-Lieferzähler
Re-Verifikation mit DHL-Daten ergab ein anderes Bild als die erste Analyse:
- Die Mai-Lieferungen **wurden versandt und zugestellt** (DHL 12.05.), aber nie bezahlt
(`txaction='prev'`). Am 17.06. hat ein Admin zwei Bestellungen per „extern erledigt"
(Status 8) nachgebucht: Bockhackers **Mai**-Order 53619 (24 P) und Dannenhauers
**Juni**-Order 55443 (54 P) – Buchungsmonat jeweils Juni (Systemregel: aktueller Monat).
- **Bockhacker:** Es fehlt nichts – die 24 P der Mai-Lieferung existieren (als Juni
gebucht). Kein Nachtrag, sonst Doppelbuchung.
- **Dannenhauer:** Ihre Mai-Order 53274 (geliefert!) wurde beim Nachtragen am 17.06.
**vergessen** → 54 P fehlten wirklich. Nachgebucht am 21.08. per neuem Command
`abo:extern-done 53274` (Buchung August, keine Retro-Buchung; +54 P auch im Incentive).
Hinweis: Abo 198 ist seit 06.07. gelöscht, Kundin hat kein aktives Abo mehr; die
Orders 54890/55441/55442/57268 wurden laut DHL nie versandt und bleiben offen.
- **Bockhacker Lieferzähler:** Abo 197 (09.04.) wurde am 06.07. gelöscht und durch
Abo 496 ersetzt → Zähler startete bei 0. Per neuem Command `abo:move-orders 197 496`
wurde die Lieferhistorie umgehängt; Abo 496 zählt jetzt **4 ausgeführte Lieferungen**
(09.04., 12.05. [extern erledigt], 10.07., 10.08.). Laut DHL war die Juli-Lieferung
die **3.** (nicht die 4. wie im Ticket) – es gab keine Juni-Sendung an Bockhacker;
die Juni-*Buchung* vom 17.06. gehörte zur Mai-Lieferung.
Code-Änderungen dafür:
- `AboExternalCompletionService`: funktioniert jetzt auch für Bestellungen
gelöschter/ersetzter Abos (Punkte + Status 8 + Versandstatus; das gelöschte Abo wird
**nicht** reaktiviert, kein neuer Zyklus).
- `IncentiveTracker::trackSalesVolume`: Abo-Zuordnung inkl. gelöschter Abos
(`withTrashed`) – nachgebuchte Umsätze bereits getrackter, inzwischen gelöschter
Abos zählen weiter im Incentive.
- Neue Wartungs-Commands: `abo:extern-done {shopping_order_id}` und
`abo:move-orders {from_abo} {to_abo} [--dry-run]` (Guards: gleicher Kunde/Berater).
- Fix eines latent datumsabhängigen Tests in `AboExternalCompletionTest`
(Erwartung nutzte `setTestNow`, Produktion bucht mit `date()`).
**Live-Ausführung nach Deploy:**
```bash
php artisan abo:extern-done 53274 # Dannenhauer Mai-Lieferung: +54 P an Kunz
php artisan abo:move-orders 197 496 # Bockhacker Lieferhistorie → Abo 496
php artisan incentive:calculate 2 # optional sofort (sonst Nacht-Cron 05:00)
```
## Geänderte Dateien
| Datei | Änderung |

View file

@ -3,6 +3,7 @@
use App\Models\Country;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveParticipant;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
@ -11,6 +12,7 @@ use App\Models\UserAccount;
use App\Models\UserSalesVolume;
use App\Services\AboExternalCompletionService;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@ -134,7 +136,7 @@ it('markiert eine angehaltene Abo-Bestellung als extern erledigt, schreibt Punkt
// 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();
expect(Carbon::parse($userAbo->getRawOriginal('next_date'))->isFuture())->toBeTrue();
// Abo-Lieferungs-Zaehler beruecksichtigt die extern erledigte Ausfuehrung.
$fresh = UserAbo::find($userAbo->id);
@ -153,14 +155,14 @@ it('markiert eine angehaltene Abo-Bestellung als extern erledigt, schreibt Punkt
});
it('trackt Incentive-Punkte mit dem Bestelldatum (Ausfuehrungsmonat), nicht mit heute', function () {
\Carbon\Carbon::setTestNow('2026-06-15');
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');
$executionDate = Carbon::parse('2026-04-10');
ShoppingOrder::where('id', $order->id)->update(['created_at' => $executionDate]);
Incentive::factory()->active()->create([
@ -177,15 +179,16 @@ it('trackt Incentive-Punkte mit dem Bestelldatum (Ausfuehrungsmonat), nicht mit
->and($newAbo->activated_at->toDateString())->toBe('2026-04-10');
// Incentive-Zaehler (qualified_abos) des Teilnehmers wurde erhoeht.
$participant = \App\Models\IncentiveParticipant::find($newAbo->participant_id);
$participant = IncentiveParticipant::find($newAbo->participant_id);
expect((int) $participant->qualified_abos)->toBe(1);
// Reguläre Provisionspunkte hingegen mit heutigem Datum (Juni).
// Reguläre Provisionspunkte hingegen mit dem Buchungstag (SalesPointsVolume
// nutzt date(), nicht Carbon::setTestNow - daher gegen die echte Uhr pruefen).
$volume = UserSalesVolume::where('shopping_order_id', $order->id)->first();
expect((int) $volume->month)->toBe(6)
->and((int) $volume->year)->toBe(2026);
expect((int) $volume->month)->toBe((int) date('m'))
->and((int) $volume->year)->toBe((int) date('Y'));
\Carbon\Carbon::setTestNow();
Carbon::setTestNow();
});
it('ist idempotent und schreibt Punkte nicht doppelt gut', function () {
@ -220,6 +223,28 @@ it('verweigert die Aktion fuer eine Nicht-Abo-Bestellung', function () {
expect($result['success'])->toBeFalse();
});
it('erledigt Bestellungen geloeschter (ersetzter) Abos: Punkte + Zaehler, ohne das Abo zu reaktivieren', function () {
[$userAbo, $order, $aboOrder, $user] = heldAboSeed();
$userAbo->delete();
$result = app(AboExternalCompletionService::class)->complete($order->fresh());
expect($result['success'])->toBeTrue();
expect((int) $aboOrder->fresh()->status)->toBe(8);
// Abo bleibt geloescht und unveraendert - kein neuer Zyklus, keine Reaktivierung
$trashed = UserAbo::withTrashed()->find($userAbo->id);
expect($trashed->trashed())->toBeTrue()
->and((int) $trashed->getRawOriginal('status'))->toBe(3)
->and(Carbon::parse($trashed->getRawOriginal('next_date'))->isPast())->toBeTrue();
// Punkte trotzdem gutgeschrieben
$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);
});
it('erledigt ueber die Abo-Detailseite die juengste angehaltene Bestellung', function () {
[$userAbo, $order] = heldAboSeed();

View file

@ -0,0 +1,88 @@
<?php
use App\Models\Country;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function moveOrdersSeedAbo(string $email, int $memberId, bool $deleted = false): UserAbo
{
$country = Country::firstOrCreate(['code' => 'DE'], [
'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania',
'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $memberId,
'member_id' => $memberId,
'billing_email' => $email,
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'is_for' => 'ot',
]);
$abo = UserAbo::create([
'shopping_user_id' => $shoppingUser->id,
'member_id' => $memberId,
'is_for' => 'ot',
'email' => $email,
'payone_userid' => 1,
'clearingtype' => 'cc',
'active' => ! $deleted,
'status' => 2,
'abo_interval' => 5,
]);
if ($deleted) {
$abo->delete();
}
return $abo;
}
it('haengt die Lieferhistorie eines geloeschten Abos auf das Nachfolge-Abo um', function () {
$old = moveOrdersSeedAbo('kundin@example.com', 42, deleted: true);
$new = moveOrdersSeedAbo('kundin@example.com', 42);
UserAboOrder::create(['user_abo_id' => $old->id, 'shopping_order_id' => 1001, 'status' => 2, 'paid' => true]);
UserAboOrder::create(['user_abo_id' => $old->id, 'shopping_order_id' => 1002, 'status' => 8, 'paid' => false]);
UserAboOrder::create(['user_abo_id' => $new->id, 'shopping_order_id' => 1003, 'status' => 2, 'paid' => true]);
$this->artisan('abo:move-orders', ['from_abo' => $old->id, 'to_abo' => $new->id])
->assertSuccessful();
expect(UserAboOrder::where('user_abo_id', $old->id)->count())->toBe(0)
->and(UserAboOrder::where('user_abo_id', $new->id)->count())->toBe(3);
// bezahlte + extern erledigte (Status 8) Lieferungen zaehlen fuer die Mindestlaufzeit
expect(UserAbo::find($new->id)->getCountPaidOrders())->toBe(3);
});
it('verweigert das Umhaengen zwischen Abos verschiedener Kunden', function () {
$old = moveOrdersSeedAbo('kundin-a@example.com', 42, deleted: true);
$new = moveOrdersSeedAbo('kundin-b@example.com', 42);
UserAboOrder::create(['user_abo_id' => $old->id, 'shopping_order_id' => 1001, 'status' => 2, 'paid' => true]);
$this->artisan('abo:move-orders', ['from_abo' => $old->id, 'to_abo' => $new->id])
->assertFailed();
expect(UserAboOrder::where('user_abo_id', $old->id)->count())->toBe(1);
});
it('aendert im dry-run nichts', function () {
$old = moveOrdersSeedAbo('kundin@example.com', 42, deleted: true);
$new = moveOrdersSeedAbo('kundin@example.com', 42);
UserAboOrder::create(['user_abo_id' => $old->id, 'shopping_order_id' => 1001, 'status' => 2, 'paid' => true]);
$this->artisan('abo:move-orders', ['from_abo' => $old->id, 'to_abo' => $new->id, '--dry-run' => true])
->assertSuccessful();
expect(UserAboOrder::where('user_abo_id', $old->id)->count())->toBe(1)
->and(UserAboOrder::where('user_abo_id', $new->id)->count())->toBe(0);
});

View file

@ -0,0 +1,122 @@
<?php
use App\Models\Country;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveParticipant;
use App\Models\IncentivePointsLog;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Models\UserSalesVolume;
use App\Services\Incentive\IncentiveTracker;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2026-08-19 12:00:00'));
});
afterEach(function () {
Carbon::setTestNow();
});
it('zaehlt nachgebuchte Umsatzpunkte auch, wenn das getrackte Kundenabo inzwischen geloescht ist', function () {
$consultant = User::forceCreate([
'email' => 'berater-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$incentive = Incentive::factory()->create([
'qualification_start' => '2026-04-01',
'qualification_end' => '2026-08-31',
'calculation_end' => '2026-08-31',
'status' => 1,
]);
$participant = IncentiveParticipant::factory()->create([
'incentive_id' => $incentive->id,
'user_id' => $consultant->id,
]);
$country = Country::firstOrCreate(['code' => 'DE'], [
'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania',
'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
]);
$shoppingUser = ShoppingUser::create([
'auth_user_id' => $consultant->id,
'member_id' => $consultant->id,
'billing_email' => 'kundin-'.uniqid('', true).'@example.com',
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'is_for' => 'ot',
]);
$abo = UserAbo::create([
'shopping_user_id' => $shoppingUser->id,
'member_id' => $consultant->id,
'is_for' => 'ot',
'email' => 'kundin-'.uniqid('', true).'@example.com',
'payone_userid' => 1,
'clearingtype' => 'cc',
'active' => true,
'status' => 2,
'abo_interval' => 5,
]);
IncentiveNewAbo::create([
'participant_id' => $participant->id,
'user_abo_id' => $abo->id,
'activated_at' => '2026-04-09',
]);
$order = ShoppingOrder::create([
'shopping_user_id' => $shoppingUser->id,
'auth_user_id' => $consultant->id,
'country_id' => $country->id,
'user_shop_id' => 1,
'payment_for' => 6,
'total' => 92.60,
'subtotal' => 82.79,
'points' => 54,
'paid' => false,
'is_abo' => true,
'mode' => 'live',
]);
UserAboOrder::create([
'user_abo_id' => $abo->id,
'shopping_order_id' => $order->id,
'status' => 8,
'paid' => false,
]);
// Abo wurde inzwischen geloescht (ersetzt) - wie im Fall Dannenhauer (#198)
$abo->delete();
$usv = UserSalesVolume::create([
'user_id' => $consultant->id,
'shopping_order_id' => $order->id,
'month' => 8,
'year' => 2026,
'points' => 54,
'status_points' => 1,
'total_net' => 82.79,
'status' => 2,
]);
IncentiveTracker::trackSalesVolume($usv);
$log = IncentivePointsLog::where('participant_id', $participant->id)
->where('user_sales_volume_id', $usv->id)
->where('is_storno', false)
->first();
expect($log)->not->toBeNull()
->and((int) $log->points_accumulated)->toBe(54)
->and((int) $log->month)->toBe(8);
});