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 rejects effective_from равную сегодня', function (): void { $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, ])->assertStatus(422); }); 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']); });