1332 lines
45 KiB
PHP
1332 lines
45 KiB
PHP
<?php
|
||||
|
|
|
|||
|
|
namespace App\Http\Controllers\Admin;
|
|||
|
|
|
|||
|
|
use App\Http\Controllers\Controller;
|
|||
|
|
use App\Http\Controllers\Site\ReportController;
|
|||
|
|
use App\Models\Customer;
|
|||
|
|
use App\Models\Order;
|
|||
|
|
use App\Models\Product;
|
|||
|
|
use App\Models\Zarin;
|
|||
|
|
use App\Services\ConfigService;
|
|||
|
|
use App\Services\ZarinSyncService;
|
|||
|
|
use Carbon\Carbon;
|
|||
|
|
use Illuminate\Http\Request;
|
|||
|
|
use Illuminate\Support\Facades\Cache;
|
|||
|
|
use Illuminate\Support\Facades\Http;
|
|||
|
|
use Illuminate\Support\Facades\Log;
|
|||
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|||
|
|
|
|||
|
|
class ZarinController extends Controller
|
|||
|
|
{
|
|||
|
|
protected function sync(): ZarinSyncService
|
|||
|
|
{
|
|||
|
|
return app(ZarinSyncService::class);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function lastPriceData(): array
|
|||
|
|
{
|
|||
|
|
$response = app(ReportController::class)->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();
|
|||
|
|
}
|
|||
|
|
}
|