From 00b23ac404a354f7e8528bbfb9da8023f7e7afc0 Mon Sep 17 00:00:00 2001 From: adonis Date: Sat, 5 Sep 2026 22:30:56 +0330 Subject: [PATCH] feat(tahesab,colleague): push app balance to Tahesab and always wait callback Add admin decision to write app wallet onto Tahesab even when zero, always await colleague callback regardless of the wait switch, and SMS admins on colleague reject. Co-authored-by: Cursor --- .../Controllers/Admin/TahesabController.php | 18 ++- .../Single/SingleOrderController.php | 44 ++++++- .../ColleagueOrderCallbackService.php | 5 +- app/Services/MontzarService.php | 3 +- app/Services/TahesabInboundWalletService.php | 116 +++++++++++++++++- database/sql/deploy_settings_const.sql | 5 +- 6 files changed, 176 insertions(+), 15 deletions(-) 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';