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 <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 17:01:39 +03:30
co-authored by Cursor
parent 1c7ea16300
commit 8cee34bfba
13 changed files with 344 additions and 58 deletions
+43 -14
View File
@@ -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));
@@ -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<string, array<string, mixed>> $map
*/
+25 -1
View File
@@ -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,
]);