diff --git a/app/Http/Controllers/Admin/WalletController.php b/app/Http/Controllers/Admin/WalletController.php index 45faf85..c64dce3 100644 --- a/app/Http/Controllers/Admin/WalletController.php +++ b/app/Http/Controllers/Admin/WalletController.php @@ -155,6 +155,16 @@ class WalletController extends Controller } } + if (app(ConfigService::class)->isZarinAccounting()) { + try { + $data['direction'] = $deposit ? 0 : 1; + $data['withdraw_id'] = $Withdraw->id; + app(\App\Http\Controllers\Admin\ZarinController::class)->wallet($data); + } catch (\Exception $e) { + report($e); + } + } + try { app(PargarSyncService::class)->queueWalletDocument( $customer, @@ -243,6 +253,16 @@ class WalletController extends Controller } } + if (app(ConfigService::class)->isZarinAccounting()) { + try { + $data['direction'] = $deposit ? 1 : 0; + $data['withdraw_id'] = $Withdraw->id; + app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data); + } catch (\Exception $e) { + report($e); + } + } + try { app(PargarSyncService::class)->queueWalletDocument( $customer, @@ -408,6 +428,16 @@ class WalletController extends Controller } } + if (app(ConfigService::class)->isZarinAccounting()) { + try { + $data['direction'] = $deposit ? 1 : 0; + $data['withdraw_id'] = $Withdraw->id; + app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data); + } catch (\Exception $e) { + report($e); + } + } + try { // فقط محصول متصل به کیف اصلی → سند طلای پرگار app(PargarSyncService::class)->queueWalletDocument( @@ -651,6 +681,32 @@ class WalletController extends Controller } } + if (app(ConfigService::class)->isZarinAccounting() && isset($records['rows']) && is_array($records['rows'])) { + $withdrawIds = collect($records['rows'])->pluck('id')->filter()->values()->all(); + if ($withdrawIds !== []) { + $failedByWithdraw = \App\Models\Zarin::query() + ->whereIn('withdraw_id', $withdrawIds) + ->where(function ($query) { + $query->whereNull('zarin_id'); + if (\Illuminate\Support\Facades\Schema::hasColumn('zarin', 'sync_status')) { + $query->orWhere('sync_status', \App\Services\ZarinSyncService::STATUS_FAILED); + } + }) + ->orderByDesc('id') + ->get() + ->unique('withdraw_id') + ->keyBy('withdraw_id'); + + foreach ($records['rows'] as &$row) { + $zarin = $failedByWithdraw->get($row['id'] ?? null); + if ($zarin) { + $row['zarin_failed_id'] = $zarin->id; + } + } + unset($row); + } + } + return response()->json($records); } diff --git a/app/Http/Controllers/Admin/ZarinController.php b/app/Http/Controllers/Admin/ZarinController.php new file mode 100644 index 0000000..736d307 --- /dev/null +++ b/app/Http/Controllers/Admin/ZarinController.php @@ -0,0 +1,1331 @@ +lastPrice(request()); + + if (is_array($response)) { + return $response; + } + + if (is_object($response) && method_exists($response, 'getData')) { + return (array) $response->getData(true); + } + + return (array) $response; + } + + private function zarinConfigError() + { + return response()->json([ + 'message' => 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.' + ], 422); + } + + private function zarinAccountingUrl() + { + $api = trim((string) $this->Config('zarin_api')); + + if (empty($api)) { + return null; + } + + return rtrim($api, '/') . '/ZarrinService/accounting'; + } + + private function zarinToken() + { + $token = trim((string) $this->Config('zarin_token')); + + return empty($token) ? null : $token; + } + + private function zarinRequest(array $params) + { + $url = $this->zarinAccountingUrl(); + $token = $this->zarinToken(); + + if (!$url || !$token) { + return null; + } + + $params['token'] = $token; + + return Http::withoutVerifying() + ->timeout(15) + ->get($url, $params); + } + + private function zarinDefaultNoeKar() + { + $value = (int) $this->Config('zarin_noe_kar'); + + return $value > 0 ? $value : 16; + } + + private function zarinDefaultGoroheKar() + { + $value = (int) $this->Config('zarin_gorohe_kar'); + + return $value > 0 ? $value : 52; + } + + /** نام گروه حساب مشتری در زرین — برای جداسازی چند پنل روی یک سرور زرین */ + private function zarinCustomerGroupName(): string + { + $nameG = trim((string) $this->Config('zarin_name_g')); + if ($nameG !== '') { + return $nameG; + } + + return trim((string) $this->Config('name_fa')); + } + + /** + * پیشوند عددی uid — وقتی چند پنل یک توکن زرین دارند باید متفاوت باشد + * تا uid تکراری و پاسخ Cache قاطی نشود (فقط ارقام، طبق داکیومنت زرین). + */ + private function zarinUid(string $kindPrefix, $localId): string + { + $panel = preg_replace('/\D+/', '', (string) $this->Config('zarin_uid_prefix')) ?: ''; + $local = preg_replace('/\D+/', '', (string) $localId) ?: '0'; + + return $panel . $kindPrefix . $local; + } + + private function isListArray(array $array) + { + if (function_exists('array_is_list')) { + return array_is_list($array); + } + + return array_keys($array) === range(0, count($array) - 1); + } + + private function containsText($haystack, $needle) + { + $haystack = (string) $haystack; + $needle = (string) $needle; + + if ($needle === '') { + return false; + } + + return mb_strpos($haystack, $needle) !== false; + } + + private function normalizeZarinName($value) + { + $value = trim((string) $value); + + $value = str_replace(['ي', 'ك', '‌'], ['ی', 'ک', ' '], $value); + $value = preg_replace('/\s+/u', ' ', $value); + + return mb_strtolower($value); + } + + private function zarinRowsFromBody($body) + { + if (!is_array($body)) { + return []; + } + + $data = $body['data'] ?? $body['Data'] ?? []; + + if (!is_array($data)) { + return []; + } + + return $this->isListArray($data) ? $data : [$data]; + } + + private function extractResponseRow(?array $body): array + { + if (!is_array($body)) { + return []; + } + + if (isset($body['data']) && is_array($body['data'])) { + $data = $this->isListArray($body['data']) + ? ($body['data'][0] ?? []) + : $body['data']; + + if (is_array($data)) { + return array_merge($body, $data); + } + } + + return $body; + } + + private function findZarinKarForProduct(Product $product, $goroheKar = null) + { + $goroheKar = $goroheKar ?: $this->zarinDefaultGoroheKar(); + + $productName = $this->normalizeZarinName($product->title); + + for ($page = 1; $page <= 10; $page++) { + $params = [ + 'kind' => 'infokarbarcodi', + 'id' => $goroheKar, + 'vazneat' => 0, + 'pageNum' => $page, + 'detailCount' => 250, + ]; + + $response = $this->zarinRequest($params); + + if (!$response || !$response->successful()) { + return null; + } + + $body = $response->json(); + $rows = $this->zarinRowsFromBody($body); + + if (empty($rows)) { + break; + } + + foreach ($rows as $row) { + if (!is_array($row)) { + continue; + } + + $name = $row['nameKar'] + ?? $row['nam_kar_sakhte'] + ?? $row['nam'] + ?? $row['name'] + ?? $row['namKar'] + ?? $row['title'] + ?? ''; + + $rowName = $this->normalizeZarinName($name); + + $idKar = $this->extractUsableZarinKarId($row); + + if (!$idKar) { + continue; + } + + if ( + $rowName === $productName || + $this->containsText($rowName, $productName) || + $this->containsText($productName, $rowName) + ) { + return [ + 'idKar' => $idKar, + 'row' => $row, + ]; + } + } + + if (count($rows) < 250) { + break; + } + } + + return null; + } + + private function getZarinKarById($idKar) + { + if (empty($idKar) || !is_numeric($idKar)) { + return null; + } + + $response = $this->zarinRequest([ + 'kind' => 'infosakhte', + 'id' => (int) $idKar, + ]); + + if (!$response || !$response->successful()) { + return null; + } + + $body = $response->json(); + $rows = $this->zarinRowsFromBody($body); + + foreach ($rows as $row) { + if (!is_array($row)) { + continue; + } + + $rowId = $row['pkFelez'] + ?? $row['pkfelez'] + ?? $row['PKFelez'] + ?? $row['id'] + ?? $row['idKar'] + ?? $row['idkar'] + ?? $row['zarrinId'] + ?? $row['zarinId'] + ?? null; + if ((string) $rowId === (string) $idKar) { + return $row; + } + } + + return null; + } + + private function extractUsableZarinKarId(array $row) + { + foreach ([ + 'zarrinId', + 'zarinId', + 'idKar', + 'idkar', + 'IDKar', + 'id', + 'code', + 'Code', + ] as $key) { + if (isset($row[$key]) && is_numeric($row[$key])) { + return (int) $row[$key]; + } + } + + return null; + } + + private function createZarinKarForProduct(Product $product) + { + $params = [ + 'kind' => 'addFelezsakhteModel', + 'nameKar' => $product->title, + 'noeKar' => $this->zarinDefaultNoeKar(), + 'gorooheKar' => $this->zarinDefaultGoroheKar(), + ]; + + $response = $this->zarinRequest($params); + + if (!$response) { + return [ + 'ok' => false, + 'message' => 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.', + 'status_code' => 422, + 'sent_params' => $params, + ]; + } + + if (!$response->successful()) { + return [ + 'ok' => false, + 'message' => 'ساخت کد کار در زرین ناموفق بود.', + 'status_code' => 502, + 'status' => $response->status(), + 'body' => $response->body(), + 'json' => $response->json(), + 'sent_params' => $params, + ]; + } + + $body = $response->json(); + + $createdPkFelez = $body['data'][0]['pkFelez'] + ?? $body['data']['pkFelez'] + ?? $body['pkFelez'] + ?? null; + + $foundAfterCreate = $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar()); + + if ($foundAfterCreate && !empty($foundAfterCreate['idKar']) && is_numeric($foundAfterCreate['idKar'])) { + $product->finance_p_name = (string) $foundAfterCreate['idKar']; + $product->save(); + + return [ + 'ok' => true, + 'source' => 'created_and_found_by_name', + 'idKar' => (int) $foundAfterCreate['idKar'], + 'product' => $product->fresh(), + 'zarin_row' => $foundAfterCreate['row'], + 'zarin_response' => $body, + 'created_pkFelez' => $createdPkFelez, + 'sent_params_success' => $params, + ]; + } + + return [ + 'ok' => false, + 'message' => 'زرین کالا را ساخت، اما idKar قابل استفاده برای sabtSanadTedadi از لیست infokarbarcodi پیدا نشد.', + 'status_code' => 422, + 'created_pkFelez' => $createdPkFelez, + 'zarin_response' => $body, + 'sent_params' => $params, + ]; + } + + private function ensureZarinKarForProduct(Product $product, $forceCreate = false) + { + $currentIdKar = $product->finance_p_name; + + if (!$forceCreate && !empty($currentIdKar) && is_numeric($currentIdKar)) { + $existingRow = $this->getZarinKarById($currentIdKar); + + if ($existingRow) { + $usableIdKar = $this->extractUsableZarinKarId($existingRow) ?: (int) $currentIdKar; + + if ((string) $product->finance_p_name !== (string) $usableIdKar) { + $product->finance_p_name = (string) $usableIdKar; + $product->save(); + } + + return [ + 'ok' => true, + 'source' => 'existing_verified', + 'idKar' => (int) $usableIdKar, + 'product' => $product->fresh(), + 'zarin_row' => $existingRow, + ]; + } + } + + if (!$forceCreate) { + $found = $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar()); + + if ($found && !empty($found['idKar']) && is_numeric($found['idKar'])) { + $product->finance_p_name = (string) $found['idKar']; + $product->save(); + + return [ + 'ok' => true, + 'source' => 'found_by_name', + 'idKar' => (int) $found['idKar'], + 'product' => $product->fresh(), + 'zarin_row' => $found['row'], + ]; + } + } + + return $this->createZarinKarForProduct($product); + } + + public function table(Request $request) + { + $query = Zarin::with('customer')->orderByDesc('id'); + \App\DB\Te\TableEngine::create($query, $request) + ->initSearch(['desc', 'type']) + ->select(['*']) + ->export($query); + + return response()->json($query); + } + + public function show($id) + { + $zarin = Zarin::with('customer')->findOrFail($id); + + return response()->json($zarin); + } + + /** همه مشتریان زرین را از API می‌گیرد و برمی‌گرداند */ + private function fetchAllZarinCustomers(): array + { + try { + $response = $this->zarinRequest([ + 'kind' => 'Get100Account', + ]); + + if ($response && $response->successful()) { + $rows = $this->zarinRowsFromBody($response->json()); + if (!empty($rows)) { + return $rows; + } + } + + $response = $this->zarinRequest([ + 'kind' => 'listhesabha', + ]); + + if ($response && $response->successful()) { + return $this->zarinRowsFromBody($response->json()); + } + + return []; + } catch (\Exception $e) { + return []; + } + } + + public function customerSyncStatus(Request $request): \Illuminate\Http\JsonResponse + { + $panelTotal = Customer::whereNull('deleted_at')->count(); + $panelRegistered = Customer::whereNull('deleted_at')->whereNotNull('moshtari_code')->count(); + $panelOnly = $panelTotal - $panelRegistered; + + $panelOnlyQuery = Customer::whereNull('deleted_at')->whereNull('moshtari_code') + ->select('id', 'name', 'mobile', 'created_at'); + + if ($search = $request->input('search')) { + $panelOnlyQuery->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('mobile', 'like', "%{$search}%"); + }); + } + $panelOnlyList = $panelOnlyQuery->orderBy('id')->paginate(20); + + $zarinTotal = null; + $zarinOnlyCount = null; + $zarinOnlyResult = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1]; + $debugSample = []; + + $include = $request->boolean('include_zarin') || $request->boolean('include_tahesab'); + + if ($include) { + $zarinCustomers = $this->fetchAllZarinCustomers(); + $zarinTotal = count($zarinCustomers); + + $panelMobiles = Customer::whereNull('deleted_at') + ->whereNotNull('mobile') + ->pluck('mobile') + ->mapWithKeys(fn ($m) => [ltrim((string) $m, '0') => true]) + ->toArray(); + + $panelCodes = Customer::whereNull('deleted_at') + ->whereNotNull('moshtari_code') + ->pluck('moshtari_code') + ->mapWithKeys(fn ($c) => [(string) $c => true]) + ->toArray(); + + $zarinOnlyList = []; + foreach ($zarinCustomers as $item) { + if (!is_array($item)) { + continue; + } + if (count($debugSample) < 3) { + $debugSample[] = $item; + } + $code = $item['idHesab'] ?? $item['Code'] ?? $item['code'] ?? $item['id'] ?? null; + if ($code !== null && isset($panelCodes[(string) $code])) { + continue; + } + $phone = $this->extractZarinPhone($item); + $tel = ltrim($phone, '0'); + if ($tel && isset($panelMobiles[$tel])) { + continue; + } + $name = $item['name'] ?? $item['Name'] ?? ''; + $family = $item['family'] ?? $item['Family'] ?? ''; + $fullName = trim($name . ' ' . $family); + $zarinOnlyList[] = [ + 'code' => $code, + 'name' => $fullName !== '' ? $fullName : ($item['Name'] ?? ''), + 'tel' => $phone, + 'gid' => $item['GID'] ?? null, + 'group' => $item['GoroupName'] ?? ($item['group'] ?? ''), + ]; + } + $zarinOnlyCount = count($zarinOnlyList); + $zarinOnlyResult = [ + 'data' => array_values($zarinOnlyList), + 'total' => $zarinOnlyCount, + 'current_page' => 1, + 'last_page' => 1, + ]; + } + + $payload = [ + 'panel_total' => $panelTotal, + 'panel_registered' => $panelRegistered, + 'panel_only' => $panelOnly, + 'zarin_total' => $zarinTotal, + 'zarin_only_count' => $zarinOnlyCount, + 'panel_only_list' => $panelOnlyList, + 'zarin_only_list' => $zarinOnlyResult, + ]; + + if ($include && $request->boolean('raw')) { + $payload['zarin_debug_sample'] = $debugSample; + } + + return response()->json($payload); + } + + private function extractZarinPhone(array $item): string + { + $candidates = ['mobile', 'Mobile', 'tel', 'Tel', 'Tell', 'Phone', 'CellPhone', 'Cell', 'MobileNo', 'Telephone', 'Mobile1']; + foreach ($candidates as $key) { + if (isset($item[$key]) && trim((string) $item[$key]) !== '') { + return trim((string) $item[$key]); + } + } + foreach ($item as $k => $v) { + if ($v === null || trim((string) $v) === '') { + continue; + } + $lk = strtolower((string) $k); + if (str_contains($lk, 'mobile') || str_contains($lk, 'tel') + || str_contains($lk, 'phone') || str_contains($lk, 'cell')) { + return trim((string) $v); + } + } + + return ''; + } + + public function syncPanelToZarin(): \Illuminate\Http\JsonResponse + { + $customers = Customer::whereNull('deleted_at') + ->whereNull('moshtari_code') + ->select('id', 'name', 'mobile') + ->get(); + + $success = 0; + $failed = 0; + $errors = []; + + foreach ($customers as $customer) { + $result = $this->sync()->registerCustomer($customer); + if ($result['ok'] ?? false) { + $success++; + } else { + $failed++; + $errors[] = ['name' => $customer->name, 'mobile' => $customer->mobile, 'error' => $result['message'] ?? '']; + } + } + + return response()->json([ + 'ok' => true, + 'success' => $success, + 'failed' => $failed, + 'errors' => $errors, + ]); + } + + public function importFromZarin(Request $request): \Illuminate\Http\JsonResponse + { + $validated = $request->validate([ + 'customers' => 'required|array', + 'customers.*.code' => 'required|integer', + 'customers.*.name' => 'required|string', + 'customers.*.tel' => 'nullable|string', + ]); + + $success = 0; + $failed = 0; + $errors = []; + + foreach ($validated['customers'] as $item) { + $mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? ''; + $fullName = trim($item['name']); + $code = (int) $item['code']; + + if ($mobile === '') { + $failed++; + $errors[] = [ + 'name' => $item['name'], + 'code' => $item['code'], + 'error' => 'شماره موبایل خالی است — وارد نشد', + ]; + continue; + } + + $existing = Customer::whereNull('deleted_at')->where('moshtari_code', $code)->first(); + if ($existing) { + $changed = false; + if (($existing->name === null || $existing->name === '') && $fullName !== '') { + $existing->fname = $fullName; + $existing->lname = (string) ($existing->lname ?? ''); + $existing->name = $fullName; + $changed = true; + } + if ($mobile !== '' && ($existing->mobile === null || $existing->mobile === '')) { + if (Customer::isMobileTaken($mobile, (int) $existing->id)) { + $failed++; + $errors[] = [ + 'name' => $item['name'], + 'code' => $item['code'], + 'error' => 'شماره موبایل تکراری است', + ]; + continue; + } + $existing->mobile = $mobile; + if (empty($existing->password)) { + $existing->password = $mobile; + } + $changed = true; + } + if ($changed) { + $existing->save(); + } + $success++; + continue; + } + + if ($mobile !== '') { + $byMobile = Customer::findActiveByMobile($mobile); + if ($byMobile) { + if (empty($byMobile->moshtari_code)) { + $byMobile->moshtari_code = $code; + if ($fullName !== '') { + $byMobile->fname = $fullName; + $byMobile->lname = ''; + $byMobile->name = $fullName; + } + if (empty($byMobile->password)) { + $byMobile->password = $mobile; + } + $byMobile->save(); + $success++; + continue; + } + + $failed++; + $errors[] = [ + 'name' => $item['name'], + 'code' => $item['code'], + 'error' => 'شماره موبایل تکراری است', + ]; + continue; + } + } + + $characters = '0123456789abcdefghijklmnopqrstuvwxyz'; + do { + $referralId = ''; + for ($i = 0; $i < 4; $i++) { + $referralId .= $characters[random_int(0, strlen($characters) - 1)]; + } + } while (Customer::where('referral_id', $referralId)->exists()); + + try { + $customer = new Customer(); + $customer->fname = $fullName; + $customer->lname = ''; + $customer->name = $fullName; + $customer->mobile = $mobile; + $customer->active = true; + $customer->moshtari_code = $item['code']; + $customer->referral_id = $referralId; + $customer->type = 2; + if ($mobile !== '') { + $customer->password = $mobile; + } + $customer->save(); + $success++; + } catch (\Throwable $e) { + $failed++; + $msg = $e->getMessage(); + if ($e instanceof UnprocessableEntityHttpException) { + $msg = $e->getMessage(); + } elseif ($e instanceof \Illuminate\Database\QueryException && (int) ($e->errorInfo[1] ?? 0) === 1062) { + $msg = 'شماره موبایل تکراری است'; + } + $errors[] = ['name' => $item['name'], 'code' => $item['code'], 'error' => $msg]; + } + } + + return response()->json([ + 'ok' => true, + 'success' => $success, + 'failed' => $failed, + 'errors' => $errors, + ]); + } + + /** + * حذف مشتریانِ خرابِ واردشده از زرین (نام خالی) برای پاک‌سازی import ناقص. + * با dry_run=1 فقط تعداد و نمونه را نشان می‌دهد و چیزی حذف نمی‌کند. + */ + public function rollbackImport(Request $request): \Illuminate\Http\JsonResponse + { + $baseQuery = fn () => Customer::whereNotNull('moshtari_code') + ->where(function ($q) { + $q->whereNull('name')->orWhere('name', ''); + }); + + $count = $baseQuery()->count(); + $sample = $baseQuery() + ->select('id', 'fname', 'name', 'mobile', 'moshtari_code', 'created_at') + ->orderByDesc('id') + ->limit(20) + ->get(); + + if ($request->boolean('dry_run')) { + return response()->json([ + 'dry_run' => true, + 'count' => $count, + 'sample' => $sample, + ]); + } + + $deleted = $baseQuery()->forceDelete(); + + return response()->json([ + 'ok' => true, + 'deleted' => $deleted, + ]); + } + + public function retrySanad($id) + { + $zarin = Zarin::with('customer')->findOrFail($id); + $result = $this->sync()->retry($zarin); + + return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); + } + + public function registerCustomer($customerId) + { + $customer = Customer::findOrFail($customerId); + $result = $this->sync()->registerCustomer($customer); + + return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); + } + + private function resolveProductId(array $data, ?Zarin $existing = null): ?int + { + if (!empty($data['product']['id'])) { + return (int) $data['product']['id']; + } + if (!empty($data['product_id'])) { + return (int) $data['product_id']; + } + + $orderId = $data['order']['id'] ?? ($existing->order_id ?? null); + if ($orderId) { + $order = Order::find($orderId); + if ($order && !empty($order->product_id)) { + return (int) $order->product_id; + } + } + + return null; + } + + public function storeTrade($data, ?Zarin $existing = null) + { + if (empty($data['customer_id']) && $existing && $existing->order_id) { + $order = Order::find($existing->order_id); + if ($order) { + $data = array_merge($data, [ + 'customer_id' => $existing->customer_id, + 'amount' => $existing->amount, + 'direction' => $order->direction, + 'price' => $order->price, + 'total_price' => $order->total_price, + 'pay_at' => $existing->date, + 'product' => [ + 'id' => $order->product_id, + 'metal_id' => $existing->type === 'طلا' ? 1 : 2, + ], + 'order' => ['id' => $existing->order_id], + ]); + } + } + + if (!isset($data['customer_id']) || !isset($data['amount'])) { + return request()->expectsJson() + ? response()->json(['error' => 'Data missing: customer_id or amount'], 422) + : false; + } + + $customer = Customer::where('id', $data['customer_id'])->first(); + + if ($customer && ($customer->moshtari_code === null)) { + $this->createCustomerInZarin($customer->id); + $customer = Customer::where('id', $data['customer_id'])->first(); + } + + if (!$customer || empty($customer->moshtari_code)) { + return false; + } + + $amount = $data['amount']; + $orderId = $data['order']['id'] ?? ($existing->order_id ?? null); + $metalId = (int) ($data['product']['metal_id'] ?? 1); + + $data['handler'] = 'storeTrade'; + if ($existing && empty($existing->data)) { + $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); + } + $zarin = $existing ?? Zarin::create([ + 'customer_id' => $customer->id, + 'desc' => $data['direction'] == 1 ? 'خرید آبشده' : 'فروش آبشده', + 'amount' => $amount, + 'order_id' => $orderId, + 'date' => $data['pay_at'] ?? Carbon::now(), + 'type' => $metalId == 1 ? 'طلا' : 'نقره', + 'created_at' => Carbon::now(), + 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), + ]); + + $isStore = app(ConfigService::class)->isStore(); + $productId = $this->resolveProductId($data, $existing); + + try { + if ($isStore && $productId) { + $product = Product::find($productId); + if (!$product) { + $this->sync()->markFailed($zarin, null, 'محصول برای ثبت سند زرین پیدا نشد'); + return false; + } + + $karResult = $this->ensureZarinKarForProduct($product); + if (empty($karResult['ok'])) { + $this->sync()->markFailed($zarin, $karResult, 'کد کار زرین برای محصول آماده نشد'); + return false; + } + + $params = [ + 'kind' => 'sabtSanadTedadi', + 'parDar' => ((int) ($data['direction'] ?? 0) === 1) ? -1 : 1, + 'idHesab' => $customer->moshtari_code, + 'idKar' => (int) $karResult['idKar'], + 'fi' => $data['price'] ?? 0, + 'tedad' => $amount, + 'sharh' => $data['direction'] == 1 ? 'خرید' : 'فروش', + 'uid' => $this->zarinUid('30', $orderId), + ]; + } else { + $params = [ + 'kind' => 'pooli', + 'radif' => 1, + 'korf' => ((int) ($data['direction'] ?? 0) === 1) ? 'f' : 'k', + 'poolorvazn' => 'vazn', + 'meghdar' => $amount, + 'fi' => $data['price'] ?? 0, + 'hesab' => $customer->moshtari_code, + 'sharh' => $data['direction'] == 1 ? 'خرید آبشده' : 'فروش آبشده', + 'ayar' => '750', + 'movaghat' => '', + 'idFactor' => '', + 'uid' => $this->zarinUid('10', $orderId), + ]; + } + + $http = $this->zarinRequest($params); + if (!$http) { + $this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); + return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; + } + + $response = $http->json(); + if (!is_array($response)) { + $response = json_decode($http->body(), true); + } + } catch (\Exception $e) { + $this->sync()->markFailed($zarin, $e->getMessage()); + return $e->getMessage(); + } + + $flat = $this->extractResponseRow(is_array($response) ? $response : null); + + if ($this->sync()->responseOk(is_array($response) ? $response : []) || $this->sync()->responseOk($flat)) { + $this->sync()->markSuccess($zarin, is_array($response) ? $response : $flat, $data); + return $data; + } + + $this->sync()->markFailed($zarin, $response, 'ثبت سند معامله در زرین ناموفق بود'); + return false; + } + + // direction: 0 = daryaft az moshtari, 1 = pardakht be moshtari + public function wallet($data, ?Zarin $existing = null) + { + if (empty($data['customer_id']) && $existing) { + $data = array_merge($data, [ + 'customer_id' => $existing->customer_id, + 'value' => $existing->amount, + 'direction' => str_contains((string) $existing->desc, 'برداشت') ? 1 : 0, + 'withdraw_id' => $existing->withdraw_id, + ]); + } + + if (!isset($data['customer_id']) || !isset($data['value'])) { + return request()->expectsJson() + ? response()->json(['error' => 'Data missing: customer_id or value'], 422) + : false; + } + + $customer = Customer::where('id', $data['customer_id'])->first(); + + if ($customer && ($customer->moshtari_code === null)) { + $this->createCustomerInZarin($customer->id); + $customer = Customer::where('id', $data['customer_id'])->first(); + } + + if (!$customer || empty($customer->moshtari_code)) { + return false; + } + + $data['handler'] = 'wallet'; + $zarin = $existing ?? Zarin::create([ + 'customer_id' => $customer->id, + 'desc' => $data['direction'] == 0 ? 'شارژ حساب' : 'برداشت از حساب', + 'amount' => $data['value'], + 'type' => 'ريال', + 'date' => Carbon::now(), + 'created_at' => Carbon::now(), + 'withdraw_id' => $data['withdraw_id'] ?? null, + 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), + ]); + + if ($existing && empty($existing->data)) { + $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); + } + + $prices = $this->lastPriceData(); + $isWithdraw = (int) ($data['direction'] ?? 0) === 1; + $mazaneh = $isWithdraw ? (int) ($prices['sell'] ?? 0) : (int) ($prices['buy'] ?? 0); + if ($mazaneh <= 0) { + $mazaneh = 1; + } + + $uidSource = $data['withdraw_id'] ?? $zarin->id; + + try { + $http = $this->zarinRequest([ + 'kind' => 'pooli', + 'radif' => 1, + 'korf' => $isWithdraw ? 'f' : 'k', + 'poolorvazn' => 'pool', + 'meghdar' => $data['value'], + 'fi' => $mazaneh, + 'hesab' => $customer->moshtari_code, + 'sharh' => $data['direction'] == 0 ? 'شارژ حساب' : 'برداشت از حساب', + 'ayar' => '750', + 'movaghat' => '', + 'idFactor' => '', + 'uid' => $this->zarinUid('20', $uidSource), + ]); + + if (!$http) { + $this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); + return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; + } + + $response = $http->json(); + + Log::info('Zarin wallet response', [ + 'data' => $data, + 'response' => $response ?? null, + ]); + } catch (\Exception $e) { + $this->sync()->markFailed($zarin, $e->getMessage()); + return $e->getMessage(); + } + + if ($this->sync()->responseOk(is_array($response) ? $response : [])) { + $this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data); + return $data; + } + + $this->sync()->markFailed($zarin, $response, 'ثبت سند ریالی در زرین ناموفق بود'); + return false; + } + + /** + * پاداش معرفی — همیشه روی حساب معرف (نه کاربر تازه‌ثبت‌نام‌شده). + */ + public function referrerGoldReward(Customer $invitedCustomer, float $gold, string $desc = 'پاداش بابت معرفی') + { + $referrer = $invitedCustomer->referrer(); + if (!$referrer) { + return false; + } + + return $this->goldReward([ + 'customer_id' => $referrer->id, + 'value' => $gold, + 'desc' => $desc, + ]); + } + + /** + * پاداش/هدیه طلایی — سند وزنی با korf=f (هدیه به مشتری). + */ + public function goldReward($data, ?Zarin $existing = null) + { + if (empty($data['customer_id']) && $existing) { + $data = array_merge($data, [ + 'customer_id' => $existing->customer_id, + 'value' => $existing->amount, + 'desc' => $existing->desc, + ]); + } + + if (!isset($data['customer_id'], $data['value'])) { + return request()->expectsJson() + ? response()->json(['error' => 'Data missing: customer_id or value'], 422) + : false; + } + + $customer = Customer::where('id', $data['customer_id'])->first(); + if (!$customer) { + return false; + } + + if ($customer->moshtari_code === null) { + $this->createCustomerInZarin($customer->id); + $customer = Customer::where('id', $data['customer_id'])->first(); + } + + if (!$customer || empty($customer->moshtari_code)) { + return false; + } + + $amount = abs((float) $data['value']); + $desc = !empty($data['desc']) ? (string) $data['desc'] : 'پاداش'; + + $data['handler'] = 'goldReward'; + $zarin = $existing ?? Zarin::create([ + 'customer_id' => $customer->id, + 'desc' => $desc, + 'amount' => $amount, + 'date' => Carbon::now(), + 'type' => 'طلا', + 'created_at' => Carbon::now(), + 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), + ]); + + if ($existing && empty($existing->data)) { + $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); + } + + $prices = $this->lastPriceData(); + $mazaneh = (int) ($prices['buy'] ?? 0); + if ($mazaneh <= 0) { + $mazaneh = 1; + } + + try { + $http = $this->zarinRequest([ + 'kind' => 'pooli', + 'radif' => 1, + 'korf' => 'f', + 'poolorvazn' => 'vazn', + 'meghdar' => $amount, + 'fi' => $mazaneh, + 'hesab' => $customer->moshtari_code, + 'sharh' => $desc, + 'ayar' => '750', + 'movaghat' => '', + 'idFactor' => '', + 'uid' => $this->zarinUid('50', $zarin->id), + ]); + + if (!$http) { + $this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); + return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; + } + + $response = $http->json(); + } catch (\Exception $e) { + $this->sync()->markFailed($zarin, $e->getMessage()); + return $e->getMessage(); + } + + if ($this->sync()->responseOk(is_array($response) ? $response : [])) { + $this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data); + return $data; + } + + $this->sync()->markFailed($zarin, $response, 'ثبت پاداش طلایی در زرین ناموفق بود'); + return false; + } + + // direction: 1 = واریز طلا (خرید)، 0 = برداشت طلا (فروش) + public function walletGold($data, ?Zarin $existing = null) + { + if (empty($data['customer_id']) && $existing) { + $data = array_merge($data, [ + 'customer_id' => $existing->customer_id, + 'value' => $existing->amount, + 'direction' => str_contains((string) $existing->desc, 'برداشت') ? 0 : 1, + 'withdraw_id' => $existing->withdraw_id, + ]); + } + + if (!isset($data['customer_id'], $data['value'])) { + return request()->expectsJson() + ? response()->json(['error' => 'Data missing: customer_id or value'], 422) + : false; + } + + $customer = Customer::where('id', $data['customer_id'])->first(); + + if ($customer && ($customer->moshtari_code === null)) { + $this->createCustomerInZarin($customer->id); + $customer = Customer::where('id', $data['customer_id'])->first(); + } + + if (!$customer || empty($customer->moshtari_code)) { + return false; + } + + $amount = abs($data['value']); + $isDeposit = (int) ($data['direction'] ?? 1) === 1; + + $data['handler'] = 'walletGold'; + $zarin = $existing ?? Zarin::create([ + 'customer_id' => $customer->id, + 'desc' => $isDeposit ? 'شارژ کیف پول آبشده' : 'برداشت از کیف پول آبشده', + 'amount' => $amount, + 'date' => Carbon::now(), + 'type' => 'طلا', + 'created_at' => Carbon::now(), + 'withdraw_id' => $data['withdraw_id'] ?? null, + 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), + ]); + + if ($existing && empty($existing->data)) { + $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); + } + + $prices = $this->lastPriceData(); + $mazaneh = $isDeposit ? (int) ($prices['buy'] ?? 0) : (int) ($prices['sell'] ?? 0); + if ($mazaneh <= 0) { + $mazaneh = 1; + } + + $sharh = !empty($data['note']) + ? $data['note'] + : (!empty($data['desc']) ? $data['desc'] : ($isDeposit ? 'خرید آبشده' : 'فروش آبشده')); + $data['note'] = $sharh; + + $uidSource = $data['withdraw_id'] ?? $zarin->id; + + try { + $http = $this->zarinRequest([ + 'kind' => 'pooli', + 'radif' => 1, + 'korf' => $isDeposit ? 'f' : 'k', + 'poolorvazn' => 'vazn', + 'meghdar' => $amount, + 'fi' => $mazaneh, + 'hesab' => $customer->moshtari_code, + 'sharh' => $sharh, + 'ayar' => '750', + 'movaghat' => '', + 'idFactor' => '', + 'uid' => $this->zarinUid('21', $uidSource), + ]); + + if (!$http) { + $this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); + return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; + } + + $response = $http->json(); + } catch (\Exception $e) { + $this->sync()->markFailed($zarin, $e->getMessage()); + return $e->getMessage(); + } + + if ($this->sync()->responseOk(is_array($response) ? $response : [])) { + $this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data); + return $data; + } + + $this->sync()->markFailed($zarin, $response, 'ثبت سند طلایی کیف پول در زرین ناموفق بود'); + return false; + } + + public function createCustomerInZarin($customer_id) + { + $customer = Customer::findOrFail($customer_id); + + if ($customer->moshtari_code != null) { + return $customer->moshtari_code; + } + + $lock = Cache::lock('zarin:register-customer:' . $customer->id, 30); + + if (!$lock->get()) { + $customer->refresh(); + + if ($customer->moshtari_code != null) { + return $customer->moshtari_code; + } + + return 'ثبت مشتری در زرین در حال انجام است'; + } + + try { + $customer->refresh(); + + if ($customer->moshtari_code != null) { + return $customer->moshtari_code; + } + + $name = $customer->fname ?: ($customer->name ?? ('مشتری ' . $customer->id)); + $family = $customer->lname ?: ''; + $mobile = $customer->mobile ?? ''; + + $params = [ + 'kind' => 'addhesab', + 'name' => $name, + 'family' => $family, + 'tel' => $mobile, + 'mobile' => $mobile, + 'address' => trim(($customer->province ?? '') . ' ' . ($customer->city ?? '')) ?: 'ثبت نشده', + 'codePosti' => '0000000000', + 'nameG' => $this->zarinCustomerGroupName(), + 'gender' => in_array($customer->gender, ['اقای', 'آقای'], true) ? 'm' : ($customer->gender == 'خانم' ? 'f' : ''), + 'uid' => $this->zarinUid('40', $customer->id), + ]; + + try { + $http = $this->zarinRequest($params); + + if (!$http) { + return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; + } + + $response = $http->json(); + $data = is_array($response) ? $this->extractResponseRow($response) : []; + + $idHesab = $data['idHesab'] ?? null; + + if (!empty($idHesab)) { + $customer->moshtari_code = $idHesab; + $customer->save(); + return $idHesab; + } + } catch (\Exception $e) { + return $e->getMessage(); + } + + return 'zarin api error!!!'; + } finally { + $lock->release(); + } + } + + public function healthCheck() + { + $http = $this->zarinRequest([ + 'kind' => 'version', + ]); + + if (!$http) { + return $this->zarinConfigError(); + } + + return $http->json() ?? $http->body(); + } +} diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index f3be80d..520d0dc 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -277,6 +277,7 @@ class AuthController extends Controller // claim بدون کد حسابداری — صف export مثل create app(\App\Services\PargarSyncService::class)->registerOnCreate($customer); app(\App\Services\TahesabSyncService::class)->registerOnCreate($customer); + app(\App\Services\ZarinSyncService::class)->registerOnCreate($customer); } return response()->json($customer); diff --git a/app/Http/Controllers/Customer/CartController.php b/app/Http/Controllers/Customer/CartController.php index 150a4ab..4f88b0f 100644 --- a/app/Http/Controllers/Customer/CartController.php +++ b/app/Http/Controllers/Customer/CartController.php @@ -384,6 +384,26 @@ class CartController extends Controller } } + if ($config->isZarinAccounting()) { + try { + app(\App\Http\Controllers\Admin\ZarinController::class)->storeTrade([ + 'customer_id' => $order->customer_id, + 'amount' => $order->amount, + 'direction' => (int) ($order->direction ?? 1), + 'price' => $order->price, + 'total_price' => $order->total_price, + 'pay_at' => $order->pay_at ?? $order->created_at, + 'product' => [ + 'metal_id' => $metalId, + 'id' => $primaryProduct?->id, + ], + 'order' => ['id' => $order->id], + ]); + } catch (\Throwable $e) { + report($e); + } + } + if ($config->isPargarAccounting()) { try { app(PargarSyncService::class)->recordTrade($order, true, [ diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index 3f70288..a585c8c 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -411,6 +411,19 @@ class WalletController extends Controller report($e); } } + + if (app(ConfigService::class)->isZarinAccounting()) { + try { + app(\App\Http\Controllers\Admin\ZarinController::class)->wallet([ + "customer_id" => auth('customer')->id(), + "value" => $data['value'], + "direction" => 1, + ]); + } catch (\Exception $e) { + report($e); + } + } + $companyName = $this->Config('name_fa'); // Company name from configuration $message = "کاربر $userName با شماره موبایل $userMobile درخواست برداشت مبلغ " . number_format($withdrawAmount) . " ریال را دارد. با تشکر، $companyName"; if (isset($request->otp)) { diff --git a/app/Http/Controllers/Single/SingleOrderController.php b/app/Http/Controllers/Single/SingleOrderController.php index 670d539..733d72e 100644 --- a/app/Http/Controllers/Single/SingleOrderController.php +++ b/app/Http/Controllers/Single/SingleOrderController.php @@ -218,6 +218,14 @@ class SingleOrderController extends Controller } } + if ($config->isZarinAccounting()) { + try { + $data['zarin'] = app(\App\Http\Controllers\Admin\ZarinController::class)->storeTrade($data); + } catch (\Exception $e) { + report($e); + } + } + if ($config->isPargarAccounting()) { try { $paymentProductId = (int) ($data['payment_product_id'] ?? 0); diff --git a/app/Models/Product.php b/app/Models/Product.php index b82d090..f209c7c 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -360,6 +360,7 @@ class Product extends Model 'amount', // The amount of the record. 'unit', // The unit of the item. 'group_type', // The unit of the item. + 'finance_p_name', // کد کار زرین (idKar) 'file_id', // The ID of the associated file. 'can_canceled', // Whether the record can be canceled (boolean). 'active', // The active status of the record (boolean). diff --git a/app/Models/Zarin.php b/app/Models/Zarin.php new file mode 100644 index 0000000..f82cfab --- /dev/null +++ b/app/Models/Zarin.php @@ -0,0 +1,48 @@ + 'باز', + 2 => 'موفق', + 3 => 'ناموفق' + ]; + + static $DIRECTION = [ + 1 => 'خرید', + 2 => 'فروش' + ]; + + public function customer() + { + return $this->belongsTo(Customer::class, 'customer_id', 'id'); + } +} diff --git a/app/Observers/CustomerObserver.php b/app/Observers/CustomerObserver.php index 140f11b..189ebfd 100644 --- a/app/Observers/CustomerObserver.php +++ b/app/Observers/CustomerObserver.php @@ -3,15 +3,16 @@ namespace App\Observers; use App\Models\Customer; -use App\Services\ConfigService; use App\Services\PargarSyncService; use App\Services\TahesabSyncService; +use App\Services\ZarinSyncService; class CustomerObserver { public function created(Customer $customer): void { app(TahesabSyncService::class)->registerOnCreate($customer); + app(ZarinSyncService::class)->registerOnCreate($customer); app(PargarSyncService::class)->registerOnCreate($customer); } } diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index 3110aa0..ecd1196 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -728,8 +728,8 @@ class ConfigService public const ACCOUNTING_NONE = 'none'; public const ACCOUNTING_TAHESAB = 'tahesab'; public const ACCOUNTING_PARGAR = 'pargar'; + public const ACCOUNTING_ZARIN = 'zarin'; - /** none | tahesab | pargar — از setting accounting_provider */ public function isConditionalOrderEnabled(): bool { return $this->getBool('enable_conditional_order', false); @@ -762,10 +762,11 @@ class ConfigService return max(0, $this->getInt('fee_conditional_order', 0)); } + /** none | tahesab | pargar | zarin — از setting accounting_provider */ public function accountingProvider(): string { $provider = strtolower(trim($this->getString('accounting_provider', ''))); - if (in_array($provider, [self::ACCOUNTING_NONE, self::ACCOUNTING_TAHESAB, self::ACCOUNTING_PARGAR], true)) { + if (in_array($provider, [self::ACCOUNTING_NONE, self::ACCOUNTING_TAHESAB, self::ACCOUNTING_PARGAR, self::ACCOUNTING_ZARIN], true)) { return $provider; } @@ -789,6 +790,11 @@ class ConfigService return $this->accountingProvider() === self::ACCOUNTING_PARGAR; } + public function isZarinAccounting(): bool + { + return $this->accountingProvider() === self::ACCOUNTING_ZARIN; + } + /** * Hostname for WebOTP SMS (@domain #code) and other public links. * Only from the current HTTP request — no settings, no .env. diff --git a/app/Services/ReferralGiftService.php b/app/Services/ReferralGiftService.php index 2edd109..f73d007 100644 --- a/app/Services/ReferralGiftService.php +++ b/app/Services/ReferralGiftService.php @@ -180,6 +180,18 @@ class ReferralGiftService } } + if ($config->isZarinAccounting()) { + try { + app(\App\Http\Controllers\Admin\ZarinController::class)->referrerGoldReward( + $customer, + $giftForInviter, + 'پاداش بابت معرفی' + ); + } catch (\Exception $e) { + report($e); + } + } + $currentGold = Controller::formatSmsGoldWeight((float) $walletInterface->wallet->refresh()->gold); $message = "سلام {$referrer->name} عزیز،\n\n{$giftForInviter} گرم طلا بابت معرفی {$customer->name} به حساب شما واریز شد.\nموجودی فعلی: {$currentGold} گرم.\n\nاطلاعات بیشتر: {$companyUrl}\n\nبا احترام،\n{$companyName}"; Controller::Send($referrer->mobile, $message); @@ -238,6 +250,18 @@ class ReferralGiftService } } + if ($config->isZarinAccounting()) { + try { + app(\App\Http\Controllers\Admin\ZarinController::class)->goldReward([ + 'customer_id' => $customer->id, + 'value' => $giftForInvited, + 'desc' => 'پاداش بابت ثبت نام', + ]); + } catch (\Exception $e) { + report($e); + } + } + $currentGold = Controller::formatSmsGoldWeight((float) $walletInterface->wallet->refresh()->gold); $message = "سلام {$customer->name} عزیز،\n\n{$giftForInvited} گرم طلا بابت ثبت نام به حساب شما واریز شد.\nموجودی فعلی: {$currentGold} گرم.\n\nاطلاعات بیشتر: {$companyUrl}\n\nبا احترام،\n{$companyName}"; Controller::Send($customer->mobile, $message); diff --git a/app/Services/ZarinSyncService.php b/app/Services/ZarinSyncService.php new file mode 100644 index 0000000..e112c0c --- /dev/null +++ b/app/Services/ZarinSyncService.php @@ -0,0 +1,268 @@ +hasNumericIdKey($response)) { + return true; + } + + $data = $response['data'] ?? null; + if (! is_array($data)) { + return false; + } + + $row = array_is_list($data) ? ($data[0] ?? null) : $data; + + return is_array($row) && $this->hasNumericIdKey($row); + } + + private function hasNumericIdKey(array $row): bool + { + foreach (['idFactor', 'idSanad', 'pkRadifSanad', 'idHesab', 'OK'] as $key) { + if (! array_key_exists($key, $row)) { + continue; + } + $value = $row[$key]; + if ($value === null || $value === '' || $value === false) { + continue; + } + if (is_numeric($value) || (is_string($value) && trim($value) !== '')) { + return true; + } + } + + return false; + } + + public function buildLog(mixed $response = null, ?string $extra = null): string + { + $parts = array_filter([ + $extra, + is_array($response) ? json_encode($response, JSON_UNESCAPED_UNICODE) : (is_string($response) ? $response : null), + ]); + + return implode("\n", $parts); + } + + public function markSuccess(Zarin $row, array $response, ?array $payload = null): void + { + $flat = $this->flattenResponse($response); + + $zarinId = $flat['OK'] + ?? $flat['idFactor'] + ?? $flat['idSanad'] + ?? $flat['pkRadifSanad'] + ?? $flat['idHesab'] + ?? null; + + $shFactor = $flat['Sh_factor'] + ?? $flat['pkRadifSanad'] + ?? $flat['idRadifSanad'] + ?? null; + + $update = [ + 'sh_factor' => $shFactor !== null ? (string) $shFactor : null, + 'zarin_id' => $zarinId !== null ? (string) $zarinId : null, + 'factor_number' => isset($flat['Factor_Number']) ? (string) $flat['Factor_Number'] : (isset($flat['idFactor']) ? (string) $flat['idFactor'] : null), + 'res' => json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), + 'sync_status' => self::STATUS_SUCCESS, + 'sync_log' => null, + ]; + + if ($payload !== null) { + $update['data'] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + + if ($this->hasSyncColumns()) { + Zarin::where('id', $row->id)->update($update); + } else { + unset($update['sync_status'], $update['sync_log']); + Zarin::where('id', $row->id)->update($update); + } + } + + public function markFailed(Zarin $row, mixed $response = null, ?string $extra = null): void + { + $update = [ + 'res' => is_array($response) + ? json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + : (is_string($response) ? $response : null), + ]; + + if ($this->hasSyncColumns()) { + $update['sync_status'] = self::STATUS_FAILED; + $update['sync_log'] = $this->buildLog($response, $extra); + } + + Zarin::where('id', $row->id)->update($update); + } + + public function isFailed(Zarin $row): bool + { + if ($this->hasSyncColumns()) { + return ($row->sync_status ?? null) === self::STATUS_FAILED + || (empty($row->zarin_id) && ($row->sync_status ?? null) !== self::STATUS_SUCCESS); + } + + return empty($row->zarin_id); + } + + public function registerOnCreate(Customer $customer): void + { + if (! empty($customer->moshtari_code) || ! $this->isSyncEnabled()) { + return; + } + + $customerId = (int) $customer->id; + + dispatch(function () use ($customerId) { + $service = app(ZarinSyncService::class); + $customer = Customer::find($customerId); + if ($customer === null || ! empty($customer->moshtari_code) || ! $service->isSyncEnabled()) { + return; + } + + try { + $result = $service->registerCustomer($customer); + if (! ($result['ok'] ?? false)) { + Log::warning('Zarin auto-register on customer create failed', [ + 'customer_id' => $customerId, + 'message' => $result['message'] ?? null, + ]); + } + } catch (\Throwable $e) { + Log::warning('Zarin auto-register on customer create threw', [ + 'customer_id' => $customerId, + 'error' => $e->getMessage(), + ]); + } + })->afterResponse(); + } + + public function registerCustomer(Customer $customer): array + { + $customer->refresh(); + + if (! empty($customer->moshtari_code)) { + return [ + 'ok' => true, + 'message' => 'مشتری قبلاً در زرین ثبت شده است', + 'moshtari_code' => $customer->moshtari_code, + ]; + } + + $controller = app(ZarinController::class); + $result = $controller->createCustomerInZarin($customer->id); + + if (is_string($result)) { + return ['ok' => false, 'message' => $result]; + } + + $customer->refresh(); + + if (empty($customer->moshtari_code)) { + return ['ok' => false, 'message' => 'ثبت مشتری در زرین ناموفق بود']; + } + + return [ + 'ok' => true, + 'message' => 'مشتری با موفقیت در زرین ثبت شد', + 'moshtari_code' => $customer->moshtari_code, + ]; + } + + public function retry(Zarin $row): array + { + if (! $this->isFailed($row) && ! empty($row->zarin_id)) { + return ['ok' => false, 'message' => 'این سند قبلاً در زرین ثبت شده است']; + } + + $payload = json_decode((string) $row->data, true) ?: []; + $handler = $payload['handler'] ?? $this->inferHandler($row); + $controller = app(ZarinController::class); + + $result = match ($handler) { + 'wallet' => $controller->wallet(array_merge($payload, ['_retry_zarin_id' => $row->id]), $row), + 'walletGold' => $controller->walletGold(array_merge($payload, ['_retry_zarin_id' => $row->id]), $row), + 'storeTrade' => $controller->storeTrade(array_merge($payload, ['_retry_zarin_id' => $row->id]), $row), + 'goldReward' => $controller->goldReward(array_merge($payload, ['_retry_zarin_id' => $row->id]), $row), + default => false, + }; + + $row->refresh(); + + if ($result !== false && ! empty($row->zarin_id)) { + return ['ok' => true, 'message' => 'سند با موفقیت در زرین ثبت شد', 'zarin' => $row]; + } + + return [ + 'ok' => false, + 'message' => $row->sync_log ?: 'ارسال مجدد به زرین ناموفق بود', + 'zarin' => $row, + ]; + } + + private function inferHandler(Zarin $row): string + { + if ($row->order_id) { + return 'storeTrade'; + } + + if ($row->type === 'ريال') { + return 'wallet'; + } + + $desc = (string) $row->desc; + + if (str_contains($desc, 'شارژ کیف') || str_contains($desc, 'برداشت از کیف')) { + return 'walletGold'; + } + + return 'goldReward'; + } + + public function isSyncEnabled(): bool + { + $config = app(ConfigService::class); + + return $config->isZarinAccounting() + && trim($config->getString('zarin_api', '')) !== '' + && trim($config->getString('zarin_token', '')) !== ''; + } + + private function flattenResponse(array $response): array + { + $data = $response['data'] ?? null; + if (is_array($data)) { + $row = array_is_list($data) ? ($data[0] ?? []) : $data; + if (is_array($row)) { + return array_merge($response, $row); + } + } + + return $response; + } + + private function hasSyncColumns(): bool + { + return Schema::hasTable('zarin') && Schema::hasColumn('zarin', 'sync_status'); + } +} diff --git a/database/migrations/2026_07_23_000001_create_zarin_table.php b/database/migrations/2026_07_23_000001_create_zarin_table.php new file mode 100644 index 0000000..6dc84f3 --- /dev/null +++ b/database/migrations/2026_07_23_000001_create_zarin_table.php @@ -0,0 +1,49 @@ +id(); + $table->unsignedBigInteger('customer_id')->nullable()->index(); + $table->unsignedBigInteger('order_id')->nullable()->index(); + $table->unsignedBigInteger('withdraw_id')->nullable()->index(); + $table->decimal('amount', 18, 4)->nullable(); + $table->string('desc')->nullable(); + $table->string('type', 32)->nullable(); + $table->timestamp('date')->nullable(); + $table->string('zarin_id')->nullable()->index(); + $table->string('sh_factor')->nullable(); + $table->string('factor_number')->nullable(); + $table->longText('data')->nullable(); + $table->longText('res')->nullable(); + $table->string('sync_status', 20)->default('pending'); + $table->text('sync_log')->nullable(); + $table->timestamps(); + }); + } + + if (Schema::hasTable('products') && ! Schema::hasColumn('products', 'finance_p_name')) { + Schema::table('products', function (Blueprint $table) { + $table->string('finance_p_name')->nullable()->after('group_type'); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('zarin'); + + if (Schema::hasTable('products') && Schema::hasColumn('products', 'finance_p_name')) { + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('finance_p_name'); + }); + } + } +}; diff --git a/database/migrations/2026_07_23_000002_add_zarin_accounting_settings.php b/database/migrations/2026_07_23_000002_add_zarin_accounting_settings.php new file mode 100644 index 0000000..9526dfd --- /dev/null +++ b/database/migrations/2026_07_23_000002_add_zarin_accounting_settings.php @@ -0,0 +1,92 @@ + [ + 'value' => '', + 'title_fa' => 'آدرس API زرین', + 'description' => 'آدرس پایه وب‌سرویس زرین (بدون /ZarrinService/accounting).', + 'type' => 'text', + ], + 'zarin_token' => [ + 'value' => '', + 'title_fa' => 'توکن زرین', + 'description' => 'توکن مکمل API زرین که در نرم‌افزار زرین تعریف می‌شود.', + 'type' => 'text', + ], + 'zarin_noe_kar' => [ + 'value' => '16', + 'title_fa' => 'نوع کار پیش‌فرض زرین', + 'description' => 'کد noeKar هنگام ساخت کار ساخته در زرین.', + 'type' => 'number', + ], + 'zarin_gorohe_kar' => [ + 'value' => '52', + 'title_fa' => 'گروه کار پیش‌فرض زرین', + 'description' => 'کد gorooheKar هنگام ساخت/جستجوی کار در زرین. برای جداسازی چند پنل روی یک زرین، برای هر پنل مقدار متفاوت بگذارید.', + 'type' => 'number', + ], + 'zarin_name_g' => [ + 'value' => '', + 'title_fa' => 'نام گروه حساب مشتریان زرین', + 'description' => 'پارامتر nameG در addhesab. اگر چند پنل یک توکن زرین دارند، برای هر پنل نام گروه متفاوت بگذارید. خالی = نام برند (name_fa).', + 'type' => 'text', + ], + 'zarin_uid_prefix' => [ + 'value' => '', + 'title_fa' => 'پیشوند عددی uid زرین', + 'description' => 'فقط عدد. وقتی چند پنل یک زرین دارند باید متفاوت باشد تا uid تکراری نشود. مثال: 11 برای این پنل، 12 برای پنل دیگر.', + 'type' => 'text', + ], + ]; + + public function up(): void + { + foreach (self::KEYS as $key => $meta) { + $exists = DB::table('settings')->where('key', $key)->exists(); + if (! $exists) { + DB::table('settings')->insert([ + 'key' => $key, + 'value' => $meta['value'], + 'title_fa' => $meta['title_fa'], + 'can_edit' => 0, + 'is_active' => 1, + 'description' => $meta['description'], + 'type' => $meta['type'], + ]); + continue; + } + + // فقط متادیتا — value دست‌نخورده + DB::table('settings')->where('key', $key)->update([ + 'title_fa' => $meta['title_fa'], + 'description' => $meta['description'], + 'type' => $meta['type'], + 'can_edit' => 0, + 'is_active' => 1, + ]); + } + + if (DB::table('settings')->where('key', 'accounting_provider')->exists()) { + DB::table('settings')->where('key', 'accounting_provider')->update([ + 'title_fa' => 'سیستم حسابداری', + 'type' => 'select', + 'can_edit' => 0, + 'description' => 'none,pargar,tahesab,zarin', + ]); + } + } + + public function down(): void + { + // کلیدهای حسابداری حذف نمی‌شوند + } +}; diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 0282e9b..c8cc933 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -489,9 +489,33 @@ SELECT 'send_to_tahesab', 'false', 'ارسال به ته‌حساب (قدیمی) WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'send_to_tahesab' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) -SELECT 'accounting_provider', 'none', 'سیستم حسابداری', 0, 1, 'none,pargar,tahesab' +SELECT 'accounting_provider', 'none', 'سیستم حسابداری', 0, 1, 'none,pargar,tahesab,zarin' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'accounting_provider' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_api', '', 'آدرس API زرین', 0, 1, 'آدرس پایه وب‌سرویس زرین (بدون /ZarrinService/accounting).', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_api' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_token', '', 'توکن زرین', 0, 1, 'توکن مکمل API زرین که در نرم‌افزار زرین تعریف می‌شود.', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_token' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_noe_kar', '16', 'نوع کار پیش‌فرض زرین', 0, 1, 'کد noeKar هنگام ساخت کار ساخته در زرین.', 'number' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_noe_kar' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_gorohe_kar', '52', 'گروه کار پیش‌فرض زرین', 0, 1, 'کد gorooheKar هنگام ساخت/جستجوی کار در زرین. برای جداسازی چند پنل روی یک زرین، برای هر پنل مقدار متفاوت بگذارید.', 'number' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_gorohe_kar' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_name_g', '', 'نام گروه حساب مشتریان زرین', 0, 1, 'پارامتر nameG در addhesab. اگر چند پنل یک توکن زرین دارند، برای هر پنل نام گروه متفاوت بگذارید. خالی = نام برند (name_fa).', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_name_g' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_uid_prefix', '', 'پیشوند عددی uid زرین', 0, 1, 'فقط عدد. وقتی چند پنل یک زرین دارند باید متفاوت باشد تا uid تکراری نشود.', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_uid_prefix' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'send_to_pargar', 'false', 'ارسال به پرگار (قدیمی)', 0, 0, NULL WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'send_to_pargar' LIMIT 1); @@ -903,7 +927,7 @@ UPDATE `settings` SET `type` = 'longtext' WHERE `key` IN ( ); -- select (+ گزینه‌ها در description) -UPDATE `settings` SET `type` = 'select', `description` = 'none,pargar,tahesab' WHERE `key` = 'accounting_provider'; +UPDATE `settings` SET `type` = 'select', `description` = 'none,pargar,tahesab,zarin' WHERE `key` = 'accounting_provider'; UPDATE `settings` SET `type` = 'select', `description` = 'منبع فعال قیمت|montzar,zaarbox,main_product' WHERE `key` = 'price_provider'; UPDATE `settings` SET `type` = 'switch', `title_fa` = 'کد معرف اجباری', `description` = 'روشن = اجباری در ثبت‌نام | خاموش = اختیاری' WHERE `key` = 'referral_id'; UPDATE `settings` SET `type` = 'select', `description` = 'sepal,' WHERE `key` = 'payping_gateway_name'; @@ -1697,12 +1721,22 @@ UPDATE `settings` SET `can_edit` = 0, `is_active` = 0 WHERE `key` IN ('send_to_t UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'accounting_provider', 'tahesab_host', 'tahesab_port', 'tahesab_token', 'tahesab_db', 'tahesab_bank_account', + 'zarin_api', 'zarin_token', 'zarin_noe_kar', 'zarin_gorohe_kar', 'zarin_name_g', 'zarin_uid_prefix', 'pargar_username', 'pargar_password', 'pargar_api_token', 'pargar_item_id_rial', 'pargar_item_id_gold' ); -UPDATE `settings` SET `title_fa` = 'سیستم حسابداری', `type` = 'select', `description` = 'none,pargar,tahesab' +UPDATE `settings` SET `can_edit` = 0, `is_active` = 1 WHERE `key` LIKE 'zarin_%'; +UPDATE `settings` SET `type` = 'text', `can_edit` = 0 WHERE `key` IN ('zarin_api', 'zarin_token', 'zarin_name_g', 'zarin_uid_prefix'); +UPDATE `settings` SET `type` = 'number', `can_edit` = 0 WHERE `key` IN ('zarin_noe_kar', 'zarin_gorohe_kar'); +UPDATE `settings` SET `title_fa` = 'سیستم حسابداری', `type` = 'select', `description` = 'none,pargar,tahesab,zarin' WHERE `key` = 'accounting_provider'; -UPDATE `settings` SET `description` = 'سیستم حسابداری: none=هیچکدام، pargar=API پرگار، tahesab=ارسال خودکار ته‌حساب.' WHERE `key` = 'accounting_provider'; +UPDATE `settings` SET `description` = 'سیستم حسابداری: none=هیچکدام، pargar=API پرگار، tahesab=ارسال خودکار ته‌حساب، zarin=ارسال خودکار زرین.' WHERE `key` = 'accounting_provider'; +UPDATE `settings` SET `title_fa` = 'آدرس API زرین', `description` = 'آدرس پایه وب‌سرویس زرین (بدون /ZarrinService/accounting).' WHERE `key` = 'zarin_api'; +UPDATE `settings` SET `title_fa` = 'توکن زرین', `description` = 'توکن مکمل API زرین که در نرم‌افزار زرین تعریف می‌شود.' WHERE `key` = 'zarin_token'; +UPDATE `settings` SET `title_fa` = 'نوع کار پیش‌فرض زرین', `type` = 'number', `description` = 'کد noeKar هنگام ساخت کار ساخته در زرین.' WHERE `key` = 'zarin_noe_kar'; +UPDATE `settings` SET `title_fa` = 'گروه کار پیش‌فرض زرین', `type` = 'number', `description` = 'کد gorooheKar هنگام ساخت/جستجوی کار در زرین. برای جداسازی چند پنل روی یک زرین، برای هر پنل مقدار متفاوت بگذارید.' WHERE `key` = 'zarin_gorohe_kar'; +UPDATE `settings` SET `title_fa` = 'نام گروه حساب مشتریان زرین', `type` = 'text', `description` = 'پارامتر nameG در addhesab. اگر چند پنل یک توکن زرین دارند، برای هر پنل نام گروه متفاوت بگذارید. خالی = نام برند (name_fa).' WHERE `key` = 'zarin_name_g'; +UPDATE `settings` SET `title_fa` = 'پیشوند عددی uid زرین', `type` = 'text', `description` = 'فقط عدد. وقتی چند پنل یک زرین دارند باید متفاوت باشد تا uid تکراری نشود.' WHERE `key` = 'zarin_uid_prefix'; UPDATE `settings` SET `value` = 'pargar' WHERE `key` = 'accounting_provider' AND TRIM(COALESCE(`value`, '')) IN ('', 'none') diff --git a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql index 481df37..410ec17 100644 --- a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql +++ b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql @@ -444,9 +444,33 @@ SELECT 'send_to_tahesab', 'false', 'ارسال به ته‌حساب (قدیمی) WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'send_to_tahesab' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) -SELECT 'accounting_provider', 'none', 'سیستم حسابداری', 0, 1, 'none,pargar,tahesab' +SELECT 'accounting_provider', 'none', 'سیستم حسابداری', 0, 1, 'none,pargar,tahesab,zarin' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'accounting_provider' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_api', '', 'آدرس API زرین', 0, 1, 'آدرس پایه وب‌سرویس زرین (بدون /ZarrinService/accounting).', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_api' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_token', '', 'توکن زرین', 0, 1, 'توکن مکمل API زرین که در نرم‌افزار زرین تعریف می‌شود.', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_token' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_noe_kar', '16', 'نوع کار پیش‌فرض زرین', 0, 1, 'کد noeKar هنگام ساخت کار ساخته در زرین.', 'number' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_noe_kar' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_gorohe_kar', '52', 'گروه کار پیش‌فرض زرین', 0, 1, 'کد gorooheKar هنگام ساخت/جستجوی کار در زرین. برای جداسازی چند پنل روی یک زرین، برای هر پنل مقدار متفاوت بگذارید.', 'number' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_gorohe_kar' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_name_g', '', 'نام گروه حساب مشتریان زرین', 0, 1, 'پارامتر nameG در addhesab. اگر چند پنل یک توکن زرین دارند، برای هر پنل نام گروه متفاوت بگذارید. خالی = نام برند (name_fa).', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_name_g' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'zarin_uid_prefix', '', 'پیشوند عددی uid زرین', 0, 1, 'فقط عدد. وقتی چند پنل یک زرین دارند باید متفاوت باشد تا uid تکراری نشود.', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zarin_uid_prefix' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'send_to_pargar', 'false', 'ارسال به پرگار (قدیمی)', 0, 0, NULL WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'send_to_pargar' LIMIT 1); @@ -1389,11 +1413,21 @@ UPDATE `settings` SET `can_edit` = 0, `is_active` = 0 WHERE `key` IN ('send_to_t UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'accounting_provider', 'tahesab_host', 'tahesab_port', 'tahesab_token', 'tahesab_db', 'tahesab_bank_account', + 'zarin_api', 'zarin_token', 'zarin_noe_kar', 'zarin_gorohe_kar', 'zarin_name_g', 'zarin_uid_prefix', 'pargar_username', 'pargar_password', 'pargar_api_token' ); -UPDATE `settings` SET `title_fa` = 'سیستم حسابداری', `type` = 'select', `description` = 'none,pargar,tahesab' +UPDATE `settings` SET `can_edit` = 0, `is_active` = 1 WHERE `key` LIKE 'zarin_%'; +UPDATE `settings` SET `type` = 'text', `can_edit` = 0 WHERE `key` IN ('zarin_api', 'zarin_token', 'zarin_name_g', 'zarin_uid_prefix'); +UPDATE `settings` SET `type` = 'number', `can_edit` = 0 WHERE `key` IN ('zarin_noe_kar', 'zarin_gorohe_kar'); +UPDATE `settings` SET `title_fa` = 'سیستم حسابداری', `type` = 'select', `description` = 'none,pargar,tahesab,zarin' WHERE `key` = 'accounting_provider'; -UPDATE `settings` SET `description` = 'سیستم حسابداری: none=هیچکدام، pargar=API پرگار، tahesab=ارسال خودکار ته‌حساب.' WHERE `key` = 'accounting_provider'; +UPDATE `settings` SET `description` = 'سیستم حسابداری: none=هیچکدام، pargar=API پرگار، tahesab=ارسال خودکار ته‌حساب، zarin=ارسال خودکار زرین.' WHERE `key` = 'accounting_provider'; +UPDATE `settings` SET `title_fa` = 'آدرس API زرین', `description` = 'آدرس پایه وب‌سرویس زرین (بدون /ZarrinService/accounting).' WHERE `key` = 'zarin_api'; +UPDATE `settings` SET `title_fa` = 'توکن زرین', `description` = 'توکن مکمل API زرین که در نرم‌افزار زرین تعریف می‌شود.' WHERE `key` = 'zarin_token'; +UPDATE `settings` SET `title_fa` = 'نوع کار پیش‌فرض زرین', `type` = 'number', `description` = 'کد noeKar هنگام ساخت کار ساخته در زرین.' WHERE `key` = 'zarin_noe_kar'; +UPDATE `settings` SET `title_fa` = 'گروه کار پیش‌فرض زرین', `type` = 'number', `description` = 'کد gorooheKar هنگام ساخت/جستجوی کار در زرین. برای جداسازی چند پنل روی یک زرین، برای هر پنل مقدار متفاوت بگذارید.' WHERE `key` = 'zarin_gorohe_kar'; +UPDATE `settings` SET `title_fa` = 'نام گروه حساب مشتریان زرین', `type` = 'text', `description` = 'پارامتر nameG در addhesab. اگر چند پنل یک توکن زرین دارند، برای هر پنل نام گروه متفاوت بگذارید. خالی = نام برند (name_fa).' WHERE `key` = 'zarin_name_g'; +UPDATE `settings` SET `title_fa` = 'پیشوند عددی uid زرین', `type` = 'text', `description` = 'فقط عدد. وقتی چند پنل یک زرین دارند باید متفاوت باشد تا uid تکراری نشود.' WHERE `key` = 'zarin_uid_prefix'; UPDATE `settings` SET `value` = 'pargar' WHERE `key` = 'accounting_provider' AND TRIM(COALESCE(`value`, '')) IN ('', 'none') diff --git a/routes/admin.php b/routes/admin.php index 547c83d..0b776cd 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -15,6 +15,7 @@ use App\Http\Controllers\Admin\PriceTriggerController as AdminPriceTriggerContro use App\Http\Controllers\Finance\FinanceController; use App\Http\Controllers\Admin\ProductController; use App\Http\Controllers\Admin\TahesabController; +use App\Http\Controllers\Admin\ZarinController; use App\Http\Controllers\Admin\PargarController; use App\Http\Controllers\Admin\FileController; use App\Http\Controllers\Site\OfferCodesController; @@ -188,6 +189,16 @@ Route::middleware('auth:user')->group(function (){ Route::post('tahesab/{id}/retry', [TahesabController::class, 'retrySanad']); Route::post('customer/{id}/tahesab/register', [TahesabController::class, 'registerCustomer']); + Route::post('wallet/zarin', [ZarinController::class, 'wallet']); + Route::post('zarin/table', [ZarinController::class, 'table']); + Route::get('zarin/customer-sync/status', [ZarinController::class, 'customerSyncStatus']); + Route::post('zarin/customer-sync/panel-to-zarin', [ZarinController::class, 'syncPanelToZarin']); + Route::post('zarin/customer-sync/import-from-zarin', [ZarinController::class, 'importFromZarin']); + Route::post('zarin/customer-sync/rollback-import', [ZarinController::class, 'rollbackImport']); + Route::get('zarin/{id}', [ZarinController::class, 'show']); + Route::post('zarin/{id}/retry', [ZarinController::class, 'retrySanad']); + Route::post('customer/{id}/zarin/register', [ZarinController::class, 'registerCustomer']); + Route::post('pargar/table', [PargarController::class, 'table']); Route::get('pargar/customer-sync/status', [PargarController::class, 'customerSyncStatus']); Route::post('pargar/customer-sync/panel-to-pargar', [PargarController::class, 'syncPanelToPargar']);