startOfMonth() ->setDay($day)->format('Y-m-d').' '.$time; } /** Начало ПРОШЛОГО месяца МСК. */ function earn_prevMonthStart(): CarbonImmutable { return CarbonImmutable::now('Europe/Moscow')->subMonthsNoOverflow(1)->startOfMonth(); } /** Дата/время в ПРОШЛОМ месяце МСК (день $day). Требует созданной партиции. */ function earn_prevBtDate(int $day, string $time = '10:00:00'): string { return earn_prevMonthStart()->setDay($day)->format('Y-m-d').' '.$time; } /** Создаёт партицию balance_transactions на прошлый месяц (для cross-month тестов). */ function earn_ensurePrevPartition(): void { app(MonthlyPartitionManager::class)->ensureMonth('balance_transactions', earn_prevMonthStart()); } /** SalesPeriodRange на весь текущий месяц МСК. */ function earn_thisMonthRange(): SalesPeriodRange { $start = CarbonImmutable::now('Europe/Moscow')->startOfMonth(); return new SalesPeriodRange( $start->startOfDay(), $start->endOfMonth()->setTime(23, 59, 59), ); } /** SalesPeriodRange на произвольный календарный месяц (для детерминированных тестов оклада). */ function earn_monthRange(int $year, int $month): SalesPeriodRange { $start = CarbonImmutable::create($year, $month, 1, 0, 0, 0, 'Europe/Moscow'); return new SalesPeriodRange( $start->startOfDay(), $start->endOfMonth()->setTime(23, 59, 59), ); } /** Свежий сервис с реальными зависимостями. */ function earn_service(): SalesEarningsService { return new SalesEarningsService( new SalesMetricsService, new SalesPeriodResolver, ); } /** Тенант. */ function earn_makeTenant(): Tenant { return Tenant::factory()->create([ 'balance_rub' => 0.0, 'delivered_in_month' => 0, ]); } /** Тариф-шаблон. */ function earn_makeTariff(string $kind, array $params = []): SalesTariff { return SalesTariff::create([ 'name' => $kind.' '.uniqid(), 'kind' => $kind, 'params' => $params, 'is_active' => true, ]); } /** * Менеджер отдела продаж. base_salary_rub для daily_salary = ставка ₽/день. * $createdAt (дата приёма) — переопределяет авто-created_at, если задан. */ function earn_makeSalesUser( float $baseSalary = 0.0, ?int $currentTariffId = null, ?string $createdAt = null, ): SalesUser { $user = SalesUser::create([ 'name' => 'Manager '.fake()->firstName(), 'email' => uniqid('mgr_').'@t.local', 'password' => Hash::make('secret-x'), 'role' => 'manager', 'is_active' => true, 'base_salary_rub' => $baseSalary, 'current_tariff_id' => $currentTariffId, ]); if ($createdAt !== null) { $user->created_at = CarbonImmutable::parse($createdAt.' 00:00:00', 'Europe/Moscow'); $user->save(); } return $user; } /** * Привязка клиента к менеджеру со снимком тарифа. * * @param array $tariffParams */ function earn_assignmentWithTariff( SalesUser $user, Tenant $tenant, ?string $tariffKind, array $tariffParams, ?Carbon $assignedAt = null, ): SalesClientAssignment { return SalesClientAssignment::create([ 'sales_user_id' => $user->id, 'tenant_id' => $tenant->id, 'tariff_kind' => $tariffKind, 'tariff_params' => $tariffParams, 'assigned_at' => $assignedAt ?? Carbon::now('Europe/Moscow')->subMonthsNoOverflow(2)->subDays(15), ]); } /** balance_transaction (пополнение). */ function earn_insertBalanceTx(int $tenantId, string $amountRub, string $createdAt): void { DB::table('balance_transactions')->insert([ 'tenant_id' => $tenantId, 'type' => 'topup', 'amount_rub' => $amountRub, 'amount_leads' => 0, 'balance_rub_after' => $amountRub, 'description' => 'test', 'created_at' => $createdAt, ]); } /** Ступени 1-3=10% 4-6=7% 7-9=5% 10-12=3% 13-24=1%. */ function earn_defaultSteps(): array { return [ ['from' => 1, 'to' => 3, 'rate' => 10], ['from' => 4, 'to' => 6, 'rate' => 7], ['from' => 7, 'to' => 9, 'rate' => 5], ['from' => 10, 'to' => 12, 'rate' => 3], ['from' => 13, 'to' => 24, 'rate' => 1], ]; } // ── 1. topup_step: порог + бонус + процент сверх ───────────────────────────── test('topup_step: пополнения ниже порога → 0 (ни бонуса, ни процента)', function () { $user = earn_makeSalesUser(); $tenant = earn_makeTenant(); $a = earn_assignmentWithTariff($user, $tenant, 'topup_step', [ 'threshold' => 30000, 'reward' => 10000, 'periods' => earn_defaultSteps(), ]); // 20 000 < порог 30 000 → бонус не начислен, ничего сверх порога → 0. earn_insertBalanceTx($tenant->id, '20000.00', earn_btDate(10)); expect(earn_service()->forAssignment($a, earn_thisMonthRange()))->toBe(0.0); }); test('topup_step: достижение порога → разовый бонус + процент со суммы СВЕРХ порога', function () { $user = earn_makeSalesUser(); $tenant = earn_makeTenant(); // Привязан ~2.5 мес назад → tenure текущего месяца = 3 → ступень 1-3 = 10%. $a = earn_assignmentWithTariff($user, $tenant, 'topup_step', [ 'threshold' => 30000, 'reward' => 10000, 'periods' => earn_defaultSteps(), ]); // Пополнения 50 000: бонус 10 000 + процент со сверх-порога (20 000 × 10% = 2000) = 12 000. earn_insertBalanceTx($tenant->id, '20000.00', earn_btDate(5)); earn_insertBalanceTx($tenant->id, '30000.00', earn_btDate(20)); expect(earn_service()->forAssignment($a, earn_thisMonthRange()))->toBe(12000.0); }); test('topup_step: ставка процента — по ступени срока клиента (сверх-порог × ставка ступени)', function () { $user = earn_makeSalesUser(); $tenant = earn_makeTenant(); // Привязан ~4.5 мес назад → tenure = 5 → ступень 4-6 = 7%. Бонус 0 (изолируем процент). $a = earn_assignmentWithTariff($user, $tenant, 'topup_step', [ 'threshold' => 30000, 'reward' => 0, 'periods' => earn_defaultSteps(), ], assignedAt: Carbon::now('Europe/Moscow')->subMonthsNoOverflow(4)->subDays(15)); // 50 000: сверх-порог 20 000 × 7% = 1400. earn_insertBalanceTx($tenant->id, '50000.00', earn_btDate(10)); expect(earn_service()->forAssignment($a, earn_thisMonthRange()))->toBe(1400.0); }); test('topup_step: разовый бонус — ОДИН раз (порог пройден в прошлом месяце → бонуса нет)', function () { earn_ensurePrevPartition(); $user = earn_makeSalesUser(); $tenant = earn_makeTenant(); // tenure текущего месяца = 3 → 10%. $a = earn_assignmentWithTariff($user, $tenant, 'topup_step', [ 'threshold' => 30000, 'reward' => 10000, 'periods' => earn_defaultSteps(), ]); // Прошлый месяц: 40 000 → порог уже пройден до начала периода. earn_insertBalanceTx($tenant->id, '40000.00', earn_prevBtDate(10)); // Текущий месяц: 10 000 — всё сверх порога → 10% = 1000. Бонус НЕ повторяется. earn_insertBalanceTx($tenant->id, '10000.00', earn_btDate(10)); expect(earn_service()->forAssignment($a, earn_thisMonthRange()))->toBe(1000.0); }); test('topup_step: порог 0 и бонус 0 → процент со всей суммы пополнений', function () { $user = earn_makeSalesUser(); $tenant = earn_makeTenant(); // tenure = 3 → 10%. $a = earn_assignmentWithTariff($user, $tenant, 'topup_step', [ 'threshold' => 0, 'reward' => 0, 'periods' => earn_defaultSteps(), ]); earn_insertBalanceTx($tenant->id, '30000.00', earn_btDate(10)); expect(earn_service()->forAssignment($a, earn_thisMonthRange()))->toBe(3000.0); }); test('topup_step: без пополнений в периоде → 0.0', function () { $user = earn_makeSalesUser(); $tenant = earn_makeTenant(); $a = earn_assignmentWithTariff($user, $tenant, 'topup_step', [ 'threshold' => 30000, 'reward' => 10000, 'periods' => earn_defaultSteps(), ]); expect(earn_service()->forAssignment($a, earn_thisMonthRange()))->toBe(0.0); }); // ── 2. daily_salary ────────────────────────────────────────────────────────── test('daily_salary: forAssignment = 0 (оклад — уровень менеджера, не клиента)', function () { $user = earn_makeSalesUser(); $tenant = earn_makeTenant(); $a = earn_assignmentWithTariff($user, $tenant, 'daily_salary', []); earn_insertBalanceTx($tenant->id, '99999.00', earn_btDate(10)); expect(earn_service()->forAssignment($a, earn_thisMonthRange()))->toBe(0.0); }); test('forManager: суточный оклад × отработанные дни СО ДНЯ ПРИЁМА', function () { $tariff = earn_makeTariff('daily_salary'); // Приём 10.03.2026, ставка 1000 ₽/день. Период — март (31 день). // Отработано 10..31 марта = 22 дня → 22 000. $user = earn_makeSalesUser(baseSalary: 1000.0, currentTariffId: $tariff->id, createdAt: '2026-03-10'); expect(earn_service()->forManager($user, earn_monthRange(2026, 3)))->toBe(22000.0); }); test('forManager: суточный оклад — принят до периода → полный период', function () { $tariff = earn_makeTariff('daily_salary'); // Приём 01.02.2026 (до марта). Март = 31 день × 1000 = 31 000. $user = earn_makeSalesUser(baseSalary: 1000.0, currentTariffId: $tariff->id, createdAt: '2026-02-01'); expect(earn_service()->forManager($user, earn_monthRange(2026, 3)))->toBe(31000.0); }); // ── 3. forManager: текущий тариф определяет режим оплаты ────────────────────── test('forManager: текущий тариф — процент → Σ по клиентам, оклада НЕТ', function () { $tariff = earn_makeTariff('topup_step', [ 'threshold' => 0, 'reward' => 0, 'periods' => earn_defaultSteps(), ]); // base_salary 30000 задан, но НЕ начисляется (тариф не daily_salary). $user = earn_makeSalesUser(baseSalary: 30000.0, currentTariffId: $tariff->id); $tenant = earn_makeTenant(); earn_assignmentWithTariff($user, $tenant, 'topup_step', [ 'threshold' => 0, 'reward' => 0, 'periods' => earn_defaultSteps(), ]); // tenure 3 → 10%, пополнения 50 000 → 5000. Оклада нет. earn_insertBalanceTx($tenant->id, '50000.00', earn_btDate(10)); expect(earn_service()->forManager($user, earn_thisMonthRange()))->toBe(5000.0); }); test('forManager: без текущего тарифа → 0 (оклад только у daily_salary)', function () { $user = earn_makeSalesUser(baseSalary: 30000.0); expect(earn_service()->forManager($user, earn_thisMonthRange()))->toBe(0.0); }); // ── 4. perClient ───────────────────────────────────────────────────────────── test('perClient: строка на каждого клиента с earned_rub (topup_step)', function () { $user = earn_makeSalesUser(); // Клиент 1: порог 30 000, бонус 10 000, tenure 3 → 10%. Пополнения 50 000 → 10000 + 2000 = 12000. $t1 = earn_makeTenant(); earn_assignmentWithTariff($user, $t1, 'topup_step', [ 'threshold' => 30000, 'reward' => 10000, 'periods' => earn_defaultSteps(), ]); earn_insertBalanceTx($t1->id, '50000.00', earn_btDate(10)); // Клиент 2: без пополнений → 0. $t2 = earn_makeTenant(); earn_assignmentWithTariff($user, $t2, 'topup_step', [ 'threshold' => 30000, 'reward' => 10000, 'periods' => earn_defaultSteps(), ]); $rows = earn_service()->perClient($user, earn_thisMonthRange()); expect($rows)->toHaveCount(2); $byTenant = collect($rows)->keyBy('tenant_id'); expect($byTenant[$t1->id]['tariff_kind'])->toBe('topup_step') ->and($byTenant[$t1->id]['earned_rub'])->toBe(12000.0) ->and($byTenant[$t2->id]['earned_rub'])->toBe(0.0) ->and($byTenant[$t2->id])->toHaveKey('topups_rub'); }); // ── 5. края ────────────────────────────────────────────────────────────────── test('forAssignment: неизвестный tariff_kind → 0.0', function () { $user = earn_makeSalesUser(); $tenant = earn_makeTenant(); $a = earn_assignmentWithTariff($user, $tenant, 'percent_oborot', ['rate' => 99]); earn_insertBalanceTx($tenant->id, '50000.00', earn_btDate(10)); expect(earn_service()->forAssignment($a, earn_thisMonthRange()))->toBe(0.0); }); test('forAssignment: tariff_kind = null → 0.0', function () { $user = earn_makeSalesUser(); $tenant = earn_makeTenant(); $a = earn_assignmentWithTariff($user, $tenant, null, []); earn_insertBalanceTx($tenant->id, '50000.00', earn_btDate(10)); expect(earn_service()->forAssignment($a, earn_thisMonthRange()))->toBe(0.0); }); // ── 6. estimatedForCurrentTariff (оценка «если бы все клиенты на текущем тарифе») ── test('estimatedForCurrentTariff: текущий daily_salary → суточный оклад (клиенты не влияют)', function () { $tariff = earn_makeTariff('daily_salary'); $user = earn_makeSalesUser(baseSalary: 1000.0, currentTariffId: $tariff->id, createdAt: '2026-03-01'); // Клиент со снимком topup_step — на оценку daily_salary не влияет. $t = earn_makeTenant(); earn_assignmentWithTariff($user, $t, 'topup_step', ['threshold' => 0, 'reward' => 0, 'periods' => earn_defaultSteps()]); // Март 31 день × 1000 = 31 000. expect(earn_service()->estimatedForCurrentTariff($user, earn_monthRange(2026, 3)))->toBe(31000.0); }); test('estimatedForCurrentTariff: текущий topup_step → Σ по клиентам как на текущем тарифе', function () { $tariff = earn_makeTariff('topup_step', ['threshold' => 0, 'reward' => 0, 'periods' => earn_defaultSteps()]); $user = earn_makeSalesUser(currentTariffId: $tariff->id); // Снимок клиента другой (null), но оценка берёт ТЕКУЩИЙ тариф: tenure 3 → 10%. $t = earn_makeTenant(); earn_assignmentWithTariff($user, $t, null, []); earn_insertBalanceTx($t->id, '50000.00', earn_btDate(10)); expect(earn_service()->estimatedForCurrentTariff($user, earn_thisMonthRange()))->toBe(5000.0); }); test('estimatedForCurrentTariff: без текущего тарифа → 0', function () { $user = earn_makeSalesUser(baseSalary: 30000.0); expect(earn_service()->estimatedForCurrentTariff($user, earn_thisMonthRange()))->toBe(0.0); });