seed(PricingTierSeeder::class); }); it('GET /api/admin/pricing-tiers returns active + scheduled sets', function () { $response = $this->getJson('/api/admin/pricing-tiers'); $response->assertOk(); expect($response->json('data.active'))->toHaveCount(7); }); it('POST creates 7 new tiers with auto effective_from = 1st of next month', function () { $payload = ['tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '600.00'], ['tier_no' => 2, 'leads_in_tier' => 150, 'price_rub' => '550.00'], ['tier_no' => 3, 'leads_in_tier' => 300, 'price_rub' => '500.00'], ['tier_no' => 4, 'leads_in_tier' => 700, 'price_rub' => '450.00'], ['tier_no' => 5, 'leads_in_tier' => 1500, 'price_rub' => '400.00'], ['tier_no' => 6, 'leads_in_tier' => 3000, 'price_rub' => '350.00'], ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '300.00'], ]]; $this->postJson('/api/admin/pricing-tiers', $payload)->assertCreated(); $expectedDate = now('Europe/Moscow')->startOfMonth()->addMonth()->toDateString(); $newTiers = PricingTier::where('effective_from', $expectedDate)->get(); expect($newTiers)->toHaveCount(7); expect($newTiers->where('tier_no', 1)->first()->price_per_lead_kopecks)->toBe(60000); expect($newTiers->where('tier_no', 7)->first()->leads_in_tier)->toBeNull(); }); it('POST validates: exactly 7 rows required', function () { $this->postJson('/api/admin/pricing-tiers', ['tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '600.00'], ]])->assertStatus(422); }); it('POST validates: tier_no must be unique 1..7', function () { $this->postJson('/api/admin/pricing-tiers', ['tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '600.00'], ['tier_no' => 1, 'leads_in_tier' => 150, 'price_rub' => '550.00'], ['tier_no' => 3, 'leads_in_tier' => 300, 'price_rub' => '500.00'], ['tier_no' => 4, 'leads_in_tier' => 700, 'price_rub' => '450.00'], ['tier_no' => 5, 'leads_in_tier' => 1500, 'price_rub' => '400.00'], ['tier_no' => 6, 'leads_in_tier' => 3000, 'price_rub' => '350.00'], ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '300.00'], ]])->assertStatus(422); }); it('POST validates: tier 7 leads_in_tier must be null', function () { $this->postJson('/api/admin/pricing-tiers', ['tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '600.00'], ['tier_no' => 2, 'leads_in_tier' => 150, 'price_rub' => '550.00'], ['tier_no' => 3, 'leads_in_tier' => 300, 'price_rub' => '500.00'], ['tier_no' => 4, 'leads_in_tier' => 700, 'price_rub' => '450.00'], ['tier_no' => 5, 'leads_in_tier' => 1500, 'price_rub' => '400.00'], ['tier_no' => 6, 'leads_in_tier' => 3000, 'price_rub' => '350.00'], ['tier_no' => 7, 'leads_in_tier' => 99999, 'price_rub' => '300.00'], ]])->assertStatus(422); }); it('POST validates: price_rub >= 0', function () { $this->postJson('/api/admin/pricing-tiers', ['tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '-1.00'], ['tier_no' => 2, 'leads_in_tier' => 150, 'price_rub' => '550.00'], ['tier_no' => 3, 'leads_in_tier' => 300, 'price_rub' => '500.00'], ['tier_no' => 4, 'leads_in_tier' => 700, 'price_rub' => '450.00'], ['tier_no' => 5, 'leads_in_tier' => 1500, 'price_rub' => '400.00'], ['tier_no' => 6, 'leads_in_tier' => 3000, 'price_rub' => '350.00'], ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '300.00'], ]])->assertStatus(422); }); it('DELETE /scheduled/{effective_from} removes future tiers only', function () { $futureDate = now('Europe/Moscow')->addMonth()->startOfMonth()->toDateString(); PricingTier::factory()->count(7)->sequence(fn ($s) => ['tier_no' => $s->index + 1]) ->create(['effective_from' => $futureDate, 'is_active' => true]); $this->deleteJson("/api/admin/pricing-tiers/scheduled/{$futureDate}")->assertOk(); expect(PricingTier::where('effective_from', $futureDate)->count())->toBe(0); expect(PricingTier::where('effective_from', '1970-01-01')->count())->toBe(7); }); it('store accepts a custom effective_from date', function (): void { $custom = Carbon::now('Europe/Moscow')->addMonths(3)->toDateString(); $response = $this->postJson('/api/admin/pricing-tiers', [ 'tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '600.00'], ['tier_no' => 2, 'leads_in_tier' => 150, 'price_rub' => '550.00'], ['tier_no' => 3, 'leads_in_tier' => 300, 'price_rub' => '500.00'], ['tier_no' => 4, 'leads_in_tier' => 700, 'price_rub' => '450.00'], ['tier_no' => 5, 'leads_in_tier' => 1500, 'price_rub' => '400.00'], ['tier_no' => 6, 'leads_in_tier' => 3000, 'price_rub' => '350.00'], ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '300.00'], ], 'effective_from' => $custom, ]); $response->assertCreated()->assertJson(['effective_from' => $custom]); expect(PricingTier::where('effective_from', $custom)->count())->toBe(7); }); it('store принимает effective_from равную сегодня (по запросу владельца)', function (): void { // Раньше today отвергался (after:today). Владелец попросил разрешить смену // тарифа текущей датой → after_or_equal:today. Прошлое по-прежнему 422. $today = Carbon::now('Europe/Moscow')->toDateString(); $this->postJson('/api/admin/pricing-tiers', [ 'tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '600.00'], ['tier_no' => 2, 'leads_in_tier' => 150, 'price_rub' => '550.00'], ['tier_no' => 3, 'leads_in_tier' => 300, 'price_rub' => '500.00'], ['tier_no' => 4, 'leads_in_tier' => 700, 'price_rub' => '450.00'], ['tier_no' => 5, 'leads_in_tier' => 1500, 'price_rub' => '400.00'], ['tier_no' => 6, 'leads_in_tier' => 3000, 'price_rub' => '350.00'], ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '300.00'], ], 'effective_from' => $today, ])->assertCreated()->assertJson(['effective_from' => $today]); expect(PricingTier::where('effective_from', $today)->count())->toBe(7); }); it('store берёт audit admin_user_id из config и не трогает saas_admin_users', function (): void { // Прод-баг: рантайм-роль crm_app_user не имеет прав на saas_admin_users → // resolveAdminUserId падал «permission denied» → 500 на ВСЕХ admin-сохранениях. // Фикс: при config('admin.audit_system_user_id') брать id оттуда, не обращаясь // к saas_admin_users (на dev/test суперюзер — fallback на старую логику). $adminId = DB::table('saas_admin_users')->insertGetId([ 'email' => 'cfg-admin@liderra.local', 'full_name' => 'Cfg Admin', 'password_hash' => 'stub', 'role' => 'super_admin', 'is_active' => false, 'sso_provider' => 'local', 'is_break_glass' => false, ]); config(['admin.audit_system_user_id' => $adminId]); $countBefore = DB::table('saas_admin_users')->count(); $this->postJson('/api/admin/pricing-tiers', ['tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '600.00'], ['tier_no' => 2, 'leads_in_tier' => 150, 'price_rub' => '550.00'], ['tier_no' => 3, 'leads_in_tier' => 300, 'price_rub' => '500.00'], ['tier_no' => 4, 'leads_in_tier' => 700, 'price_rub' => '450.00'], ['tier_no' => 5, 'leads_in_tier' => 1500, 'price_rub' => '400.00'], ['tier_no' => 6, 'leads_in_tier' => 3000, 'price_rub' => '350.00'], ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '300.00'], ]])->assertCreated(); $log = DB::table('saas_admin_audit_log') ->where('action', 'pricing_tiers.create_scheduled')->latest('id')->first(); expect((int) $log->admin_user_id)->toBe($adminId); // стаб system-pricing НЕ создан — saas_admin_users не трогали. expect(DB::table('saas_admin_users')->count())->toBe($countBefore); }); it('store rejects effective_from in the past', function (): void { $past = Carbon::now('Europe/Moscow')->subDay()->toDateString(); $this->postJson('/api/admin/pricing-tiers', [ 'tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '600.00'], ['tier_no' => 2, 'leads_in_tier' => 150, 'price_rub' => '550.00'], ['tier_no' => 3, 'leads_in_tier' => 300, 'price_rub' => '500.00'], ['tier_no' => 4, 'leads_in_tier' => 700, 'price_rub' => '450.00'], ['tier_no' => 5, 'leads_in_tier' => 1500, 'price_rub' => '400.00'], ['tier_no' => 6, 'leads_in_tier' => 3000, 'price_rub' => '350.00'], ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '300.00'], ], 'effective_from' => $past, ])->assertStatus(422); }); it('writes audit-trail row in saas_admin_audit_log on POST', function () { $this->postJson('/api/admin/pricing-tiers', ['tiers' => [ ['tier_no' => 1, 'leads_in_tier' => 50, 'price_rub' => '600.00'], ['tier_no' => 2, 'leads_in_tier' => 150, 'price_rub' => '550.00'], ['tier_no' => 3, 'leads_in_tier' => 300, 'price_rub' => '500.00'], ['tier_no' => 4, 'leads_in_tier' => 700, 'price_rub' => '450.00'], ['tier_no' => 5, 'leads_in_tier' => 1500, 'price_rub' => '400.00'], ['tier_no' => 6, 'leads_in_tier' => 3000, 'price_rub' => '350.00'], ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '300.00'], ]])->assertCreated(); $log = DB::table('saas_admin_audit_log') ->where('action', 'pricing_tiers.create_scheduled')->first(); expect($log)->not->toBeNull(); }); test('AdminPricingTiers::store сохраняет цену 10.10 ₽ как ровно 1010 kopecks (без float-drift)', function () { $tiers = []; for ($i = 1; $i <= 6; $i++) { $tiers[] = ['tier_no' => $i, 'leads_in_tier' => 50, 'price_rub' => '10.10']; } $tiers[] = ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '10.10']; $resp = $this->postJson('/api/admin/pricing-tiers', ['tiers' => $tiers]); $resp->assertCreated(); $expectedDate = now('Europe/Moscow')->startOfMonth()->addMonth()->toDateString(); foreach (PricingTier::query()->where('effective_from', $expectedDate)->orderBy('tier_no')->get() as $row) { expect((int) $row->price_per_lead_kopecks)->toBe(1010); } }); test('AdminPricingTiers::store отклоняет некорректный price_rub (например "10.123" — три знака после точки)', function () { $tiers = []; for ($i = 1; $i <= 6; $i++) { $tiers[] = ['tier_no' => $i, 'leads_in_tier' => 50, 'price_rub' => '10.10']; } $tiers[] = ['tier_no' => 7, 'leads_in_tier' => null, 'price_rub' => '10.123']; $resp = $this->postJson('/api/admin/pricing-tiers', ['tiers' => $tiers]); $resp->assertStatus(422); $resp->assertJsonValidationErrors(['tiers.6.price_rub']); });