diff --git a/app/Http/Controllers/Admin/TahesabController.php b/app/Http/Controllers/Admin/TahesabController.php index 58db7a4..6eaf281 100644 --- a/app/Http/Controllers/Admin/TahesabController.php +++ b/app/Http/Controllers/Admin/TahesabController.php @@ -807,7 +807,7 @@ class TahesabController extends Controller /** * همگام‌سازی مانده یک مشتری با ته‌حساب. - * body: overwrite=1 | keep=1 | ask_if_conflict=1 (پیش‌فرض ask برای جلوگیری از overwrite ناخواسته) + * body: overwrite=1 | keep=1 | push_to_tahesab=1 | ask_if_conflict=1 */ public function syncCustomerWallet(Request $request, $customerId) { @@ -826,12 +826,26 @@ class TahesabController extends Controller ], 422); } + $walletService = app(TahesabInboundWalletService::class); + + if ($request->boolean('push_to_tahesab')) { + $result = $walletService->pushWalletToTahesab($customer); + $applied = (bool) ($result['applied'] ?? false); + + return response()->json([ + 'ok' => $applied, + 'needs_decision' => false, + 'message' => $result['message'] ?? '', + 'result' => $result, + ], $applied ? 200 : 422); + } + $overwrite = $request->boolean('overwrite'); $keep = $request->boolean('keep'); $alwaysAsk = $request->boolean('always_ask'); $askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep && ! $alwaysAsk); - $result = app(TahesabInboundWalletService::class)->syncWalletFromTahesab( + $result = $walletService->syncWalletFromTahesab( $customer, null, [ diff --git a/app/Http/Controllers/Single/SingleOrderController.php b/app/Http/Controllers/Single/SingleOrderController.php index fa70fc9..93f2860 100644 --- a/app/Http/Controllers/Single/SingleOrderController.php +++ b/app/Http/Controllers/Single/SingleOrderController.php @@ -32,6 +32,7 @@ use App\Services\ReferralGiftService; use App\Services\WalletBalanceService; use App\Services\WalletBroadcastService; use App\Services\ColleagueOrderCallbackService; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; @@ -1010,9 +1011,8 @@ class SingleOrderController extends Controller $this->assertProviderOrderAccepted($data); $this->finalizeProviderOrder($data); $order->refresh(); - if (app(ColleagueOrderCallbackService::class)->waitEnabled()) { - $data['_awaiting_callback'] = true; - } + // همیشه تا کالبک/استعلام همکار در انتظار می‌ماند (حتی اگر سوییچ خاموش باشد) + $data['_awaiting_callback'] = true; } catch (\Throwable $e) { $this->markOrderFailed($order, $e->getMessage()); throw $e instanceof UnprocessableEntityHttpException @@ -2093,6 +2093,44 @@ class SingleOrderController extends Controller public function rejectColleaguePendingOrder(Order $order, string $message): void { $this->markOrderFailed($order, $message); + $this->notifyAdminsColleagueOrderRejected($order, $message); + } + + /** + * SMS ادمین وقتی سفارش در اپ ثبت شده ولی همکار رد کرده است. + */ + private function notifyAdminsColleagueOrderRejected(Order $order, string $rejectMessage): void + { + if (! self::isSmsNotifEnabled('sms_notif_order_admin')) { + return; + } + + try { + $customer = Customer::query()->find((int) $order->customer_id); + $name = trim((string) ($customer->name ?? '')) ?: 'بدون نام'; + $mobile = trim((string) ($customer->mobile ?? '')) ?: '—'; + $dir = ((int) ($order->direction ?? 0) === 2) ? 'فروش' : 'خرید'; + $amount = self::formatSmsGoldWeight((float) ($order->amount ?? 0)); + $company = (string) ($this->Config('name_fa') ?: ''); + $reason = trim($rejectMessage); + if ($reason === '') { + $reason = 'رد شده توسط همکار'; + } + + $message = "سفارش {$dir} کاربر {$name} ({$mobile}) به مقدار {$amount} گرم در اپ ثبت شد ولی از سمت همکار رد شد. علت: {$reason}"; + if ($company !== '') { + $message .= " — {$company}"; + } + + User::where('send_sms', 1)->get()->each(function ($admin) use ($message) { + $this->send($admin->mobile, $message); + }); + } catch (\Throwable $e) { + Log::warning('Colleague reject admin SMS failed', [ + 'order_id' => $order->id, + 'error' => $e->getMessage(), + ]); + } } public function finalizeColleaguePendingOrder(Order $order): void diff --git a/app/Services/ColleagueOrderCallbackService.php b/app/Services/ColleagueOrderCallbackService.php index 9e2013a..3eca446 100644 --- a/app/Services/ColleagueOrderCallbackService.php +++ b/app/Services/ColleagueOrderCallbackService.php @@ -16,9 +16,8 @@ class ColleagueOrderCallbackService public function waitEnabled(): bool { - $config = app(ConfigService::class); - - return $config->usesMontzarPricing() && $config->getBool('montzar_order_wait_callback', true); + // همیشه منتظر کالبک/استعلام همکار — سوییچ montzar_order_wait_callback دیگر مسیر «قطعی بلافاصله» را باز نمی‌کند + return app(ConfigService::class)->usesMontzarPricing(); } public function callbackUrl(int $localOrderId): string diff --git a/app/Services/MontzarService.php b/app/Services/MontzarService.php index 429d2ab..4e451df 100644 --- a/app/Services/MontzarService.php +++ b/app/Services/MontzarService.php @@ -627,8 +627,7 @@ class MontzarService ]; if ( - $this->config()->getBool('montzar_order_wait_callback', true) - && isset($data['order']) + isset($data['order']) && $data['order'] instanceof Order ) { $payload['callback'] = app(ColleagueOrderCallbackService::class)->callbackUrl((int) $data['order']->id); diff --git a/app/Services/TahesabInboundWalletService.php b/app/Services/TahesabInboundWalletService.php index 0a4eefd..c22f1f5 100644 --- a/app/Services/TahesabInboundWalletService.php +++ b/app/Services/TahesabInboundWalletService.php @@ -19,10 +19,11 @@ use Rooyesh\Wallet\models\Wallet; * MandeyeKolBePool → موجودی ریالی * * سیاست تصمیم ادمین: - * overwrite=true → اختلاف را اعمال کن - * keep=true → موجودی اپ را دست نزن - * ask_if_conflict → اگر اپ مانده دارد و با ته‌حساب فرق دارد، بدون نوشتن برگردان - * always_ask → هر اختلافی (حتی کیف خالی) بدون نوشتن برای تأیید ادمین برمی‌گردد + * overwrite=true → اختلاف را روی کیف اپ اعمال کن (ته‌حساب → اپ) + * keep=true → موجودی اپ را دست نزن + * push_to_tahesab → مانده اپ را با سند تعدیل روی ته‌حساب بنویس (حتی اگر اپ صفر باشد) + * ask_if_conflict → اگر اپ مانده دارد و با ته‌حساب فرق دارد، بدون نوشتن برگردان + * always_ask → هر اختلافی (حتی کیف خالی) بدون نوشتن برای تأیید ادمین برمی‌گردد * جمع دو مانده هرگز انجام نمی‌شود تا موجودی دو برابر نشود. */ class TahesabInboundWalletService @@ -190,6 +191,113 @@ class TahesabInboundWalletService ]); } + /** + * ماندهٔ اپ → ته‌حساب (سند تعدیل در ته‌حساب؛ کیف اپ دست نخورده). + * حتی اگر اپ صفر باشد، مانده ته‌حساب با سند برداشت/واریز هم‌تراز اپ می‌شود. + * + * @param array{gold?: float, cash?: float}|null $mande + * @return array{ + * applied: bool, + * gold_delta?: float, + * rial_delta?: float, + * app_gold?: float, + * app_cash?: float, + * tahesab_gold?: float, + * tahesab_cash?: float, + * message: string + * } + */ + public function pushWalletToTahesab(Customer $customer, ?array $mande = null): array + { + $code = (int) ($customer->moshtari_code ?? 0); + if ($code <= 0) { + return ['applied' => false, 'message' => 'کد ته‌حساب ندارد']; + } + + if ($mande === null) { + $all = $this->fetchMandeByCodes([$code]); + $mande = $all[(string) $code] ?? null; + } + + if ($mande === null) { + return ['applied' => false, 'message' => 'مانده از ته‌حساب دریافت نشد']; + } + + $compared = $this->compareWithWallet($customer, $mande); + $comparison = $compared['comparison']; + $goldSame = $compared['gold_same']; + $rialSame = $compared['rial_same']; + + if ($goldSame && $rialSame) { + return array_merge($comparison, [ + 'applied' => false, + 'message' => 'مانده با ته‌حساب یکسان است', + ]); + } + + $controller = app(\App\Http\Controllers\Admin\TahesabController::class); + $note = 'تعدیل مانده از اپ به ته‌حساب'; + $errors = []; + + // gold_delta در مقایسه = ته‌حساب − اپ؛ برای رساندن ته‌حساب به اپ باید معکوس اعمال شود + if (! $goldSame) { + $goldToAdd = (float) $comparison['app_gold'] - (float) $comparison['tahesab_gold']; + $goldOk = $controller->walletGold([ + 'customer_id' => (int) $customer->id, + 'value' => abs($goldToAdd), + 'direction' => $goldToAdd > 0 ? 1 : 0, + 'note' => $note, + ]); + if ($goldOk === false || is_string($goldOk)) { + $errors[] = is_string($goldOk) && $goldOk !== '' + ? $goldOk + : 'ثبت سند طلایی تعدیل در ته‌حساب ناموفق بود'; + } + } + + if (! $rialSame) { + $cashToAdd = (float) $comparison['app_cash'] - (float) $comparison['tahesab_cash']; + $cashOk = $controller->wallet([ + 'customer_id' => (int) $customer->id, + 'value' => (int) round(abs($cashToAdd)), + 'direction' => $cashToAdd > 0 ? 0 : 1, + 'note' => $note, + ]); + if ($cashOk === false || is_string($cashOk)) { + $errors[] = is_string($cashOk) && $cashOk !== '' + ? $cashOk + : 'ثبت سند ریالی تعدیل در ته‌حساب ناموفق بود'; + } + } + + if ($errors !== []) { + Log::warning('Tahesab push wallet from app failed', [ + 'customer_id' => (int) $customer->id, + 'moshtari_code' => $code, + 'errors' => $errors, + ]); + + return array_merge($comparison, [ + 'applied' => false, + 'message' => implode('؛ ', $errors), + ]); + } + + Log::info('Tahesab wallet pushed from app', [ + 'customer_id' => (int) $customer->id, + 'moshtari_code' => $code, + 'app_gold' => $comparison['app_gold'], + 'app_cash' => $comparison['app_cash'], + 'tahesab_gold_before' => $comparison['tahesab_gold'], + 'tahesab_cash_before' => $comparison['tahesab_cash'], + ]); + + return array_merge($comparison, [ + 'applied' => true, + 'message' => 'مانده اپ روی ته‌حساب اعمال شد (کیف اپ تغییر نکرد)', + ]); + } + /** * مقایسه بدون نوشتن — برای تأیید ادمین قبل از ست کردن کد. * diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 109d15c..5b4347e 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -348,7 +348,7 @@ WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'montzar_open_sell' LIM INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'montzar_order_wait_callback', 'true', 'انتظار کالبک سفارش همکار', 1, 1, - 'روشن: سفارش همکار تا دریافت کالبک (یا استعلام مسیر وضعیت) در انتظار می‌ماند و کیف ثبت نمی‌شود. خاموش: مثل قبل بلافاصله با پاسخ HTTP همکار قطعی می‌شود.', + 'با منبع قیمت همکار همیشه تا کالبک/استعلام وضعیت منتظر می‌ماند (خاموش بودن سوییچ مسیر قطعی بلافاصله را باز نمی‌کند). رد همکار → SMS به ادمین.', 'switch' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'montzar_order_wait_callback' LIMIT 1); @@ -1948,6 +1948,9 @@ UPDATE `settings` SET `description` = 'مظنه واقعی سمت خرید از UPDATE `settings` SET `description` = 'مظنه واقعی سمت فروش از همکار. توسط سیستم به‌روز می‌شود.' WHERE `key` = 'montzar_truly_sell_mazaneh'; UPDATE `settings` SET `description` = 'روشن: امکان ثبت سفارش خرید از طریق قیمت همکار باز است (طبق وضعیت سرویس خارجی).' WHERE `key` = 'montzar_open_buy'; UPDATE `settings` SET `description` = 'روشن: امکان ثبت سفارش فروش از طریق قیمت همکار باز است.' WHERE `key` = 'montzar_open_sell'; +UPDATE `settings` SET + `description` = 'با منبع قیمت همکار همیشه تا کالبک/استعلام وضعیت منتظر می‌ماند (خاموش بودن سوییچ مسیر قطعی بلافاصله را باز نمی‌کند). رد همکار → SMS به ادمین‌های دارای ارسال پیامک (sms_notif_order_admin).' +WHERE `key` = 'montzar_order_wait_callback'; UPDATE `settings` SET `description` = 'روشن: سرویس همکار در حال حاضر بسته/غیرفعال گزارش شده است.' WHERE `key` = 'montzar_closed'; UPDATE `settings` SET `description` = 'پاسخ JSON آخرین سفارش ارسالی به همکار. برای عیب‌یابی؛ توسط سیستم پر می‌شود.' WHERE `key` = 'montzar_order_response'; UPDATE `settings` SET `description` = 'لاگ JSON آخرین محصول دریافت‌شده از API همکار. توسط سیستم پر می‌شود.' WHERE `key` = 'montzar_last_product_log';