Files
portal/app/tests/Feature/AdminBillingActionsTest.php
T
Дмитрий 6e1f5355b8 refactor(webhook): Phase 4 — DROP migration + schema v8.35 + test/factory cleanup
Task 4.1 Steps 1–7: legacy direct webhook channel DDL removal.

Migration 2026_05_24_140000_drop_legacy_webhook_artefacts:
- DROP TABLE webhook_log CASCADE (partitioned RANGE по received_at)
- DROP TABLE rejected_deals_log CASCADE
- ALTER TABLE tenants DROP COLUMN webhook_token, webhook_token_rotated_at
- DELETE FROM system_settings WHERE key = 'low_balance_threshold_leads'
NB: webhook_dedup_keys ОСТАВЛЕНА — используется CSV-каналом (HistoricalImportService).

Services fixed (не покрыты Phase 3):
- MonthlyPartitionManager::PARTITIONED_TABLES — убрана строка webhook_log
- PdErasureService::eraseSubject() — убрана секция 4 (SELECT/UPDATE webhook_log)

Factory + tests cleanup (webhook_token column gone):
- TenantFactory: убрано webhook_token из definition()
- 7 test files: убраны вставки webhook_token в DB::table('tenants')->insert(...)
- storage/_demo_split_tenants.php: убрана строка webhook_token

Schema v8.35:
- −2 таблицы (webhook_log partitioned + rejected_deals_log)
- −5 индексов (idx_webhook_log_*, idx_rejected_*, idx_tenants_webhook_token)
- −2 RLS-политики
- db/CHANGELOG_schema.md: запись v8.35

Tests updated:
- SchemaDeltaTest: 66 base tables / 120 indexes / 40 RLS policies
- PartitionsCreateMonthsTest: webhook_log убрана из regex / 48 skipped вместо 54

Smoke: 36/36 passed (RlsSmoke, AdminBilling, AdminPdSubject, PartitionsCreateMonths, SchemaDelta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 18:51:17 +03:00

134 lines
6.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\BalanceTransaction;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
function makeBillingTenant(array $overrides = []): int
{
return (int) DB::table('tenants')->insertGetId(array_merge([
'subdomain' => 'bt-'.bin2hex(random_bytes(4)),
'organization_name' => 'Billing Test Co',
'contact_email' => 'bt-'.bin2hex(random_bytes(3)).'@test.local',
'status' => 'active',
'balance_rub' => '5000.00',
'is_trial' => false,
'created_at' => now(),
], $overrides));
}
function makeTariffPlan(array $overrides = []): int
{
return (int) DB::table('tariff_plans')->insertGetId(array_merge([
'code' => 'test-'.bin2hex(random_bytes(4)),
'name' => 'Test Plan',
'billing_model' => 'monthly',
'price_monthly' => '999.00',
'created_at' => now(),
], $overrides));
}
test('GET tariff-plans возвращает список планов', function () {
$planId = makeTariffPlan(['name' => 'Visible Plan', 'price_monthly' => '1500.00']);
$r = $this->getJson('/api/admin/billing/tariff-plans');
$r->assertOk();
$plans = $r->json('plans');
expect($plans)->toBeArray();
$found = collect($plans)->first(fn ($p) => $p['id'] === $planId);
expect($found)->not->toBeNull();
expect($found['id'])->toBeInt();
expect($found['name'])->toBeString();
expect($found['price_monthly'])->toBeString();
});
test('PATCH status suspended меняет статус + пишет audit-log', function () {
$id = makeBillingTenant(['status' => 'active']);
$r = $this->patchJson("/api/admin/billing/tenants/{$id}/status", [
'status' => 'suspended',
'reason' => 'Просрочка оплаты более 30 дней.',
]);
$r->assertOk();
expect($r->json('status'))->toBe('suspended');
expect(DB::table('tenants')->where('id', $id)->value('status'))->toBe('suspended');
expect(DB::table('saas_admin_audit_log')->where('action', 'tenant.suspend')->where('target_id', $id)->exists())->toBeTrue();
});
test('PATCH status active разблокирует', function () {
$id = makeBillingTenant(['status' => 'suspended']);
$this->patchJson("/api/admin/billing/tenants/{$id}/status", [
'status' => 'active', 'reason' => 'Оплата получена, блокировка снята.',
])->assertOk();
expect(DB::table('tenants')->where('id', $id)->value('status'))->toBe('active');
});
test('PATCH status reason короче 10 символов → 422', function () {
$id = makeBillingTenant();
$this->patchJson("/api/admin/billing/tenants/{$id}/status", ['status' => 'suspended', 'reason' => 'мало'])
->assertStatus(422);
});
test('PATCH status несуществующий тенант → 404', function () {
$this->patchJson('/api/admin/billing/tenants/99999999/status', [
'status' => 'suspended', 'reason' => 'Любое основание длиной более десяти.',
])->assertStatus(404);
});
test('PATCH status soft-deleted тенант → 404', function () {
$id = makeBillingTenant(['deleted_at' => now()]);
$this->patchJson("/api/admin/billing/tenants/{$id}/status", [
'status' => 'suspended', 'reason' => 'Любое основание длиной более десяти.',
])->assertStatus(404);
});
test('POST refund списывает с баланса + создаёт balance_transactions refund', function () {
$id = makeBillingTenant(['balance_rub' => '5000.00']);
$r = $this->postJson("/api/admin/billing/tenants/{$id}/refund", [
'amount_rub' => 1500, 'reason' => 'Возврат по обращению клиента №42.',
]);
$r->assertOk();
expect($r->json('balance_rub'))->toBe('3500.00');
expect(DB::table('tenants')->where('id', $id)->value('balance_rub'))->toBe('3500.00');
$tx = BalanceTransaction::where('tenant_id', $id)->where('type', 'refund')->first();
expect($tx)->not->toBeNull();
expect((string) $tx->amount_rub)->toBe('-1500.00');
expect((string) $tx->balance_rub_after)->toBe('3500.00');
expect(DB::table('saas_admin_audit_log')->where('action', 'tenant.refund')->where('target_id', $id)->exists())->toBeTrue();
});
test('POST refund больше баланса 422, баланс не меняется', function () {
$id = makeBillingTenant(['balance_rub' => '1000.00']);
$this->postJson("/api/admin/billing/tenants/{$id}/refund", [
'amount_rub' => 5000, 'reason' => 'Возврат по обращению клиента №7.',
])->assertStatus(422);
expect(DB::table('tenants')->where('id', $id)->value('balance_rub'))->toBe('1000.00');
expect(BalanceTransaction::where('tenant_id', $id)->count())->toBe(0);
});
test('POST refund неположительная сумма 422', function () {
$id = makeBillingTenant();
$this->postJson("/api/admin/billing/tenants/{$id}/refund", ['amount_rub' => 0, 'reason' => 'Основание длиннее десяти символов.'])
->assertStatus(422);
});
test('PATCH tariff меняет current_tariff_id + audit-log', function () {
$id = makeBillingTenant();
$tariffId = makeTariffPlan(['name' => 'Corp Plan', 'price_monthly' => '2500.00']);
$r = $this->patchJson("/api/admin/billing/tenants/{$id}/tariff", [
'tariff_id' => $tariffId, 'reason' => 'Переход на тариф по договорённости с клиентом.',
]);
$r->assertOk();
expect((int) DB::table('tenants')->where('id', $id)->value('current_tariff_id'))->toBe($tariffId);
expect(DB::table('saas_admin_audit_log')->where('action', 'tenant.change_tariff')->where('target_id', $id)->exists())->toBeTrue();
});
test('PATCH tariff несуществующий tariff_id 422', function () {
$id = makeBillingTenant();
$this->patchJson("/api/admin/billing/tenants/{$id}/tariff", [
'tariff_id' => 88888888, 'reason' => 'Основание длиннее десяти символов.',
])->assertStatus(422);
});