actingAs($user, 'sales'). * Изоляция: DatabaseTransactions. Глобально подключён SharesAdminPdo (Pest.php). * DB_DATABASE=liderra_testing ОБЯЗАТЕЛЕН при запуске. * * ⚠️ ВРЕМЯ: замораживаем на день 15 — детерминизм периода/партиций. * ⚠️ ПАРТИЦИИ: balance_transactions / lead_charges партиционированы помесячно * динамически → сеем ТОЛЬКО в текущий месяц. */ uses(DatabaseTransactions::class); beforeEach(function () { $now = CarbonImmutable::create(null, null, 15, 12, 0, 0, 'Europe/Moscow'); CarbonImmutable::setTestNow($now); Carbon::setTestNow($now); }); afterEach(function () { CarbonImmutable::setTestNow(); Carbon::setTestNow(); }); // ── helpers (уникальный префикс inc_) ──────────────────────────────────────── function inc_manager(float $baseSalary = 0.0): SalesUser { return SalesUser::create([ 'name' => 'Менеджер '.uniqid(), 'email' => 'incmgr'.uniqid().'@sales.local', 'password' => bcrypt('secret'), 'role' => 'manager', 'is_active' => true, 'base_salary_rub' => $baseSalary, ]); } function inc_head(): SalesUser { return SalesUser::create([ 'name' => 'Начальник '.uniqid(), 'email' => 'inchead'.uniqid().'@sales.local', 'password' => bcrypt('secret'), 'role' => 'head', 'is_active' => true, ]); } function inc_tenant(string $orgName): Tenant { return Tenant::factory()->create([ 'organization_name' => $orgName, 'balance_rub' => 0.0, 'delivered_in_month' => 0, ]); } /** @param array $params */ function inc_assignment(SalesUser $user, Tenant $tenant, string $kind, array $params): SalesClientAssignment { return SalesClientAssignment::create([ 'sales_user_id' => $user->id, 'tenant_id' => $tenant->id, 'tariff_kind' => $kind, 'tariff_params' => $params, 'assigned_at' => Carbon::now('Europe/Moscow')->subMonthsNoOverflow(2)->subDays(15), ]); } /** Дата в ТЕКУЩЕМ месяце МСК (день $day, только Y-m-d). */ function inc_date(int $day): string { return CarbonImmutable::now('Europe/Moscow')->startOfMonth() ->setDay($day)->format('Y-m-d'); } /** lead_charge (для оборота) — текущий месяц. */ function inc_leadCharge(int $tenantId, int $kopecks): void { $dt = inc_date(10).' 10:00:00'; DB::table('lead_charges')->insert([ 'tenant_id' => $tenantId, 'deal_id' => fake()->numberBetween(1, 99999), 'deal_received_at' => $dt, 'tier_no' => 1, 'price_per_lead_kopecks' => $kopecks, 'charge_source' => 'rub', 'charged_at' => $dt, 'created_at' => $dt, ]); } /** Пополнение (topup) — текущий месяц. Доход по тарифу topup_step считается от пополнений. */ function inc_topup(int $tenantId, string $amountRub): void { $dt = inc_date(10).' 10:00:00'; DB::table('balance_transactions')->insert([ 'tenant_id' => $tenantId, 'type' => 'topup', 'amount_rub' => $amountRub, 'amount_leads' => 0, 'balance_rub_after' => $amountRub, 'description' => 'test', 'created_at' => $dt, ]); } /** topup_step с порогом 0, бонусом 0 и одной ступенью → процент со всех пополнений. */ function inc_stepParams(int $rate): array { return ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => $rate]]]; } /** Готовая выплата в БД. */ function inc_payout(int $managerId, int $headId, string $amount, string $paidOn): SalesPayout { return SalesPayout::create([ 'sales_user_id' => $managerId, 'amount_rub' => $amount, 'paid_on' => $paidOn, 'comment' => null, 'created_by' => $headId, ]); } // ── 1. менеджер с 2 клиентами: totals + per_client ─────────────────────────── test('income: 2 клиента topup_step — totals.oborot + accrued + per_client', function () { // Тариф topup_step (оклада нет). Клиент А: 20% от пополнений 50 000 → 10 000. // Клиент Б: 10% от пополнений 30 000 → 3 000. $manager = inc_manager(baseSalary: 30000.0); $tA = inc_tenant('ООО Альфа'); inc_assignment($manager, $tA, 'topup_step', inc_stepParams(20)); inc_leadCharge($tA->id, 5_000_000); // оборот 50 000 ₽ (для метрики oborot_rub) inc_topup($tA->id, '50000.00'); // пополнения 50 000 ₽ → комиссия 10 000 $tB = inc_tenant('ООО Бета'); inc_assignment($manager, $tB, 'topup_step', inc_stepParams(10)); inc_leadCharge($tB->id, 3_000_000); // оборот 30 000 ₽ inc_topup($tB->id, '30000.00'); // пополнения 30 000 ₽ → комиссия 3 000 $response = $this->actingAs($manager, 'sales')->getJson('/api/sales/income?period=this'); $response->assertOk(); // totals: оборот 50 000 + 30 000 = 80 000; начислено 10 000 + 3 000 = 13 000 (оклада нет). expect((float) $response->json('totals.oborot_rub'))->toBe(80000.0) ->and((float) $response->json('totals.accrued_rub'))->toBe(13000.0) ->and((float) $response->json('totals.paid_all_time_rub'))->toBe(0.0) ->and((float) $response->json('totals.to_pay_period_rub'))->toBe(13000.0); $perClient = collect($response->json('per_client'))->keyBy('tenant_id'); expect($perClient)->toHaveCount(2); expect($perClient[$tA->id]['organization_name'])->toBe('ООО Альфа') ->and($perClient[$tA->id]['tariff_kind'])->toBe('topup_step') ->and($perClient[$tA->id]['tariff_label'])->toBe('Процент от пополнений') ->and((float) $perClient[$tA->id]['earned_rub'])->toBe(10000.0) ->and($perClient[$tA->id])->toHaveKey('topups_rub') ->and($perClient[$tB->id]['organization_name'])->toBe('ООО Бета') ->and((float) $perClient[$tB->id]['earned_rub'])->toBe(3000.0); expect($response->json('payouts'))->toBe([]); }); // ── 2. выплата в периоде: to_pay_period + paid_all_time + payouts ───────────── test('income: проведённая выплата уменьшает to_pay_period, попадает в payouts', function () { $head = inc_head(); $manager = inc_manager(baseSalary: 30000.0); $tA = inc_tenant('ООО Гамма'); inc_assignment($manager, $tA, 'topup_step', inc_stepParams(20)); inc_leadCharge($tA->id, 5_000_000); // оборот 50 000 (метрика) inc_topup($tA->id, '50000.00'); // пополнения 50 000 → комиссия 10 000 // Начислено = 10 000 (оклада нет). Выплата в периоде 6 000 → остаток 4 000. inc_payout($manager->id, $head->id, '6000.00', inc_date(12)); $response = $this->actingAs($manager, 'sales')->getJson('/api/sales/income?period=this'); $response->assertOk(); expect((float) $response->json('totals.accrued_rub'))->toBe(10000.0) ->and((float) $response->json('totals.to_pay_period_rub'))->toBe(4000.0) ->and((float) $response->json('totals.paid_all_time_rub'))->toBe(6000.0); $payouts = $response->json('payouts'); expect($payouts)->toHaveCount(1) ->and((float) $payouts[0]['amount_rub'])->toBe(6000.0) ->and($payouts[0]['sales_user_id'])->toBe($manager->id); }); // ── 3. пустой менеджер: нули ────────────────────────────────────────────────── test('income: менеджер без клиентов → per_client=[], totals нули, payouts=[]', function () { $manager = inc_manager(baseSalary: 0.0); $response = $this->actingAs($manager, 'sales')->getJson('/api/sales/income?period=this'); $response->assertOk() ->assertJsonPath('per_client', []) ->assertJsonPath('payouts', []); expect((float) $response->json('totals.oborot_rub'))->toBe(0.0) ->and((float) $response->json('totals.accrued_rub'))->toBe(0.0) ->and((float) $response->json('totals.paid_all_time_rub'))->toBe(0.0) ->and((float) $response->json('totals.to_pay_period_rub'))->toBe(0.0); }); // ── 4. payouts менеджера — только свои ──────────────────────────────────────── test('income: payouts содержит ТОЛЬКО выплаты этого менеджера', function () { $head = inc_head(); $manager = inc_manager(); $other = inc_manager(); inc_payout($manager->id, $head->id, '1000.00', inc_date(5)); inc_payout($other->id, $head->id, '9999.00', inc_date(6)); $payouts = $this->actingAs($manager, 'sales') ->getJson('/api/sales/income?period=this')->json('payouts'); expect($payouts)->toHaveCount(1) ->and($payouts[0]['sales_user_id'])->toBe($manager->id); }); // ── 5. head без клиентов → нули ─────────────────────────────────────────────── test('income: head без закреплённых клиентов → нули', function () { $head = inc_head(); $response = $this->actingAs($head, 'sales')->getJson('/api/sales/income?period=this'); $response->assertOk() ->assertJsonPath('per_client', []); expect((float) $response->json('totals.accrued_rub'))->toBe(0.0); }); // ── 6. unauthenticated ─────────────────────────────────────────────────────── test('income: неаутентифицированный GET /api/sales/income → 401', function () { $this->getJson('/api/sales/income')->assertUnauthorized(); });