1618 lines
55 KiB
PHP
1618 lines
55 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, int $timeout = 15)
|
||
{
|
||
$url = $this->zarinAccountingUrl();
|
||
$token = $this->zarinToken();
|
||
|
||
if (!$url || !$token) {
|
||
return null;
|
||
}
|
||
|
||
$params['token'] = $token;
|
||
|
||
return Http::withoutVerifying()
|
||
->timeout($timeout)
|
||
->get($url, $params);
|
||
}
|
||
|
||
/**
|
||
* decode پاسخ HTTP زرین — خطاهای متنی (مثل ERROR: radif is empty) را هم برمیگرداند.
|
||
*
|
||
* @return array{0: ?array, 1: ?string} [json, error]
|
||
*/
|
||
private function zarinDecodeResponse($http): array
|
||
{
|
||
if (!$http) {
|
||
return [null, 'تنظیمات zarin_api یا zarin_token خالی است.'];
|
||
}
|
||
|
||
$body = (string) $http->body();
|
||
$json = $http->json();
|
||
if (!is_array($json) && $body !== '') {
|
||
$decoded = json_decode($body, true);
|
||
if (is_array($decoded)) {
|
||
$json = $decoded;
|
||
}
|
||
}
|
||
|
||
if (!$http->successful()) {
|
||
$detail = null;
|
||
if (is_array($json)) {
|
||
$detail = $json['detail'] ?? $json['msg'] ?? $json['message'] ?? $json['error'] ?? null;
|
||
}
|
||
if ($detail === null || $detail === '') {
|
||
$detail = trim($body) !== '' ? mb_substr(trim($body), 0, 300) : null;
|
||
}
|
||
$error = 'HTTP ' . $http->status() . ($detail ? (': ' . $detail) : '');
|
||
|
||
return [is_array($json) ? $json : null, $error];
|
||
}
|
||
|
||
if (!is_array($json)) {
|
||
return [null, 'پاسخ زرین JSON نیست (طول ' . strlen($body) . ')'];
|
||
}
|
||
|
||
return [$json, null];
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/** نام گروه حساب مشتری در زرین — تنظیم zarin_name_g؛ خالی = گروه ۶ مشتری نقدی */
|
||
private function zarinCustomerGroupName(): string
|
||
{
|
||
$nameG = trim((string) $this->Config('zarin_name_g'));
|
||
|
||
return $nameG !== '' ? $nameG : 'مشتری نقدی (خریدار)';
|
||
}
|
||
|
||
/** آیا درخواست لیست زرین را خواسته؟ (query/body/header؛ مقاوم به QUERY_STRING خالی) */
|
||
private function requestWantsZarinList(Request $request): bool
|
||
{
|
||
$header = $request->header('X-Include-Zarin', $request->header('Include-Zarin'));
|
||
if ($header !== null && $header !== '') {
|
||
if (filter_var($header, FILTER_VALIDATE_BOOLEAN) || in_array($header, [1, '1', 'yes', 'on'], true)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
foreach (['include_zarin', 'include_tahesab'] as $key) {
|
||
$raw = $request->query($key, $request->input($key, null));
|
||
if ($raw === null && !$request->query->has($key) && !$request->exists($key)) {
|
||
continue;
|
||
}
|
||
// ?include_zarin بدون مقدار
|
||
if ($raw === null || $raw === '' || $raw === []) {
|
||
return true;
|
||
}
|
||
if ($request->boolean($key)) {
|
||
return true;
|
||
}
|
||
if (in_array($raw, [1, '1', true, 'true', 'yes', 'on', 'TRUE', 'Yes'], true)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/** مقایسه نام گروه زرین (ی/ک، پرانتز/آندرلاین، فاصله) */
|
||
private function zarinGroupNamesMatch(string $a, string $b): bool
|
||
{
|
||
$norm = function (string $s): string {
|
||
$s = $this->normalizeZarinName($s);
|
||
$s = str_replace(['(', ')', '_', '-', '–', '—'], ' ', $s);
|
||
|
||
return trim(preg_replace('/\s+/u', ' ', $s) ?? $s);
|
||
};
|
||
|
||
$na = $norm($a);
|
||
$nb = $norm($b);
|
||
if ($na === '' || $nb === '') {
|
||
return false;
|
||
}
|
||
|
||
return $na === $nb
|
||
|| $this->containsText($na, $nb)
|
||
|| $this->containsText($nb, $na);
|
||
}
|
||
|
||
/**
|
||
* پیشوند عددی 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;
|
||
}
|
||
|
||
/**
|
||
* پیدا کردن کار/کالای محصول در زرین.
|
||
* اول infokarbarcodi (اگر روی آن سرور فعال باشد)؛ وگرنه infosakhte (لیست کامل).
|
||
*/
|
||
private function findZarinKarForProduct(Product $product, $goroheKar = null)
|
||
{
|
||
$goroheKar = $goroheKar ?: $this->zarinDefaultGoroheKar();
|
||
$productName = $this->normalizeZarinName($product->title);
|
||
|
||
$fromBarcode = $this->findZarinKarInInfokarbarcodi($productName, $goroheKar);
|
||
if ($fromBarcode) {
|
||
return $fromBarcode;
|
||
}
|
||
|
||
return $this->findZarinKarInInfosakhte($productName, $goroheKar);
|
||
}
|
||
|
||
private function matchZarinKarRow(string $productName, array $row): ?array
|
||
{
|
||
$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) {
|
||
return null;
|
||
}
|
||
|
||
if (
|
||
$rowName === $productName ||
|
||
$this->containsText($rowName, $productName) ||
|
||
$this->containsText($productName, $rowName)
|
||
) {
|
||
return [
|
||
'idKar' => $idKar,
|
||
'row' => $row,
|
||
];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private function findZarinKarInInfokarbarcodi(string $productName, $goroheKar): ?array
|
||
{
|
||
for ($page = 1; $page <= 10; $page++) {
|
||
$response = $this->zarinRequest([
|
||
'kind' => 'infokarbarcodi',
|
||
'id' => $goroheKar,
|
||
'vazneat' => 0,
|
||
'pageNum' => $page,
|
||
'detailCount' => 250,
|
||
]);
|
||
|
||
// روی بعضی سرورهای زرین جدول barcodetala نیست → 400؛ برو سراغ infosakhte
|
||
if (!$response || !$response->successful()) {
|
||
return null;
|
||
}
|
||
|
||
$rows = $this->zarinRowsFromBody($response->json());
|
||
if (empty($rows)) {
|
||
break;
|
||
}
|
||
|
||
foreach ($rows as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$hit = $this->matchZarinKarRow($productName, $row);
|
||
if ($hit) {
|
||
return $hit;
|
||
}
|
||
}
|
||
|
||
if (count($rows) < 250) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/** لیست کارهای ساخته — فیلد nam_kar_sakhte / id (تستشده روی API زرین) */
|
||
private function findZarinKarInInfosakhte(string $productName, $goroheKar): ?array
|
||
{
|
||
$response = $this->zarinRequest([
|
||
'kind' => 'infosakhte',
|
||
], 30);
|
||
|
||
if (!$response || !$response->successful()) {
|
||
return null;
|
||
}
|
||
|
||
$rows = $this->zarinRowsFromBody($response->json());
|
||
if (empty($rows)) {
|
||
return null;
|
||
}
|
||
|
||
$noeKar = (string) $this->zarinDefaultNoeKar();
|
||
$gorohe = (string) $goroheKar;
|
||
$preferred = null;
|
||
$fallback = null;
|
||
|
||
foreach ($rows as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$hit = $this->matchZarinKarRow($productName, $row);
|
||
if (!$hit) {
|
||
continue;
|
||
}
|
||
|
||
$idGp = (string) ($row['idGp'] ?? $row['idgp'] ?? '');
|
||
if ($idGp !== '' && ($idGp === $gorohe || $idGp === $noeKar)) {
|
||
$preferred = $hit;
|
||
break;
|
||
}
|
||
if ($fallback === null) {
|
||
$fallback = $hit;
|
||
}
|
||
}
|
||
|
||
return $preferred ?? $fallback;
|
||
}
|
||
|
||
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 قابل استفاده از infosakhte/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);
|
||
}
|
||
|
||
/**
|
||
* لیست حسابهای زرین — فقط listhesabha (Get100Account لیست نیست و ۴۰۰ میدهد).
|
||
*
|
||
* @return array{rows: array<int, array>, error: ?string, http_status: ?int}
|
||
*/
|
||
private function fetchAllZarinCustomers(): array
|
||
{
|
||
if (!$this->zarinAccountingUrl() || !$this->zarinToken()) {
|
||
return [
|
||
'rows' => [],
|
||
'error' => 'تنظیمات zarin_api یا zarin_token خالی است.',
|
||
'http_status' => null,
|
||
];
|
||
}
|
||
|
||
try {
|
||
// listhesabha کامل است (~۳۰۰ ردیف / چند ثانیه)؛ timeout بالاتر از درخواستهای کوتاه
|
||
$response = $this->zarinRequest([
|
||
'kind' => 'listhesabha',
|
||
], 45);
|
||
|
||
if (!$response) {
|
||
return [
|
||
'rows' => [],
|
||
'error' => 'درخواست به زرین ساخته نشد (آدرس/توکن).',
|
||
'http_status' => null,
|
||
];
|
||
}
|
||
|
||
$status = $response->status();
|
||
$body = $response->body();
|
||
|
||
if (!$response->successful()) {
|
||
$snippet = mb_substr(trim((string) $body), 0, 180);
|
||
$msg = "listhesabha ناموفق (HTTP {$status})";
|
||
if ($snippet !== '') {
|
||
$msg .= ': ' . $snippet;
|
||
}
|
||
|
||
return [
|
||
'rows' => [],
|
||
'error' => $msg,
|
||
'http_status' => $status,
|
||
];
|
||
}
|
||
|
||
$json = $response->json();
|
||
if (!is_array($json)) {
|
||
return [
|
||
'rows' => [],
|
||
'error' => 'پاسخ listhesabha JSON معتبر نیست. طول بدنه: ' . strlen((string) $body),
|
||
'http_status' => $status,
|
||
];
|
||
}
|
||
|
||
$rows = $this->zarinRowsFromBody($json);
|
||
|
||
if (empty($rows)) {
|
||
return [
|
||
'rows' => [],
|
||
'error' => 'listhesabha خالی بود یا کلید data پیدا نشد. کلیدها: '
|
||
. implode(',', array_keys($json)),
|
||
'http_status' => $status,
|
||
];
|
||
}
|
||
|
||
return [
|
||
'rows' => $rows,
|
||
'error' => null,
|
||
'http_status' => $status,
|
||
];
|
||
} catch (\Exception $e) {
|
||
Log::warning('zarin listhesabha failed', ['message' => $e->getMessage()]);
|
||
|
||
return [
|
||
'rows' => [],
|
||
'error' => 'خطا در ارتباط با زرین: ' . $e->getMessage(),
|
||
'http_status' => null,
|
||
];
|
||
}
|
||
}
|
||
|
||
/** نام حساب از پاسخ listhesabha (فیلد nam، نه name) */
|
||
private function extractZarinName(array $item): string
|
||
{
|
||
$nam = trim((string) ($item['nam'] ?? $item['Nam'] ?? ''));
|
||
if ($nam !== '') {
|
||
// "تستی * هوم" → "تستی هوم"
|
||
$nam = preg_replace('/\s*\*\s*/u', ' ', $nam) ?? $nam;
|
||
|
||
return trim(preg_replace('/\s+/u', ' ', $nam) ?? $nam);
|
||
}
|
||
|
||
$name = trim((string) ($item['name'] ?? $item['Name'] ?? ''));
|
||
$family = trim((string) ($item['family'] ?? $item['Family'] ?? ''));
|
||
$full = trim($name . ' ' . $family);
|
||
|
||
return $full !== '' ? $full : $name;
|
||
}
|
||
|
||
private function extractZarinGroup(array $item): string
|
||
{
|
||
return trim((string) (
|
||
$item['gorooh']
|
||
?? $item['GoroupName']
|
||
?? $item['group']
|
||
?? $item['Group']
|
||
?? ''
|
||
));
|
||
}
|
||
|
||
public function customerSyncStatus(Request $request): \Illuminate\Http\JsonResponse
|
||
{
|
||
return $this->buildCustomerSyncStatus($request, $this->requestWantsZarinList($request));
|
||
}
|
||
|
||
/** مسیر بدون query — وقتی nginx QUERY_STRING را نمیفرستد */
|
||
public function customerSyncStatusWithZarin(Request $request): \Illuminate\Http\JsonResponse
|
||
{
|
||
return $this->buildCustomerSyncStatus($request, true);
|
||
}
|
||
|
||
private function buildCustomerSyncStatus(Request $request, bool $include): \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;
|
||
$zarinFetchError = null;
|
||
$zarinHttpStatus = null;
|
||
$zarinGroupFilter = $this->zarinCustomerGroupName();
|
||
$zarinOnlyResult = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1];
|
||
$debugSample = [];
|
||
|
||
if ($include) {
|
||
$fetched = $this->fetchAllZarinCustomers();
|
||
$zarinCustomers = $fetched['rows'];
|
||
$zarinFetchError = $fetched['error'];
|
||
$zarinHttpStatus = $fetched['http_status'];
|
||
|
||
// فقط گروه تنظیمشده (گروه ۶ / zarin_name_g) — نه همهٔ حسابهای زرین
|
||
if ($zarinGroupFilter !== '' && !empty($zarinCustomers)) {
|
||
$zarinCustomers = array_values(array_filter($zarinCustomers, function ($item) use ($zarinGroupFilter) {
|
||
if (!is_array($item)) {
|
||
return false;
|
||
}
|
||
$g = $this->extractZarinGroup($item);
|
||
|
||
return $g !== '' && $this->zarinGroupNamesMatch($g, $zarinGroupFilter);
|
||
}));
|
||
}
|
||
|
||
$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;
|
||
}
|
||
$zarinOnlyList[] = [
|
||
'code' => $code,
|
||
'name' => $this->extractZarinName($item),
|
||
'tel' => $phone,
|
||
'gid' => $item['GID'] ?? null,
|
||
'group' => $this->extractZarinGroup($item),
|
||
];
|
||
}
|
||
$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,
|
||
'zarin_fetch_error' => $zarinFetchError,
|
||
'zarin_http_status' => $zarinHttpStatus,
|
||
'zarin_group_filter' => $zarinGroupFilter,
|
||
'include_zarin' => $include,
|
||
'include_zarin_param' => $request->query('include_zarin', $request->input('include_zarin')),
|
||
'panel_only_list' => $panelOnlyList,
|
||
'zarin_only_list' => $zarinOnlyResult,
|
||
];
|
||
|
||
if ($include && ($request->boolean('raw') || $request->input('raw'))) {
|
||
$payload['zarin_debug_sample'] = $debugSample;
|
||
}
|
||
|
||
return response()->json($payload)->header('Cache-Control', 'no-store, private');
|
||
}
|
||
|
||
private function extractZarinPhone(array $item): string
|
||
{
|
||
$candidates = ['mobile', 'Mobile', 'tell', 'Tell', 'tel', 'Tel', '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' => 'nullable|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 ($fullName === '') {
|
||
$fullName = 'مشتری زرین ' . $code;
|
||
}
|
||
|
||
// موبایل برای خیلی از حسابهای زرین خالی است (مثل تستی هوم) — مانع import نیست
|
||
$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 !== '' ? $mobile : null;
|
||
$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, $httpError] = $this->zarinDecodeResponse($http);
|
||
if ($httpError) {
|
||
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
|
||
return false;
|
||
}
|
||
} 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, $httpError] = $this->zarinDecodeResponse($http);
|
||
if ($httpError) {
|
||
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
|
||
return false;
|
||
}
|
||
|
||
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, $httpError] = $this->zarinDecodeResponse($http);
|
||
if ($httpError) {
|
||
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
|
||
return false;
|
||
}
|
||
} 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, $httpError] = $this->zarinDecodeResponse($http);
|
||
if ($httpError) {
|
||
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
|
||
return false;
|
||
}
|
||
} 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, $httpError] = $this->zarinDecodeResponse($http);
|
||
if ($httpError) {
|
||
return $httpError;
|
||
}
|
||
|
||
$data = $this->extractResponseRow($response);
|
||
$idHesab = $data['idHesab'] ?? null;
|
||
|
||
if (!empty($idHesab)) {
|
||
$customer->moshtari_code = $idHesab;
|
||
$customer->save();
|
||
|
||
// int برگردان تا با پیام خطای string اشتباه نشود
|
||
return is_numeric($idHesab) ? (0 + $idHesab) : $idHesab;
|
||
}
|
||
|
||
$detail = $data['msg'] ?? $data['message'] ?? $data['detail'] ?? null;
|
||
return $detail ? (string) $detail : 'zarin api error!!!';
|
||
} catch (\Exception $e) {
|
||
return $e->getMessage();
|
||
}
|
||
} finally {
|
||
$lock->release();
|
||
}
|
||
}
|
||
|
||
public function healthCheck()
|
||
{
|
||
$http = $this->zarinRequest([
|
||
'kind' => 'version',
|
||
]);
|
||
|
||
[$json, $error] = $this->zarinDecodeResponse($http);
|
||
|
||
if ($error) {
|
||
return response()->json([
|
||
'ok' => false,
|
||
'message' => $error,
|
||
'body' => $json,
|
||
], 422);
|
||
}
|
||
|
||
return response()->json([
|
||
'ok' => true,
|
||
'version' => $json['version'] ?? null,
|
||
'message' => $json['message'] ?? 'OK',
|
||
'raw' => $json,
|
||
]);
|
||
}
|
||
}
|