Files
portal/app/tests/Feature/Admin/AdminPricingTiersControllerTest.php
T
Дмитрий ed5e3f495d feat(admin): Plan 4 Task 9 — AdminPricingTiersController + AdminPricingTiersView (CRUD 7-tier + audit)
Backend AdminPricingTiersController:
- GET /api/admin/pricing-tiers — active + scheduled.
- POST — create 7-tier set с effective_from=DATE_TRUNC('month', NOW()+1 month).
- DELETE /scheduled/{date} — отмена будущей сетки.
- Validation: ровно 7 tier_no 1..7 unique, tier 7 leads_in_tier=null, price>=0.
- Audit trail saas_admin_audit_log на POST + DELETE (через SaasAdminAuditLog
  model: payload_before/after, NOT NULL admin_user_id резолвится через стаб
  system-pricing@liderra.local + ip_address из $request->ip()).
- 8 Pest integration tests.

Frontend AdminPricingTiersView (Vue 3 + Vuetify 3):
- v-data-table активной сетки + scheduled groups + dialog editor.
- Forest-palette + JetBrains Mono для tnum-цифр.
- 5 Vitest unit tests (tests/Frontend/, авто-импорт Vuetify через vite-plugin).
- Histoire story для preview.

Router /admin/pricing-tiers route (layout 'admin', requiresAuth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:18:01 +03:00

109 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\PricingTier;
use Database\Seeders\PricingTierSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->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('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();
});