increments('id'); $table->string('first_name')->nullable(); $table->string('last_name')->nullable(); }); Schema::create('user_levels', function ($table) { $table->increments('id'); $table->string('name'); $table->unsignedInteger('pos')->nullable(); $table->boolean('active')->default(true); $table->timestamps(); }); Schema::create('users', function ($table) { $table->increments('id'); $table->string('email')->unique(); $table->string('password'); $table->unsignedInteger('account_id')->nullable(); $table->unsignedInteger('m_level')->nullable(); $table->unsignedInteger('m_sponsor')->nullable(); $table->boolean('active')->default(false); $table->timestamp('active_date')->nullable(); $table->unsignedTinyInteger('admin')->default(0); $table->unsignedTinyInteger('wizard')->default(0); $table->unsignedTinyInteger('blocked')->default(0); $table->char('lang', 2)->default('de'); $table->timestamp('payment_account')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('user_abos', function ($table) { $table->increments('id'); $table->unsignedInteger('user_id')->nullable(); $table->unsignedInteger('member_id')->nullable(); $table->char('is_for', 2)->nullable(); $table->boolean('active')->default(true); $table->unsignedTinyInteger('status')->default(2); $table->date('start_date')->nullable(); $table->date('next_date')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('user_abo_items', function ($table) { $table->increments('id'); $table->unsignedInteger('user_abo_id'); $table->unsignedInteger('product_id')->nullable(); $table->unsignedTinyInteger('comp')->nullable(); $table->unsignedInteger('qty')->default(1); $table->timestamps(); }); Schema::create('user_abo_orders', function ($table) { $table->increments('id'); $table->unsignedInteger('user_abo_id'); $table->unsignedInteger('shopping_order_id'); $table->unsignedTinyInteger('status')->default(2); $table->boolean('paid')->default(true); $table->timestamps(); }); Schema::create('shopping_orders', function ($table) { $table->increments('id'); $table->boolean('is_abo')->default(false); $table->timestamps(); $table->softDeletes(); }); Schema::create('shopping_payments', function ($table) { $table->increments('id'); $table->unsignedInteger('shopping_order_id'); $table->string('clearingtype')->nullable(); $table->string('reference')->nullable(); $table->integer('amount')->nullable(); $table->string('currency')->nullable(); $table->timestamps(); }); Schema::create('payment_transactions', function ($table) { $table->increments('id'); $table->unsignedInteger('shopping_payment_id'); $table->string('request')->nullable(); $table->unsignedInteger('errorcode')->nullable(); $table->string('errormessage')->nullable(); $table->string('customermessage')->nullable(); $table->timestamps(); }); Schema::create('user_sales_volumes', function ($table) { $table->increments('id'); $table->unsignedInteger('user_id'); $table->unsignedInteger('shopping_order_id')->nullable(); $table->unsignedTinyInteger('month')->nullable(); $table->unsignedSmallInteger('year')->nullable(); $table->decimal('month_KP_points', 13, 2)->nullable(); $table->decimal('month_shop_points', 13, 2)->nullable(); $table->decimal('month_total_net', 13, 2)->nullable(); $table->decimal('month_shop_total_net', 13, 2)->nullable(); $table->timestamps(); }); Schema::create('backoffice_statistics_snapshots', function ($table) { $table->id(); $table->unsignedInteger('user_id'); $table->unsignedSmallInteger('year'); $table->unsignedTinyInteger('month'); $table->json('payload'); $table->timestamp('calculated_at')->nullable(); $table->timestamps(); }); }); it('aggregiert Linien, Abos und Punkte fuer die Backoffice-Statistik', function () { UserLevel::forceCreate([ 'id' => 1, 'name' => 'Partner', 'pos' => 1, 'active' => true, ]); $root = User::forceCreate([ 'email' => 'root@test.test', 'password' => 'secret', 'lang' => 'de', 'admin' => 1, ]); $lineOne = User::forceCreate([ 'email' => 'line-one@test.test', 'password' => 'secret', 'lang' => 'de', 'm_sponsor' => $root->id, 'm_level' => 1, 'active_date' => '2026-05-03 00:00:00', 'payment_account' => '2030-01-01 00:00:00', ]); $lineTwo = User::forceCreate([ 'email' => 'line-two@test.test', 'password' => 'secret', 'lang' => 'de', 'm_sponsor' => $lineOne->id, 'm_level' => 1, 'active_date' => '2026-04-03 00:00:00', 'payment_account' => '2030-01-01 00:00:00', ]); User::forceCreate([ 'email' => 'expired-line-one@test.test', 'password' => 'secret', 'lang' => 'de', 'm_sponsor' => $root->id, 'm_level' => 1, 'active_date' => '2026-01-03 00:00:00', 'payment_account' => '2020-01-01 00:00:00', ]); $sponsor = $lineTwo; foreach (range(3, 9) as $lineNumber) { $sponsor = User::forceCreate([ 'email' => 'line-'.$lineNumber.'@test.test', 'password' => 'secret', 'lang' => 'de', 'm_sponsor' => $sponsor->id, 'm_level' => 1, 'active_date' => '2026-04-03 00:00:00', 'payment_account' => '2030-01-01 00:00:00', ]); } $activePartnerAbo = UserAbo::forceCreate([ 'user_id' => $lineOne->id, 'member_id' => $lineOne->id, 'is_for' => 'me', 'active' => true, 'status' => 2, 'start_date' => '2026-05-04', 'next_date' => '2026-06-01', ]); UserAbo::forceCreate([ 'user_id' => $lineTwo->id, 'member_id' => $lineOne->id, 'is_for' => 'ot', 'active' => true, 'status' => 2, 'start_date' => '2026-05-05', 'next_date' => '2026-06-01', ]); $pausedPartnerAbo = UserAbo::forceCreate([ 'user_id' => $lineOne->id, 'member_id' => $lineOne->id, 'is_for' => 'me', 'active' => true, 'status' => 3, 'start_date' => '2026-04-15', 'next_date' => '2026-06-02', ]); $aboOrderId = DB::table('shopping_orders')->insertGetId([ 'is_abo' => true, 'created_at' => now(), 'updated_at' => now(), ]); $singleOrderId = DB::table('shopping_orders')->insertGetId([ 'is_abo' => false, 'created_at' => now(), 'updated_at' => now(), ]); $failedAboOrderId = DB::table('shopping_orders')->insertGetId([ 'is_abo' => true, 'created_at' => now(), 'updated_at' => now(), ]); $failedAboPaymentId = DB::table('shopping_payments')->insertGetId([ 'shopping_order_id' => $failedAboOrderId, 'clearingtype' => 'cc', 'reference' => 'abo-failed', 'amount' => 9900, 'currency' => 'EUR', 'created_at' => now(), 'updated_at' => now(), ]); DB::table('payment_transactions')->insert([ 'shopping_payment_id' => $failedAboPaymentId, 'request' => 'authorization', 'errorcode' => 902, 'errormessage' => 'Bank hat abgelehnt', 'customermessage' => 'Bitte Zahlungsmittel prüfen', 'created_at' => now(), 'updated_at' => now(), ]); DB::table('user_abo_orders')->insert([ 'user_abo_id' => $pausedPartnerAbo->id, 'shopping_order_id' => $failedAboOrderId, 'status' => 3, 'paid' => false, 'created_at' => now(), 'updated_at' => now(), ]); UserSalesVolume::forceCreate([ 'user_id' => $lineOne->id, 'shopping_order_id' => $aboOrderId, 'month' => 5, 'year' => 2026, 'month_KP_points' => 400, 'month_shop_points' => 700, 'month_total_net' => 100, 'month_shop_total_net' => 200, ]); UserSalesVolume::forceCreate([ 'user_id' => $lineTwo->id, 'shopping_order_id' => $singleOrderId, 'month' => 5, 'year' => 2026, 'month_KP_points' => 200, 'month_shop_points' => 300, 'month_total_net' => 50, 'month_shop_total_net' => 60, ]); $overview = (new BackofficeDashboardService)->overview($root, 5, 2026); expect($overview['lines'][1]['consultants'])->toBe(2); expect($overview['lines'][1]['new_partners'])->toBe(1); expect($overview['lines'][1]['team_partner_abos'])->toBe(2); expect($overview['lines'][1]['team_partner_abos_new'])->toBe(1); expect($overview['lines'][1]['team_customer_abos'])->toBe(1); expect($overview['lines'][1]['team_customer_abos_new'])->toBe(1); expect($overview['lines'][1]['own_points'])->toBe(400.0); expect($overview['lines'][1]['external_points'])->toBe(700.0); expect($overview['lines'][1]['customer_abo_points'])->toBe(700.0); expect($overview['lines'][1]['customer_single_order_points'])->toBe(0.0); expect($overview['lines'][1]['customer_other_points'])->toBe(0.0); expect($overview['lines'][1]['total_points'])->toBe(1100.0); expect($overview['lines'][1]['shop_1000'])->toBe(1); expect($overview['lines'][2]['consultants'])->toBe(1); expect($overview['lines'])->toHaveKey(9); expect($overview['lines'])->not->toHaveKey(10); expect($overview['lines'][9]['consultants'])->toBe(1); expect($overview['totals']['total_points'])->toBe(1600.0); $details = (new BackofficeDrilldownService(new BackofficeDashboardService))->details($root, 0, 'total_points', 5, 2026); expect($details['line_label'])->toBe('Alle Linien'); expect($details['rows'])->toHaveCount(2); expect($details['summary']['own_points'])->toBe(600.0); expect($details['summary']['external_points'])->toBe(1000.0); expect($details['summary']['customer_abo_points'])->toBe(700.0); expect($details['summary']['customer_single_order_points'])->toBe(300.0); expect($details['summary']['customer_other_points'])->toBe(0.0); expect($details['summary']['total_points'])->toBe(1600.0); $shop1000Details = (new BackofficeDrilldownService(new BackofficeDashboardService))->details($root, 0, 'shop_1000', 5, 2026); expect($shop1000Details['rows'])->toHaveCount(1); expect($shop1000Details['rows'][0]['career_level'])->toBe('Partner'); expect($shop1000Details['rows'][0]['own_points'])->toBe(400.0); expect($shop1000Details['rows'][0]['customer_abo_points'])->toBe(700.0); $consultantDetails = (new BackofficeDrilldownService(new BackofficeDashboardService))->details($root, 1, 'consultants', 5, 2026); expect($consultantDetails['rows'])->toHaveCount(2); expect($consultantDetails['summary']['count'])->toBe(2); expect(collect($consultantDetails['rows'])->pluck('is_account_active')->all())->toBe([true, false]); expect(collect($consultantDetails['rows'])->pluck('career_level')->all())->toBe(['Partner', 'Partner']); $aboDetails = (new BackofficeDrilldownService(new BackofficeDashboardService))->details($root, 1, 'team_partner_abos', 5, 2026); expect($aboDetails['rows'][0]['start_date'])->toBe('04.05.2026'); expect($aboDetails['rows'][0]['is_new_this_month'])->toBeTrue(); expect($aboDetails['rows'])->toHaveCount(2); expect(collect($aboDetails['rows'])->firstWhere('abo_id', $activePartnerAbo->id)['status_reason'])->toBeNull(); expect(collect($aboDetails['rows'])->firstWhere('abo_id', $pausedPartnerAbo->id)['status_reason'])->toBe('[902] Bank hat abgelehnt'); expect(collect($aboDetails['rows'])->firstWhere('abo_id', $pausedPartnerAbo->id)['is_new_this_month'])->toBeFalse(); }); it('verwendet gespeicherte Snapshots fuer abgeschlossene Monate', function () { $root = User::forceCreate([ 'email' => 'snapshot-root@test.test', 'password' => 'secret', 'lang' => 'de', 'admin' => 1, ]); $payload = [ 'month' => 4, 'year' => 2026, 'metric_labels' => [], 'lines' => [ 1 => [ 'line' => 1, 'label' => 'Linie 1', 'user_ids' => [], 'consultants' => 99, 'new_partners' => 0, 'team_partner_abos' => 0, 'team_partner_abos_new' => 0, 'team_customer_abos' => 0, 'team_customer_abos_new' => 0, 'own_points' => 0, 'external_points' => 0, 'customer_abo_points' => 0, 'customer_single_order_points' => 0, 'customer_other_points' => 0, 'total_points' => 0, 'turnover_net' => 0, 'shop_1000' => 0, ], ], 'totals' => [ 'label' => 'Summe', 'consultants' => 99, ], ]; BackofficeStatisticsSnapshot::create([ 'user_id' => $root->id, 'year' => 2026, 'month' => 4, 'payload' => $payload, 'calculated_at' => now(), ]); $overview = (new BackofficeDashboardService)->overview($root, 4, 2026); expect($overview['lines'][1]['consultants'])->toBe(99); expect($overview['_meta']['source'])->toBe('snapshot'); expect($overview['_meta']['source_label'])->toBe('Snapshot'); });