feat(admin): G4 backend — billing tenant actions (status/refund/tariff)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Concerns\ResolvesAdminUserId;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BalanceTransaction;
|
||||
use App\Models\SaasAdminAuditLog;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -19,6 +22,168 @@ use Illuminate\Support\Facades\DB;
|
||||
*/
|
||||
class AdminBillingController extends Controller
|
||||
{
|
||||
use ResolvesAdminUserId;
|
||||
|
||||
/** GET /api/admin/billing/tariff-plans — список планов для диалога смены тарифа. */
|
||||
public function tariffPlans(): JsonResponse
|
||||
{
|
||||
$plans = DB::table('tariff_plans')
|
||||
->select(['id', 'name', 'price_monthly'])
|
||||
->orderBy('price_monthly')
|
||||
->get()
|
||||
->map(fn ($p) => [
|
||||
'id' => (int) $p->id,
|
||||
'name' => $p->name,
|
||||
'price_monthly' => (string) $p->price_monthly,
|
||||
]);
|
||||
|
||||
return response()->json(['plans' => $plans]);
|
||||
}
|
||||
|
||||
/** PATCH /api/admin/billing/tenants/{id}/status — приостановить/разблокировать тенанта. */
|
||||
public function updateStatus(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'status' => ['required', 'in:active,suspended'],
|
||||
'reason' => ['required', 'string', 'min:10', 'max:1000'],
|
||||
]);
|
||||
|
||||
$tenant = $this->findActiveTenant($id);
|
||||
$adminUserId = $this->resolveAdminUserId($request, 'system-billing@liderra.local', 'System Billing Bot');
|
||||
|
||||
DB::transaction(function () use ($tenant, $validated, $adminUserId, $request): void {
|
||||
DB::table('tenants')->where('id', $tenant->id)->update([
|
||||
'status' => $validated['status'],
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
SaasAdminAuditLog::create([
|
||||
'admin_user_id' => $adminUserId,
|
||||
'action' => $validated['status'] === 'suspended' ? 'tenant.suspend' : 'tenant.activate',
|
||||
'target_type' => 'tenant',
|
||||
'target_id' => $tenant->id,
|
||||
'target_tenant_id' => $tenant->id,
|
||||
'payload_before' => ['status' => $tenant->status],
|
||||
'payload_after' => ['status' => $validated['status']],
|
||||
'reason' => $validated['reason'],
|
||||
'ip_address' => $request->ip() ?? '127.0.0.1',
|
||||
'user_agent' => $request->userAgent(),
|
||||
]);
|
||||
});
|
||||
|
||||
return response()->json(['id' => $tenant->id, 'status' => $validated['status']]);
|
||||
}
|
||||
|
||||
/** POST /api/admin/billing/tenants/{id}/refund — возврат средств: списание с баланса + ledger-запись. */
|
||||
public function refund(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'amount_rub' => ['required', 'numeric', 'gt:0'],
|
||||
'reason' => ['required', 'string', 'min:10', 'max:1000'],
|
||||
]);
|
||||
|
||||
$tenant = $this->findActiveTenant($id);
|
||||
|
||||
$amount = number_format((float) $validated['amount_rub'], 2, '.', '');
|
||||
$balance = (string) $tenant->balance_rub;
|
||||
if (bccomp($amount, $balance, 2) === 1) {
|
||||
abort(422, 'refund amount exceeds tenant balance');
|
||||
}
|
||||
$newBalance = bcsub($balance, $amount, 2);
|
||||
$adminUserId = $this->resolveAdminUserId($request, 'system-billing@liderra.local', 'System Billing Bot');
|
||||
|
||||
$transactionId = DB::transaction(function () use ($tenant, $amount, $newBalance, $validated, $adminUserId, $request): int {
|
||||
DB::statement('SET LOCAL app.current_tenant_id = '.(int) $tenant->id);
|
||||
|
||||
DB::table('tenants')->where('id', $tenant->id)->update([
|
||||
'balance_rub' => $newBalance,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$tx = BalanceTransaction::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'type' => BalanceTransaction::TYPE_REFUND,
|
||||
'amount_rub' => '-'.$amount,
|
||||
'amount_leads' => 0,
|
||||
'balance_rub_after' => $newBalance,
|
||||
'description' => $validated['reason'],
|
||||
'admin_user_id' => $adminUserId,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
SaasAdminAuditLog::create([
|
||||
'admin_user_id' => $adminUserId,
|
||||
'action' => 'tenant.refund',
|
||||
'target_type' => 'tenant',
|
||||
'target_id' => $tenant->id,
|
||||
'target_tenant_id' => $tenant->id,
|
||||
'payload_before' => ['balance_rub' => (string) $tenant->balance_rub],
|
||||
'payload_after' => ['balance_rub' => $newBalance, 'amount_rub' => $amount, 'transaction_id' => $tx->id],
|
||||
'reason' => $validated['reason'],
|
||||
'ip_address' => $request->ip() ?? '127.0.0.1',
|
||||
'user_agent' => $request->userAgent(),
|
||||
]);
|
||||
|
||||
return (int) $tx->id;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'id' => $tenant->id,
|
||||
'balance_rub' => $newBalance,
|
||||
'transaction_id' => $transactionId,
|
||||
]);
|
||||
}
|
||||
|
||||
/** PATCH /api/admin/billing/tenants/{id}/tariff — сменить тарифный план тенанта. */
|
||||
public function changeTariff(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'tariff_id' => ['required', 'integer', 'exists:tariff_plans,id'],
|
||||
'reason' => ['required', 'string', 'min:10', 'max:1000'],
|
||||
]);
|
||||
|
||||
$tenant = $this->findActiveTenant($id);
|
||||
$tariff = DB::table('tariff_plans')->where('id', $validated['tariff_id'])->first();
|
||||
$adminUserId = $this->resolveAdminUserId($request, 'system-billing@liderra.local', 'System Billing Bot');
|
||||
|
||||
DB::transaction(function () use ($tenant, $tariff, $validated, $adminUserId, $request): void {
|
||||
DB::table('tenants')->where('id', $tenant->id)->update([
|
||||
'current_tariff_id' => $tariff->id,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
SaasAdminAuditLog::create([
|
||||
'admin_user_id' => $adminUserId,
|
||||
'action' => 'tenant.change_tariff',
|
||||
'target_type' => 'tenant',
|
||||
'target_id' => $tenant->id,
|
||||
'target_tenant_id' => $tenant->id,
|
||||
'payload_before' => ['current_tariff_id' => $tenant->current_tariff_id],
|
||||
'payload_after' => ['current_tariff_id' => (int) $tariff->id],
|
||||
'reason' => $validated['reason'],
|
||||
'ip_address' => $request->ip() ?? '127.0.0.1',
|
||||
'user_agent' => $request->userAgent(),
|
||||
]);
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'id' => $tenant->id,
|
||||
'tariff_id' => (int) $tariff->id,
|
||||
'tariff_name' => $tariff->name,
|
||||
]);
|
||||
}
|
||||
|
||||
/** Возвращает не-удалённого тенанта либо abort(404). */
|
||||
private function findActiveTenant(int $id): object
|
||||
{
|
||||
$tenant = DB::table('tenants')->where('id', $id)->whereNull('deleted_at')->first();
|
||||
if ($tenant === null) {
|
||||
abort(404, 'tenant not found');
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
/** GET /api/admin/billing?search= */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user