Files
boursecu_server/app/Http/Controllers/Admin/SettingsController.php
T
mohmdsdgandCursor 027170a33f fix(settings): fee only from fee_service_charge_percentage; deploy:tenant
Remove service_charge_percentage alias and config/fee.php fallback (empty → 0).
Move code-managed Montzar/price runtime keys to can_edit=1 in deploy SQL.
Add settings:deploy and deploy:tenant artisan commands for server deploy.
Advanced admin settings merge runtime keys from local DB; block manual edits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 19:55:29 +03:30

290 lines
9.6 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use App\Services\PriceChannelBroadcastService;
use App\Services\SuperAdminProgramSettingsOverlay;
use Illuminate\Http\Request;
class SettingsController extends Controller
{
private const DEPLOY_ADMIN_MOBILE_SUFFIX = '9128081392';
private const NEW_SETTING_NOTIFY_MOBILE = '09134469985';
/**
* کلیدهایی که پنل ادمین باید بتواند ویرایش کند حتی اگر در DB هنوز can_edit=0 باشد
* (بنرها / سایت‌های ری‌گیری).
*/
private const PANEL_MANAGED_KEYS = [
'tracking_systems',
];
public function index(Request $request)
{
$query = Setting::query()
->select(Setting::adminSelectColumns())
->orderBy('key');
if ($request->boolean('system_only')) {
$local = $query->get();
return response()->json($this->authorizeSettingsPayload(
$this->composeSystemSettings($local)
));
}
if ($request->boolean('editable_only')) {
return response()->json($this->authorizeSettingsPayload(
$query->where(function ($q) {
$q->where('can_edit', 1)->orWhere('can_edit', true)->orWhere('can_edit', '1');
})->get()
));
}
$local = $query->get();
return response()->json($this->authorizeSettingsPayload($this->composeGetSettings($local)));
}
/**
* جدول settings همین نسخه — بدون overlay سوپرادمین.
* سوپرادمین برای ایمپورت از این مسیر can_edit=0 را برمی‌دارد.
*/
public function localTable()
{
$rows = Setting::query()
->select(Setting::adminSelectColumns())
->orderBy('key')
->get();
return response()->json($this->authorizeSettingsPayload($rows));
}
public function edit(Request $request)
{
$setting = Setting::where('key', $request->key)->first();
$isNew = !$setting;
if ($isNew) {
if ($request->boolean('system_deploy')) {
return response()->json([
'message' => 'تنظیمات پیشرفته فقط از سوپرادمین اضافه می‌شوند',
], 403);
}
$setting = new Setting();
$setting->key = $request->key;
$setting->can_edit = 1;
} elseif (
SuperAdminProgramSettingsOverlay::isRuntime((string) $request->key)
&& ! $this->isPanelManagedKey((string) $request->key)
) {
return response()->json([
'message' => 'این مقدار توسط سیستم به‌روز می‌شود و از پنل قابل ویرایش نیست',
], 403);
} elseif (!$setting->can_edit && !$this->isPanelManagedKey((string) $request->key)) {
return response()->json([
'message' => 'این تنظیم از سوپرادمین کنترل می‌شود و در پنل خانگی قابل ویرایش نیست',
], 403);
}
$previousValue = $setting->exists ? (string) ($setting->value ?? '') : null;
$setting->value = $request->value;
$setting->description = $request->description;
if ($request->has('is_active')) {
$setting->is_active = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
} elseif ($isNew) {
$setting->is_active = 1;
}
$setting->title_fa = $request->title_fa ?? '';
if (Setting::hasTypeColumn()) {
$setting->type = $request->type ?? '';
}
$setting->save();
if (
$request->key === 'price_channel_message_text'
&& $previousValue !== (string) ($request->value ?? '')
) {
dispatch(function () {
app(PriceChannelBroadcastService::class)->broadcastAfterTemplateChange();
})->afterResponse();
}
if ($isNew) {
$projectName = Setting::where('key', 'name_fa')->value('value') ?: 'پروژه';
$settingTitle = trim($request->title_fa ?? '') ?: $request->key;
$this->Send(
self::NEW_SETTING_NOTIFY_MOBILE,
"تنظیم جدید: {$settingTitle}\nپروژه: {$projectName}",
false
);
}
return response()->json($setting);
}
private function normalizeAdminMobile(?string $mobile): string
{
$digits = preg_replace('/\D+/', '', (string) $mobile) ?? '';
if (str_starts_with($digits, '98') && strlen($digits) >= 12) {
$digits = substr($digits, 2);
}
if (strlen($digits) === 11 && str_starts_with($digits, '0')) {
$digits = substr($digits, 1);
}
return $digits;
}
private function isDeployAdminUser(): bool
{
$user = auth('user')->user() ?? auth()->user();
if (!$user) {
return false;
}
return $this->normalizeAdminMobile($user->mobile ?? '') === self::DEPLOY_ADMIN_MOBILE_SUFFIX;
}
private function isPanelManagedKey(string $key): bool
{
return in_array($key, self::PANEL_MANAGED_KEYS, true);
}
/**
* تنظیمات پیشرفته: can_edit=0 (سوپرادمین) + کلیدهای can_edit=1 که کد مستقیم می‌نویسد.
*/
private function composeSystemSettings($localRows)
{
$overlay = app(SuperAdminProgramSettingsOverlay::class);
$saMap = $overlay->map();
$byKey = [];
if ($saMap !== null) {
foreach ($saMap as $key => $item) {
if (! is_array($item)) {
continue;
}
$byKey[(string) $key] = $this->overlayRowToSetting($item);
}
} else {
foreach ($localRows as $row) {
if (! $this->isEditableCanEdit($row->can_edit ?? null)) {
$byKey[(string) $row->key] = $row;
}
}
}
foreach ($localRows as $row) {
$key = (string) $row->key;
if (
SuperAdminProgramSettingsOverlay::isLocalOwned($key)
|| SuperAdminProgramSettingsOverlay::isRuntime($key)
) {
$byKey[$key] = $row;
}
}
return collect(array_values($byKey))->sortBy(fn ($row) => (string) ($row->key ?? ''))->values();
}
/**
* get_settings: کلیدهای سوپرادمین (can_edit=0) + ردیف‌های جدول لوکال با can_edit=1.
* اگر سوپرادمین در دسترس نباشد کل جدول لوکال برمی‌گردد.
* کلیدهای runtime / متعلق به پنل خانگی همیشه از جدول لوکال می‌آیند.
*/
private function composeGetSettings($localRows)
{
$overlay = app(SuperAdminProgramSettingsOverlay::class);
$saMap = $overlay->map();
if ($saMap === null) {
return $localRows;
}
$byKey = [];
foreach ($localRows as $row) {
if ($this->isEditableCanEdit($row->can_edit ?? null)) {
$byKey[(string) $row->key] = $row;
}
}
foreach ($saMap as $key => $item) {
if (! is_array($item)) {
continue;
}
$byKey[(string) $key] = $this->overlayRowToSetting($item);
}
foreach ($localRows as $row) {
$key = (string) $row->key;
if (
SuperAdminProgramSettingsOverlay::isLocalOwned($key)
|| SuperAdminProgramSettingsOverlay::isRuntime($key)
) {
$byKey[$key] = $row;
}
}
return collect(array_values($byKey))->sortBy(fn ($row) => (string) ($row->key ?? ''))->values();
}
private function isEditableCanEdit(mixed $canEdit): bool
{
return $canEdit === 1 || $canEdit === true || $canEdit === '1';
}
private function mergeOverlayIntoLocal($rows)
{
return $this->composeGetSettings($rows);
}
/**
* @param array<int, array<string, mixed>> $overlay
*/
private function overlayToRows(array $overlay)
{
return collect($overlay)
->filter(fn ($item) => is_array($item) && trim((string) ($item['key'] ?? '')) !== '')
->map(fn ($item) => $this->overlayRowToSetting($item))
->values();
}
private function overlayRowToSetting(array $item): Setting
{
$setting = new Setting();
$setting->id = isset($item['id']) && is_numeric($item['id']) ? (int) $item['id'] : null;
$setting->key = (string) ($item['key'] ?? '');
$setting->value = $item['value'] ?? '';
$setting->title_fa = $item['title_fa'] ?? '';
$setting->description = $item['description'] ?? '';
$setting->type = $item['type'] ?? '';
$setting->is_active = $item['is_active'] ?? 1;
$setting->can_edit = 0;
return $setting;
}
/**
* اپ کاربر (بدون auth ادمین) هرگز secret/credential نمی‌بیند.
* پنل ادمین با auth:user همه کلیدها را می‌بیند.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Collection|iterable $rows
* @return \Illuminate\Support\Collection
*/
private function authorizeSettingsPayload($rows)
{
if (auth('user')->check()) {
return collect($rows)->values();
}
return Setting::forPublicApi($rows);
}
}