From 8cee34bfbaeb30718609b8b9e67257471d90fb64 Mon Sep 17 00:00:00 2001 From: adonis Date: Sat, 5 Sep 2026 17:01:39 +0330 Subject: [PATCH] feat: landing settings, instant withdraw deduct, and OTP site URL Add has_landing plus landing_* deploy keys, deduct balance on withdraw request, keep app_url for SMS login links, and clarify Zarin rial wallet document titles. Co-authored-by: Cursor --- .../Controllers/Admin/SettingsController.php | 4 +- .../Controllers/Admin/WalletController.php | 7 ++ .../Controllers/Admin/ZarinController.php | 17 ++- app/Http/Controllers/Controller.php | 4 +- .../Controllers/Customer/WalletController.php | 110 +++++++++++++----- app/Services/ConfigService.php | 57 ++++++--- .../SuperAdminProgramSettingsOverlay.php | 6 +- app/Services/ZarinSyncService.php | 26 ++++- database/sql/deploy_settings_const.sql | 101 ++++++++++++++++ .../sql/settings_menu_and_show_to_user.sql | 7 +- tests/Unit/ConfigServiceOtpHostTest.php | 10 ++ .../SuperAdminProgramSettingsOverlayTest.php | 3 +- tests/Unit/ZarinRialWalletDirectionTest.php | 50 ++++++++ 13 files changed, 344 insertions(+), 58 deletions(-) create mode 100644 tests/Unit/ZarinRialWalletDirectionTest.php diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php index 65f598d..59299da 100644 --- a/app/Http/Controllers/Admin/SettingsController.php +++ b/app/Http/Controllers/Admin/SettingsController.php @@ -235,7 +235,7 @@ class SettingsController extends Controller } /** - * تنظیمات پیشرفته: ۱۵ دسترسی از سوپرادمین + بقیهٔ can_edit=0 از DB لوکال. + * تنظیمات پیشرفته: ۱۶ دسترسی از سوپرادمین + بقیهٔ can_edit=0 از DB لوکال. */ private function composeSystemSettings($localRows) { @@ -319,7 +319,7 @@ class SettingsController extends Controller /** * get_settings ادمین (و پایهٔ publicIndex قبل از فیلتر show_to_user): * can_edit=1 (+ runtime + local-owned) از DB لوکال - * فقط ۱۵ دسترسی سوپرادمین از overlay؛ بقیهٔ can_edit=0 از کپی لوکال + * فقط ۱۶ دسترسی سوپرادمین از overlay؛ بقیهٔ can_edit=0 از کپی لوکال * اگر سوپرادمین در دسترس نباشد کل جدول لوکال برمی‌گردد. */ private function composeGetSettings($localRows) diff --git a/app/Http/Controllers/Admin/WalletController.php b/app/Http/Controllers/Admin/WalletController.php index db9cd16..f3519ea 100644 --- a/app/Http/Controllers/Admin/WalletController.php +++ b/app/Http/Controllers/Admin/WalletController.php @@ -275,6 +275,9 @@ class WalletController extends Controller try { $data['direction'] = $deposit ? 0 : 1; $data['withdraw_id'] = $Withdraw->id; + if (!$deposit && trim((string) ($data['note'] ?? '')) === '') { + $data['note'] = 'برداشت وجه'; + } app(\App\Http\Controllers\Admin\ZarinController::class)->wallet($data); } catch (\Exception $e) { report($e); @@ -1446,6 +1449,10 @@ class WalletController extends Controller $withdraw = Withdraw::findOrFail($request->id); $withdraw->update($data); + + if ($withdraw->customer) { + $this->broadcastCustomerWalletAssets($withdraw->customer); + } // $tahesab = new TahesabController(); // $tahesab->wallet([ // 'customer_id' => $withdraw->customer_id, diff --git a/app/Http/Controllers/Admin/ZarinController.php b/app/Http/Controllers/Admin/ZarinController.php index 8db4c9d..ad60cf6 100644 --- a/app/Http/Controllers/Admin/ZarinController.php +++ b/app/Http/Controllers/Admin/ZarinController.php @@ -1861,12 +1861,21 @@ class ZarinController extends Controller : ['korf' => 'k', 'parDar' => 1]; } - /** شرح سند ریالی زرین — همیشه با واریز/برداشت شروع شود تا «خرید فی» دیده نشود */ + /** + * شرح سند ریالی زرین — همیشه «واریز وجه» یا «برداشت وجه». + * دشارژ ادمین و برداشت مشتری هر دو → «برداشت وجه» (پیشوند «واریز اینترنتی به» را زرین خودش می‌گذارد). + */ private function zarinRialWalletSharh(bool $isDeposit, ?string $note): string { - $base = $isDeposit ? 'واریز' : 'برداشت'; + $base = $isDeposit ? 'واریز وجه' : 'برداشت وجه'; $normalized = self::accountingWalletDesc($isDeposit, $note); - if ($normalized === '' || $normalized === $base) { + $generic = [ + '', 'واریز', 'برداشت', 'واریز وجه', 'برداشت وجه', + 'واریز ریالی', 'برداشت ریالی', + 'شارژ', 'دشارژ', 'شارژ حساب', 'کسر', + 'واریز به کیف پول', 'برداشت از کیف پول', 'برداشت از حساب', + ]; + if (in_array($normalized, $generic, true) || in_array(trim((string) $note), $generic, true)) { return $base; } if (str_starts_with($normalized, $base)) { @@ -2029,7 +2038,7 @@ class ZarinController extends Controller $data = array_merge($data, [ 'customer_id' => $existing->customer_id, 'value' => $existing->amount, - 'direction' => str_contains((string) $existing->desc, 'برداشت') ? 1 : 0, + 'direction' => $this->sync()->rialWalletDirectionFromDesc((string) $existing->desc), 'withdraw_id' => $existing->withdraw_id, ]); } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 93ab4dc..98132da 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -227,13 +227,13 @@ class Controller extends BaseController return app(ConfigService::class)->Config($name); } - /** دامنهٔ عمومی سایت — فقط از درخواست HTTP فعلی */ + /** دامنهٔ عمومی سایت — settings.app_url؛ نه هاست API */ protected static function publicHost(?\Illuminate\Http\Request $request = null): string { return app(ConfigService::class)->resolvePublicHost($request); } - /** آدرس کامل وب‌اپ کاربر — مثلاً https://example.ir */ + /** آدرس کامل وب‌اپ کاربر از settings.app_url — مثلاً https://example.ir */ protected static function publicSiteUrl(?\Illuminate\Http\Request $request = null): string { return app(ConfigService::class)->resolvePublicSiteUrl($request); diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index c096195..deafc67 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -17,8 +17,11 @@ use App\Http\Controllers\Admin\TahesabController; use App\Services\ConfigService; use App\Services\AssetSummaryService; use App\Services\WalletBalanceService; +use App\Services\WalletBroadcastService; use App\Services\ZaarboxPriceService; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Rooyesh\Wallet\interfaces\TransactionInterface; @@ -372,43 +375,72 @@ class WalletController extends Controller return response()->json(['message' => 'لطفا کارت بانکی را وارد کنید'], 422); } $bCart = BankAccount::where('id', $data['card_id'])->first(); - if ($bCart->sts != 2) { + if (!$bCart || $bCart->sts != 2) { return response()->json(['message' => 'کارت انتخاب شده نا معتبر است'], 422); } - - $wallet_interface_fund = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type); - if ($data['value'] < 0) { + + $amount = (int) ($data['value'] ?? 0); + if ($amount <= 0) { throw new UnprocessableEntityHttpException('مقدار برداشت باید مثبت باشد'); } - $this->validateWithdrawAmount((int) $data['value']); - $extraNote = trim((string) ($data['extra_note'] ?? '')); if (strlen($extraNote) > 500) { return response()->json(['message' => 'توضیحات اضافی حداکثر ۵۰۰ کاراکتر است'], 422); } - $Withdraw = Withdraw::create([ - 'customer_id' => auth('customer')->id(), - "cash" => $data['value'], - "note" => "برداشت ریالی", - 'extra_note' => $extraNote !== '' ? $extraNote : null, - "bank_account_id" => $data['card_id'], - 'sts' => 1, - 'type' => 2 - ]); - $transaction = new TransactionInterface($wallet_interface_fund, $_inp = [ - "cash" => $data['value'], - 'expire_at' => now(), - 'withdraw_id' => $Withdraw->id, - 'active' => true - ], 4, false, false); + $otp = (string) ($request->otp ?? ''); + if ($otp === '' || (string) ($customer->token_sms ?? '') !== $otp) { + return response()->json(['message' => 'رمز دوم اشتباه میباشد'], 422); + } + + $lock = Cache::lock('customer-rial-withdraw:'.$customer->id, 20); + if (!$lock->get()) { + return response()->json(['message' => 'درخواست برداشت در حال انجام است'], 429); + } + + try { + $customer->refresh(); + if ((string) ($customer->token_sms ?? '') !== $otp) { + return response()->json(['message' => 'رمز دوم اشتباه میباشد'], 422); + } + + $this->validateWithdrawAmount($amount); + + $Withdraw = DB::transaction(function () use ($customer, $data, $amount, $extraNote) { + $withdraw = Withdraw::create([ + 'customer_id' => $customer->id, + 'cash' => $amount, + 'note' => 'برداشت ریالی', + 'extra_note' => $extraNote !== '' ? $extraNote : null, + 'bank_account_id' => $data['card_id'], + 'sts' => 1, + 'type' => 2, + ]); + $this->debitCustomerRialWithdrawOnce($withdraw, $amount, $customer); + $customer->token_sms = null; + $customer->save(); + + return $withdraw; + }); + } finally { + $lock->release(); + } + + try { + app(WalletBroadcastService::class)->broadcastForCustomer( + (int) $customer->id, + (int) $customer->type + ); + } catch (\Throwable $e) { + report($e); + } try { app(\App\Services\PargarSyncService::class)->queueWalletDocument( $customer, 'cash', - -1 * (float) $data['value'], + -1 * (float) $amount, [ 'withdraw_id' => $Withdraw->id, 'note' => 'برداشت ریالی', @@ -421,18 +453,18 @@ class WalletController extends Controller $userMobile = $customer->mobile; // User's mobile number $userName = $customer->name; // User's name - $withdrawAmount = $data['value']; // amount + $withdrawAmount = $amount; // amount if (app(ConfigService::class)->isTahesabAccounting()) { try { $result = app(TahesabController::class)->wallet([ "customer_id" => auth('customer')->id(), - "value" => $data['value'], + "value" => $amount, "direction" => 1, ]); \Log::info('Tahesab withdraw result', [ 'customer_id' => auth('customer')->id(), - 'value' => $data['value'], + 'value' => $amount, 'result' => $result, ]); } catch (\Exception $e) { @@ -444,8 +476,10 @@ class WalletController extends Controller try { app(\App\Http\Controllers\Admin\ZarinController::class)->wallet([ "customer_id" => auth('customer')->id(), - "value" => $data['value'], + "value" => $amount, "direction" => 1, + "withdraw_id" => $Withdraw->id, + "note" => 'برداشت ریالی', ]); } catch (\Exception $e) { report($e); @@ -454,11 +488,6 @@ class WalletController extends Controller $companyName = $this->Config('name_fa'); // Company name from configuration $message = "کاربر $userName با شماره موبایل $userMobile درخواست برداشت مبلغ " . number_format($withdrawAmount) . " ریال را دارد. با تشکر، $companyName"; - if (isset($request->otp)) { - if (Customer::where("id", $customer->id)->first()->token_sms != $request->otp) { - return response()->json(['message' => 'رمز دوم اشتباه میباشد'], 422); - } - } User::where('send_sms', 1)->get()->each(function ($admin) use ($message) { if (self::isSmsNotifEnabled('sms_notif_withdraw_admin')) { $this->Send($admin->mobile, $message . ' ' . now()->format('H:i:s')); @@ -675,6 +704,25 @@ class WalletController extends Controller return response()->json(true); } + private function debitCustomerRialWithdrawOnce(Withdraw $withdraw, int $amount, Customer $customer): void + { + $alreadyDebited = Transaction::query() + ->where('withdraw_id', $withdraw->id) + ->where('cash', '<', 0) + ->exists(); + if ($alreadyDebited) { + return; + } + + $wallet = new WalletInterface((int) $customer->id, (int) $customer->type); + new TransactionInterface($wallet, [ + 'cash' => $amount, + 'expire_at' => now(), + 'withdraw_id' => $withdraw->id, + 'active' => true, + ], 4, false, false); + } + private function validateWithdrawAmount(int $value): void { $wallet_interface_fund = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type); diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index 94a6c2e..364a81b 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -121,8 +121,11 @@ class ConfigService try { $row = Setting::where('key', $key)->first(); - if ($row !== null && $row->is_active && $row->value !== null && $row->value !== '') { - return $row->value; + if ($row !== null && $row->value !== null && $row->value !== '') { + // app_url لینک ورود پیامک است — حتی اگر سوییچ is_active خاموش باشد باید خوانده شود + if ($key === 'app_url' || $row->is_active) { + return $row->value; + } } } catch (\Throwable $e) { // DB در دسترس نیست (deploy / .env اشتباه) — از config/price_providers.php استفاده شود @@ -1357,7 +1360,7 @@ class ConfigService return ''; } - /** Full user-facing site URL, e.g. https://example.ir — prefers settings.app_url */ + /** Full user-facing site URL, e.g. https://example.ir — فقط settings.app_url؛ هاست API جایگزین نمی‌شود */ public function resolvePublicSiteUrl(?\Illuminate\Http\Request $request = null): string { $fromAppUrl = $this->normalizedAppUrl(); @@ -1366,23 +1369,21 @@ class ConfigService } if ($request !== null) { - $origin = rtrim(trim((string) $request->getSchemeAndHttpHost()), '/'); - $host = $this->parseHostFromUrl($origin); - if ($this->isUsableHost($host)) { + $origin = $this->originIfNotApiHost($request); + if ($origin !== '') { return $origin; } } if (! app()->runningInConsole() && request()) { - $origin = rtrim(trim((string) request()->getSchemeAndHttpHost()), '/'); - $host = $this->parseHostFromUrl($origin); - if ($this->isUsableHost($host)) { + $origin = $this->originIfNotApiHost(request()); + if ($origin !== '') { return $origin; } } $host = $this->resolvePublicHost($request); - if ($host !== '') { + if ($host !== '' && ! $this->hostMatchesLaravelAppUrl($host)) { return 'https://' . $host; } @@ -1400,17 +1401,23 @@ class ConfigService { $candidates = []; - $appUrlHost = $this->parseHostFromUrl((string) $this->get('app_url', '')); + $appUrlHost = $this->parseHostFromUrl($this->getString('app_url', '')); if ($appUrlHost !== '') { $candidates[] = $appUrlHost; } if ($request !== null) { - $candidates[] = trim((string) $request->getHost()); + $host = trim((string) $request->getHost()); + if (! $this->hostMatchesLaravelAppUrl($host)) { + $candidates[] = $host; + } } if (! app()->runningInConsole() && request()) { - $candidates[] = trim((string) request()->getHost()); + $host = trim((string) request()->getHost()); + if (! $this->hostMatchesLaravelAppUrl($host)) { + $candidates[] = $host; + } } return $candidates; @@ -1419,7 +1426,7 @@ class ConfigService /** settings.app_url as https://host… without trailing slash; empty if unusable */ private function normalizedAppUrl(): string { - $raw = trim((string) $this->get('app_url', '')); + $raw = $this->getString('app_url', ''); if ($raw === '') { return ''; } @@ -1443,6 +1450,28 @@ class ConfigService return $origin; } + /** هاست Laravel APP_URL همان بک‌اند است — نباید لینک ورود اپ شود */ + private function hostMatchesLaravelAppUrl(string $host): bool + { + $apiHost = $this->parseHostFromUrl((string) config('app.url', '')); + if ($apiHost === '' || ! $this->isUsableHost($apiHost)) { + return false; + } + + return $this->normalizeHost($host) === $this->normalizeHost($apiHost); + } + + private function originIfNotApiHost(\Illuminate\Http\Request $request): string + { + $origin = rtrim(trim((string) $request->getSchemeAndHttpHost()), '/'); + $host = $this->parseHostFromUrl($origin); + if (! $this->isUsableHost($host) || $this->hostMatchesLaravelAppUrl($host)) { + return ''; + } + + return $origin; + } + private function isUsableHost(string $host): bool { $host = strtolower(trim($host)); diff --git a/app/Services/SuperAdminProgramSettingsOverlay.php b/app/Services/SuperAdminProgramSettingsOverlay.php index da2bf13..d3f5080 100644 --- a/app/Services/SuperAdminProgramSettingsOverlay.php +++ b/app/Services/SuperAdminProgramSettingsOverlay.php @@ -11,7 +11,7 @@ use App\Models\Setting; class SuperAdminProgramSettingsOverlay { /** - * فقط این ۱۵ دسترسی از سوپرادمین خوانده می‌شود. + * فقط این ۱۶ دسترسی از سوپرادمین خوانده می‌شود. * هر کلید دیگری (قیمت، ساعت کاری، برندینگ، …) مال پنل خانگی است. */ public const SUPERADMIN_OWNED_KEYS = [ @@ -23,6 +23,7 @@ class SuperAdminProgramSettingsOverlay 'enable_gold_credit', 'enable_product_funds', 'group_sms', + 'has_landing', 'has_page_earn_money', 'has_page_gift_card', 'has_page_notification', @@ -40,6 +41,7 @@ class SuperAdminProgramSettingsOverlay 'enable_conditional_order', 'enable_gold_credit', 'enable_product_funds', + 'has_landing', 'has_page_earn_money', 'has_page_gift_card', 'has_page_notification', @@ -119,7 +121,7 @@ class SuperAdminProgramSettingsOverlay } /** - * آخرین مقادیر ۱۵ دسترسی را در settings لوکال ذخیره می‌کند (can_edit=0). + * آخرین مقادیر ۱۶ دسترسی را در settings لوکال ذخیره می‌کند (can_edit=0). * * @param array> $map */ diff --git a/app/Services/ZarinSyncService.php b/app/Services/ZarinSyncService.php index fbde5aa..7c68dde 100644 --- a/app/Services/ZarinSyncService.php +++ b/app/Services/ZarinSyncService.php @@ -4,6 +4,7 @@ namespace App\Services; use App\Http\Controllers\Admin\ZarinController; use App\Models\Customer; +use App\Models\Withdraw; use App\Models\Zarin; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; @@ -567,6 +568,29 @@ class ZarinSyncService ->all(); } + /** + * درخواست برداشت مشتری cash را مثبت ذخیره می‌کند (type=2) — علامت مبلغ برای جهت سند کافی نیست. + * 0 = واریز، 1 = برداشت + */ + public function rialWalletDirectionFromWithdraw(Withdraw $withdraw): int + { + if ((int) ($withdraw->type ?? 0) === 2) { + return 1; + } + $note = (string) ($withdraw->note ?? ''); + if ($note !== '' && str_contains($note, 'برداشت')) { + return 1; + } + + return ((float) ($withdraw->cash ?? 0)) >= 0 ? 0 : 1; + } + + /** شرح محلی سند ریالی: «برداشت وجه» برداشت است؛ «واریز وجه» واریز. */ + public function rialWalletDirectionFromDesc(string $desc): int + { + return str_contains($desc, 'برداشت') ? 1 : 0; + } + private function backfillWithdraw(ZarinController $controller, int $withdrawId): bool { $withdraw = \App\Models\Withdraw::query()->find($withdrawId); @@ -581,7 +605,7 @@ class ZarinSyncService $result = $controller->wallet([ 'customer_id' => (int) $withdraw->customer_id, 'value' => abs($cash), - 'direction' => $cash >= 0 ? 0 : 1, + 'direction' => $this->rialWalletDirectionFromWithdraw($withdraw), 'withdraw_id' => $withdrawId, 'note' => $withdraw->note, ]); diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index e6abea5..109d15c 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -905,6 +905,66 @@ SELECT 'has_chat', 'true', 'چت پشتیبانی', 1, 1, 'روشن: چت پشتیبانی در اپ مشتری نمایش داده می‌شود (فقط وقتی show_to_users_chat هم روشن باشد). خاموش: دکمه و صفحه چت برای کاربر مخفی است.', 'switch' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'has_chat' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'has_landing', 'false', 'لندینگ وبسایت', 0, 1, + 'روشن: منوی تنظیمات وبسایت در ادمین و تنظیمات لندینگ برای اپ کاربر فعال است. خاموش: منو مخفی است.', 'switch' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'has_landing' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_site', '', 'تنظیمات کلی سایت', 1, 1, + 'محتوای تنظیمات کلی لندینگ وبسایت. در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_site' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_header', '', 'هدر و ناوبری', 1, 1, + 'محتوای هدر و ناوبری لندینگ. در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_header' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_hero', '', 'هیرو', 1, 1, + 'محتوای سکشن هیرو لندینگ. در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_hero' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_live_price', '', 'لیبل‌های قیمت لحظه‌ای', 1, 1, + 'لیبل‌ها و متن قیمت لحظه‌ای لندینگ. در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_live_price' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_calculator', '', 'عنوان ماشین حساب', 1, 1, + 'عنوان و متن ماشین حساب لندینگ. در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_calculator' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_feature', '', 'امکانات (چرا نیازی طلا)', 1, 1, + 'متن امکانات لندینگ (چرا نیازی طلا). در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_feature' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_steps', '', 'مراحل شروع', 1, 1, + 'متن مراحل شروع لندینگ. در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_steps' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_trust', '', 'اعتماد و مجوزها', 1, 1, + 'متن اعتماد و مجوزها (بدون اینماد). در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_trust' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_faq', '', 'سوالات متداول', 1, 1, + 'سوالات متداول لندینگ. در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_faq' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_buttom_cta', '', 'سکشن پایین صفحه لندینگ برای معرفی', 1, 1, + 'متن دعوت به اقدام پایین صفحه لندینگ. در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_buttom_cta' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'landing_footer', '', 'فوتر', 1, 1, + 'محتوای فوتر لندینگ. در اپ کاربر با get_settings در دسترس است.', 'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'landing_footer' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'deposit_sms_support_number', '', 'شماره تماس پشتیبانی', 1, 1, 'شماره تماس پشتیبانی که در صفحه واریز، درباره ما و قوانین به مشتری نشان داده می‌شود. با کلیک، تماس تلفنی برقرار می‌شود.', 'number' @@ -2137,6 +2197,47 @@ UPDATE `settings` SET `description` = 'روشن: چت پشتیبانی در اپ مشتری نمایش داده می‌شود (فقط وقتی show_to_users_chat هم روشن باشد). خاموش: دکمه و صفحه چت برای کاربر مخفی است.' WHERE `key` = 'has_chat'; +UPDATE `settings` SET + `title_fa` = 'لندینگ وبسایت', + `type` = 'switch', + `can_edit` = 0, + `description` = 'روشن: منوی تنظیمات وبسایت در ادمین و تنظیمات لندینگ برای اپ کاربر فعال است. خاموش: منو مخفی است.' +WHERE `key` = 'has_landing'; + +UPDATE `settings` SET `title_fa` = 'تنظیمات کلی سایت', `type` = 'longtext', `can_edit` = 1, + `description` = 'محتوای تنظیمات کلی لندینگ وبسایت. در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_site'; +UPDATE `settings` SET `title_fa` = 'هدر و ناوبری', `type` = 'longtext', `can_edit` = 1, + `description` = 'محتوای هدر و ناوبری لندینگ. در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_header'; +UPDATE `settings` SET `title_fa` = 'هیرو', `type` = 'longtext', `can_edit` = 1, + `description` = 'محتوای سکشن هیرو لندینگ. در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_hero'; +UPDATE `settings` SET `title_fa` = 'لیبل‌های قیمت لحظه‌ای', `type` = 'longtext', `can_edit` = 1, + `description` = 'لیبل‌ها و متن قیمت لحظه‌ای لندینگ. در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_live_price'; +UPDATE `settings` SET `title_fa` = 'عنوان ماشین حساب', `type` = 'longtext', `can_edit` = 1, + `description` = 'عنوان و متن ماشین حساب لندینگ. در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_calculator'; +UPDATE `settings` SET `title_fa` = 'امکانات (چرا نیازی طلا)', `type` = 'longtext', `can_edit` = 1, + `description` = 'متن امکانات لندینگ (چرا نیازی طلا). در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_feature'; +UPDATE `settings` SET `title_fa` = 'مراحل شروع', `type` = 'longtext', `can_edit` = 1, + `description` = 'متن مراحل شروع لندینگ. در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_steps'; +UPDATE `settings` SET `title_fa` = 'اعتماد و مجوزها', `type` = 'longtext', `can_edit` = 1, + `description` = 'متن اعتماد و مجوزها (بدون اینماد). در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_trust'; +UPDATE `settings` SET `title_fa` = 'سوالات متداول', `type` = 'longtext', `can_edit` = 1, + `description` = 'سوالات متداول لندینگ. در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_faq'; +UPDATE `settings` SET `title_fa` = 'سکشن پایین صفحه لندینگ برای معرفی', `type` = 'longtext', `can_edit` = 1, + `description` = 'متن دعوت به اقدام پایین صفحه لندینگ. در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_buttom_cta'; +UPDATE `settings` SET `title_fa` = 'فوتر', `type` = 'longtext', `can_edit` = 1, + `description` = 'محتوای فوتر لندینگ. در اپ کاربر با get_settings در دسترس است.' +WHERE `key` = 'landing_footer'; + -- [super-admin can_edit=0] -- UPDATE `settings` SET -- `title_fa` = 'نمایش چت به کاربران', diff --git a/database/sql/settings_menu_and_show_to_user.sql b/database/sql/settings_menu_and_show_to_user.sql index 0afd4df..913f7ff 100644 --- a/database/sql/settings_menu_and_show_to_user.sql +++ b/database/sql/settings_menu_and_show_to_user.sql @@ -72,8 +72,10 @@ UPDATE `settings` SET `menu` = CASE 'products_site_link_enabled', 'products_site_link_url', 'products_site_link_text', 'has_chat', 'enable_conditional_order', 'enable_product_funds', 'has_page_price_trigger', 'has_page_notification', 'has_page_gift_card', 'enable_customer_level_profit', 'jibit_enabled', 'customer_direct_sms', - 'has_page_earn_money', 'group_sms', 'customer_special_pricing', 'permision__enabled', 'show_to_users_chat' + 'has_page_earn_money', 'group_sms', 'customer_special_pricing', 'permision__enabled', 'show_to_users_chat', + 'has_landing' ) THEN 'customer_features' + WHEN `key` LIKE 'landing_%' THEN 'website_settings' ELSE 'misc' END; @@ -108,6 +110,7 @@ UPDATE `settings` SET `show_to_user` = 1 WHERE `key` IN ( 'fee_deduct_from_gold', 'fee_service_charge_percentage', 'has_chat', + 'has_landing', 'has_page_earn_money', 'has_page_gift_card', 'has_page_notification', @@ -148,3 +151,5 @@ UPDATE `settings` SET `show_to_user` = 1 WHERE `key` IN ( 'wss_path', 'zaarbox_api_url' ); + +UPDATE `settings` SET `show_to_user` = 1 WHERE `key` LIKE 'landing_%'; diff --git a/tests/Unit/ConfigServiceOtpHostTest.php b/tests/Unit/ConfigServiceOtpHostTest.php index 5dfb004..624f306 100644 --- a/tests/Unit/ConfigServiceOtpHostTest.php +++ b/tests/Unit/ConfigServiceOtpHostTest.php @@ -60,6 +60,16 @@ class ConfigServiceOtpHostTest extends TestCase $this->assertSame('https://my.goldarvin.ir', $cfg->resolvePublicSiteUrl($request)); } + public function test_does_not_fall_back_to_laravel_api_host(): void + { + config(['app.url' => 'https://goldarvin.gholaktala.ir/server']); + $request = Request::create('https://goldarvin.gholaktala.ir/server/admin/customer', 'POST'); + $this->app->instance('request', $request); + + $this->assertSame('', $this->cfgWithAppUrl(null)->resolvePublicSiteUrl($request)); + $this->assertSame('', $this->cfgWithAppUrl(null)->resolvePublicHost($request)); + } + public function test_app_url_without_scheme_gets_https(): void { $request = Request::create('https://goldarvin.gholaktala.ir/server/auth/login', 'POST'); diff --git a/tests/Unit/SuperAdminProgramSettingsOverlayTest.php b/tests/Unit/SuperAdminProgramSettingsOverlayTest.php index b7f018c..650fe7d 100644 --- a/tests/Unit/SuperAdminProgramSettingsOverlayTest.php +++ b/tests/Unit/SuperAdminProgramSettingsOverlayTest.php @@ -19,6 +19,7 @@ class SuperAdminProgramSettingsOverlayTest extends TestCase 'enable_gold_credit', 'enable_product_funds', 'group_sms', + 'has_landing', 'has_page_earn_money', 'has_page_gift_card', 'has_page_notification', @@ -29,7 +30,7 @@ class SuperAdminProgramSettingsOverlayTest extends TestCase ]; $this->assertSame($expected, SuperAdminProgramSettingsOverlay::SUPERADMIN_OWNED_KEYS); - $this->assertCount(15, SuperAdminProgramSettingsOverlay::SUPERADMIN_OWNED_KEYS); + $this->assertCount(16, SuperAdminProgramSettingsOverlay::SUPERADMIN_OWNED_KEYS); } public function test_overlay_keeps_only_access_keys(): void diff --git a/tests/Unit/ZarinRialWalletDirectionTest.php b/tests/Unit/ZarinRialWalletDirectionTest.php new file mode 100644 index 0000000..eef5c81 --- /dev/null +++ b/tests/Unit/ZarinRialWalletDirectionTest.php @@ -0,0 +1,50 @@ + 2, + 'cash' => 200000000, + 'note' => 'برداشت ریالی', + ]); + + $this->assertSame(1, app(ZarinSyncService::class)->rialWalletDirectionFromWithdraw($withdraw)); + } + + public function test_admin_deposit_with_positive_cash_is_direction_zero(): void + { + $withdraw = new Withdraw([ + 'type' => null, + 'cash' => 200000000, + 'note' => 'افزایش موجودی', + ]); + + $this->assertSame(0, app(ZarinSyncService::class)->rialWalletDirectionFromWithdraw($withdraw)); + } + + public function test_admin_deduction_with_negative_cash_is_direction_one(): void + { + $withdraw = new Withdraw([ + 'cash' => -50000000, + 'note' => 'کسر', + ]); + + $this->assertSame(1, app(ZarinSyncService::class)->rialWalletDirectionFromWithdraw($withdraw)); + } + + public function test_desc_bardasht_is_withdraw_and_variz_is_deposit(): void + { + $svc = app(ZarinSyncService::class); + $this->assertSame(1, $svc->rialWalletDirectionFromDesc('برداشت وجه')); + $this->assertSame(0, $svc->rialWalletDirectionFromDesc('واریز وجه')); + $this->assertSame(0, $svc->rialWalletDirectionFromDesc('واریز — افزایش موجودی')); + } +}