added some feature for accounting and enhance performance for dashboard
This commit is contained in:
@@ -84,7 +84,8 @@ class TahesabController extends Controller
|
||||
])->json();
|
||||
|
||||
if (! is_array($response)) return [];
|
||||
return $response;
|
||||
|
||||
return $this->tahesabCustomerRows($response);
|
||||
} catch (\Exception $e) {
|
||||
return [];
|
||||
}
|
||||
@@ -113,44 +114,83 @@ class TahesabController extends Controller
|
||||
$tahesabOnlyCount = null;
|
||||
$tahesabOnlyResult = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1];
|
||||
|
||||
$tahesabWithPhoneCount = null;
|
||||
$tahesabWithoutPhoneCount = null;
|
||||
$tahesabImportableCount = null;
|
||||
$phoneMatchCount = null;
|
||||
$phoneConflictCount = null;
|
||||
$phoneMatches = [];
|
||||
$phoneConflicts = [];
|
||||
|
||||
if ($request->boolean('include_tahesab')) {
|
||||
$tahesabCustomers = $this->fetchAllTahesabCustomers();
|
||||
$tahesabTotal = count($tahesabCustomers);
|
||||
|
||||
// موبایلهای پنل بدون صفر ابتدایی
|
||||
$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();
|
||||
$walletService = app(TahesabInboundWalletService::class);
|
||||
$maps = $this->panelAccountingMaps();
|
||||
|
||||
$tahesabOnlyList = [];
|
||||
$debugSample = [];
|
||||
$withPhone = 0;
|
||||
$withoutPhone = 0;
|
||||
foreach ($tahesabCustomers as $item) {
|
||||
if (! is_array($item)) continue;
|
||||
if (count($debugSample) < 3) $debugSample[] = $item; // نمونهٔ خام برای تشخیص نام فیلدها
|
||||
$code = $item['Code'] ?? null;
|
||||
if ($code !== null && isset($panelCodes[(string) $code])) continue;
|
||||
if (count($debugSample) < 3) $debugSample[] = $item;
|
||||
$code = $this->extractTahesabCode($item);
|
||||
$codeKey = $this->accountingCodeKey($code);
|
||||
$phone = $this->extractTahesabPhone($item);
|
||||
$tel = ltrim($phone, '0');
|
||||
if ($tel && isset($panelMobiles[$tel])) continue;
|
||||
$hasPhone = Customer::normalizeMobile($phone) !== null;
|
||||
if ($hasPhone) {
|
||||
$withPhone++;
|
||||
} else {
|
||||
$withoutPhone++;
|
||||
}
|
||||
|
||||
if ($codeKey !== null && isset($maps['by_code'][$codeKey])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mobileKey = Customer::normalizeMobile($phone);
|
||||
if ($mobileKey && isset($maps['by_mobile'][$mobileKey])) {
|
||||
$panel = $maps['by_mobile'][$mobileKey];
|
||||
$panelCodeKey = $this->accountingCodeKey($panel->moshtari_code);
|
||||
$row = [
|
||||
'code' => $code !== null ? (int) $code : null,
|
||||
'name' => trim((string) ($item['Name'] ?? $item['name'] ?? '')),
|
||||
'tel' => $phone,
|
||||
'panel_id' => (int) $panel->id,
|
||||
'panel_name' => $panel->name,
|
||||
'panel_code' => $panelCodeKey,
|
||||
];
|
||||
if ($panelCodeKey && $panelCodeKey !== $codeKey) {
|
||||
$phoneConflicts[] = $row;
|
||||
} else {
|
||||
$phoneMatches[] = $row;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($codeKey === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mande = $walletService->mandeFromAccountingRow($item);
|
||||
$tahesabOnlyList[] = [
|
||||
'code' => $item['Code'] ?? null,
|
||||
'name' => $item['Name'] ?? '',
|
||||
'code' => $code !== null ? (int) $code : null,
|
||||
'name' => trim((string) ($item['Name'] ?? $item['name'] ?? '')),
|
||||
'tel' => $phone,
|
||||
'gid' => $item['GID'] ?? null,
|
||||
'group' => $item['GoroupName'] ?? '',
|
||||
'gid' => $item['GID'] ?? $item['gid'] ?? null,
|
||||
'group' => $item['GoroupName'] ?? $item['group'] ?? '',
|
||||
'has_phone' => $hasPhone,
|
||||
'gold' => $mande['gold'] ?? null,
|
||||
'cash' => $mande['cash'] ?? null,
|
||||
];
|
||||
}
|
||||
$tahesabOnlyCount = count($tahesabOnlyList);
|
||||
// همه رو برمیگردونیم — frontend خودش paginate میکنه
|
||||
$tahesabWithPhoneCount = $withPhone;
|
||||
$tahesabWithoutPhoneCount = $withoutPhone;
|
||||
$tahesabImportableCount = count(array_filter($tahesabOnlyList, fn ($r) => ! empty($r['has_phone'])));
|
||||
$phoneMatchCount = count($phoneMatches);
|
||||
$phoneConflictCount = count($phoneConflicts);
|
||||
$tahesabOnlyResult = [
|
||||
'data' => array_values($tahesabOnlyList),
|
||||
'total' => $tahesabOnlyCount,
|
||||
@@ -165,6 +205,13 @@ class TahesabController extends Controller
|
||||
'panel_only' => $panelOnly,
|
||||
'tahesab_total' => $tahesabTotal,
|
||||
'tahesab_only_count' => $tahesabOnlyCount,
|
||||
'tahesab_with_phone_count' => $tahesabWithPhoneCount,
|
||||
'tahesab_without_phone_count' => $tahesabWithoutPhoneCount,
|
||||
'tahesab_importable_count' => $tahesabImportableCount,
|
||||
'phone_match_count' => $phoneMatchCount,
|
||||
'phone_conflict_count' => $phoneConflictCount,
|
||||
'phone_matches' => $phoneMatches,
|
||||
'phone_conflicts' => $phoneConflicts,
|
||||
'panel_only_list' => $panelOnlyList,
|
||||
'tahesab_only_list' => $tahesabOnlyResult,
|
||||
];
|
||||
@@ -198,6 +245,206 @@ class TahesabController extends Controller
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $response
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function tahesabCustomerRows(array $response): array
|
||||
{
|
||||
if ($response === []) {
|
||||
return [];
|
||||
}
|
||||
if (isset($response[0]) && is_array($response[0])) {
|
||||
return array_values(array_filter($response, 'is_array'));
|
||||
}
|
||||
foreach (['Moshtari', 'ListMoshtari', 'data', 'Data'] as $key) {
|
||||
if (isset($response[$key]) && is_array($response[$key])) {
|
||||
return array_values(array_filter($response[$key], 'is_array'));
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_filter($response, 'is_array'));
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $item */
|
||||
private function extractTahesabCode(array $item): mixed
|
||||
{
|
||||
foreach (['Code', 'code', 'MoshtariCode', 'moshtari_code'] as $key) {
|
||||
if (isset($item[$key]) && $item[$key] !== '' && $item[$key] !== null) {
|
||||
return $item[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function accountingCodeKey(mixed $code): ?string
|
||||
{
|
||||
if ($code === null || $code === '' || $code === false) {
|
||||
return null;
|
||||
}
|
||||
$s = trim((string) $code);
|
||||
if ($s === '' || $s === '0' || $s === '-') {
|
||||
return null;
|
||||
}
|
||||
if (is_numeric($s)) {
|
||||
$n = (int) $s;
|
||||
|
||||
return $n > 0 ? (string) $n : null;
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{by_code: array<string, Customer>, by_mobile: array<string, Customer>}
|
||||
*/
|
||||
private function panelAccountingMaps(): array
|
||||
{
|
||||
$byCode = [];
|
||||
$byMobile = [];
|
||||
$rows = Customer::whereNull('deleted_at')->get(['id', 'name', 'mobile', 'moshtari_code', 'type']);
|
||||
foreach ($rows as $row) {
|
||||
$codeKey = $this->accountingCodeKey($row->moshtari_code);
|
||||
if ($codeKey !== null) {
|
||||
$byCode[$codeKey] = $row;
|
||||
}
|
||||
$mobile = Customer::normalizeMobile((string) ($row->mobile ?? ''));
|
||||
if ($mobile !== null) {
|
||||
$byMobile[$mobile] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return ['by_code' => $byCode, 'by_mobile' => $byMobile];
|
||||
}
|
||||
|
||||
/** لینک کد تهحساب روی مشتری پنل وقتی شماره یکسان است */
|
||||
public function linkMatchingPhones(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$tahesabCustomers = $this->fetchAllTahesabCustomers();
|
||||
$maps = $this->panelAccountingMaps();
|
||||
$linked = [];
|
||||
$conflicts = [];
|
||||
$toSync = [];
|
||||
|
||||
foreach ($tahesabCustomers as $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$code = $this->extractTahesabCode($item);
|
||||
$codeKey = $this->accountingCodeKey($code);
|
||||
if ($codeKey === null) {
|
||||
continue;
|
||||
}
|
||||
$phone = $this->extractTahesabPhone($item);
|
||||
$mobileKey = Customer::normalizeMobile($phone);
|
||||
if ($mobileKey === null || ! isset($maps['by_mobile'][$mobileKey])) {
|
||||
continue;
|
||||
}
|
||||
$panel = $maps['by_mobile'][$mobileKey];
|
||||
$panelCodeKey = $this->accountingCodeKey($panel->moshtari_code);
|
||||
|
||||
if (isset($maps['by_code'][$codeKey]) && (int) $maps['by_code'][$codeKey]->id !== (int) $panel->id) {
|
||||
$conflicts[] = [
|
||||
'code' => (int) $codeKey,
|
||||
'name' => trim((string) ($item['Name'] ?? '')),
|
||||
'tel' => $phone,
|
||||
'panel_id' => (int) $panel->id,
|
||||
'panel_name' => $panel->name,
|
||||
'error' => 'این کد تهحساب روی مشتری دیگری در پنل است',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($panelCodeKey === $codeKey) {
|
||||
continue;
|
||||
}
|
||||
if ($panelCodeKey) {
|
||||
$conflicts[] = [
|
||||
'code' => (int) $codeKey,
|
||||
'name' => trim((string) ($item['Name'] ?? '')),
|
||||
'tel' => $phone,
|
||||
'panel_id' => (int) $panel->id,
|
||||
'panel_name' => $panel->name,
|
||||
'panel_code' => $panelCodeKey,
|
||||
'error' => 'مشتری پنل کد دیگری دارد — دستی تصمیم بگیرید',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$panel->moshtari_code = (int) $codeKey;
|
||||
$panel->save();
|
||||
$maps['by_code'][$codeKey] = $panel;
|
||||
$toSync[] = $panel;
|
||||
$linked[] = [
|
||||
'code' => (int) $codeKey,
|
||||
'name' => $panel->name,
|
||||
'tel' => $phone,
|
||||
'panel_id' => (int) $panel->id,
|
||||
];
|
||||
}
|
||||
|
||||
if ($toSync !== []) {
|
||||
try {
|
||||
app(TahesabInboundWalletService::class)->syncAfterImport($toSync);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning('Tahesab link-by-phone wallet sync failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'linked' => count($linked),
|
||||
'conflicts' => count($conflicts),
|
||||
'linked_rows' => $linked,
|
||||
'conflict_rows' => $conflicts,
|
||||
'message' => count($linked)
|
||||
? (count($linked).' کد روی مشتری پنل با شماره یکسان نشست')
|
||||
: (count($conflicts) ? 'کد یکسانی برای لینک خودکار نبود' : 'شماره یکسانی برای لینک پیدا نشد'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function previewCustomerWallet(Request $request, $customerId): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
if (! app(ConfigService::class)->isTahesabAccounting()) {
|
||||
return response()->json(['ok' => false, 'message' => 'حسابداری تهحساب فعال نیست'], 422);
|
||||
}
|
||||
|
||||
$customer = Customer::findOrFail($customerId);
|
||||
$code = (int) $request->input('code', $customer->moshtari_code);
|
||||
$result = app(TahesabInboundWalletService::class)->previewForCustomer($customer, $code);
|
||||
$needsDecision = (bool) ($result['needs_decision'] ?? false);
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
'needs_decision' => $needsDecision,
|
||||
'message' => $result['message'] ?? '',
|
||||
'result' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function mandeByCodes(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$codes = $request->input('codes', []);
|
||||
if (! is_array($codes) || $codes === []) {
|
||||
return response()->json(['ok' => true, 'mandes' => []]);
|
||||
}
|
||||
if (count($codes) > 50) {
|
||||
return response()->json(['ok' => false, 'message' => 'حداکثر ۵۰ کد در هر درخواست'], 422);
|
||||
}
|
||||
|
||||
$mandes = app(TahesabInboundWalletService::class)->fetchMandeByCodes($codes);
|
||||
$out = [];
|
||||
foreach ($mandes as $code => $row) {
|
||||
$out[(string) $code] = [
|
||||
'gold' => $row['gold'] ?? 0,
|
||||
'cash' => $row['cash'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true, 'mandes' => $out]);
|
||||
}
|
||||
|
||||
public function syncPanelToTahesab(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$customers = Customer::whereNull('deleted_at')
|
||||
@@ -494,8 +741,8 @@ class TahesabController extends Controller
|
||||
|
||||
$overwrite = $request->boolean('overwrite');
|
||||
$keep = $request->boolean('keep');
|
||||
// اگر صراحتاً overwrite/keep نباشد → از ادمین بپرس وقتی تداخل هست
|
||||
$askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep);
|
||||
$alwaysAsk = $request->boolean('always_ask');
|
||||
$askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep && ! $alwaysAsk);
|
||||
|
||||
$result = app(TahesabInboundWalletService::class)->syncWalletFromTahesab(
|
||||
$customer,
|
||||
@@ -504,6 +751,7 @@ class TahesabController extends Controller
|
||||
'overwrite' => $overwrite,
|
||||
'keep' => $keep,
|
||||
'ask_if_conflict' => $askIfConflict,
|
||||
'always_ask' => $alwaysAsk,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
use App\Models\Zarin;
|
||||
use App\Services\AccountingProductDocMapper;
|
||||
use App\Services\ConfigService;
|
||||
use App\Services\ZarinInboundWalletService;
|
||||
use App\Services\ZarinSyncService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Cache\LockTimeoutException;
|
||||
@@ -1056,20 +1058,14 @@ class ZarinController extends Controller
|
||||
}
|
||||
|
||||
$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();
|
||||
$maps = $this->panelAccountingMaps();
|
||||
$walletService = app(ZarinInboundWalletService::class);
|
||||
|
||||
$zarinOnlyList = [];
|
||||
$phoneMatches = [];
|
||||
$phoneConflicts = [];
|
||||
$withPhone = 0;
|
||||
$withoutPhone = 0;
|
||||
foreach ($zarinCustomers as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
@@ -1077,21 +1073,54 @@ class ZarinController extends Controller
|
||||
if (count($debugSample) < 3) {
|
||||
$debugSample[] = $item;
|
||||
}
|
||||
$code = $item['idHesab'] ?? $item['Code'] ?? $item['code'] ?? $item['id'] ?? null;
|
||||
if ($code !== null && isset($panelCodes[(string) $code])) {
|
||||
continue;
|
||||
}
|
||||
$code = $this->extractZarinHesabId($item) ?? ($item['idHesab'] ?? $item['Code'] ?? $item['code'] ?? $item['id'] ?? null);
|
||||
$codeKey = $this->accountingCodeKey($code);
|
||||
$phone = $this->extractZarinPhone($item);
|
||||
$tel = ltrim($phone, '0');
|
||||
if ($tel && isset($panelMobiles[$tel])) {
|
||||
$hasPhone = Customer::normalizeMobile($phone) !== null;
|
||||
if ($hasPhone) {
|
||||
$withPhone++;
|
||||
} else {
|
||||
$withoutPhone++;
|
||||
}
|
||||
|
||||
if ($codeKey !== null && isset($maps['by_code'][$codeKey])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mobileKey = Customer::normalizeMobile($phone);
|
||||
if ($mobileKey && isset($maps['by_mobile'][$mobileKey])) {
|
||||
$panel = $maps['by_mobile'][$mobileKey];
|
||||
$panelCodeKey = $this->accountingCodeKey($panel->moshtari_code);
|
||||
$row = [
|
||||
'code' => $codeKey !== null ? (int) $codeKey : null,
|
||||
'name' => $this->extractZarinName($item),
|
||||
'tel' => $phone,
|
||||
'panel_id' => (int) $panel->id,
|
||||
'panel_name' => $panel->name,
|
||||
'panel_code' => $panelCodeKey,
|
||||
];
|
||||
if ($panelCodeKey && $panelCodeKey !== $codeKey) {
|
||||
$phoneConflicts[] = $row;
|
||||
} else {
|
||||
$phoneMatches[] = $row;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($codeKey === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mande = $walletService->mandeFromAccountingRow($item);
|
||||
$zarinOnlyList[] = [
|
||||
'code' => $code,
|
||||
'code' => (int) $codeKey,
|
||||
'name' => $this->extractZarinName($item),
|
||||
'tel' => $phone,
|
||||
'gid' => $item['GID'] ?? null,
|
||||
'group' => $this->extractZarinGroup($item),
|
||||
'has_phone' => $hasPhone,
|
||||
'gold' => $mande['gold'] ?? null,
|
||||
'cash' => $mande['cash'] ?? null,
|
||||
];
|
||||
}
|
||||
$zarinOnlyCount = count($zarinOnlyList);
|
||||
@@ -1109,6 +1138,15 @@ class ZarinController extends Controller
|
||||
'panel_only' => $panelOnly,
|
||||
'zarin_total' => $zarinTotal,
|
||||
'zarin_only_count' => $zarinOnlyCount,
|
||||
'zarin_with_phone_count' => $include ? ($withPhone ?? 0) : null,
|
||||
'zarin_without_phone_count' => $include ? ($withoutPhone ?? 0) : null,
|
||||
'zarin_importable_count' => $include
|
||||
? count(array_filter($zarinOnlyList ?? [], fn ($r) => ! empty($r['has_phone'])))
|
||||
: null,
|
||||
'phone_match_count' => $include ? count($phoneMatches ?? []) : null,
|
||||
'phone_conflict_count' => $include ? count($phoneConflicts ?? []) : null,
|
||||
'phone_matches' => $phoneMatches ?? [],
|
||||
'phone_conflicts' => $phoneConflicts ?? [],
|
||||
'zarin_fetch_error' => $zarinFetchError,
|
||||
'zarin_http_status' => $zarinHttpStatus,
|
||||
'zarin_group_filter' => $zarinGroupFilter,
|
||||
@@ -1166,6 +1204,46 @@ class ZarinController extends Controller
|
||||
return null;
|
||||
}
|
||||
|
||||
private function accountingCodeKey(mixed $code): ?string
|
||||
{
|
||||
if ($code === null || $code === '' || $code === false) {
|
||||
return null;
|
||||
}
|
||||
$s = trim((string) $code);
|
||||
if ($s === '' || $s === '0' || $s === '-') {
|
||||
return null;
|
||||
}
|
||||
if (is_numeric($s)) {
|
||||
$n = (int) $s;
|
||||
|
||||
return $n > 0 ? (string) $n : null;
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{by_code: array<string, Customer>, by_mobile: array<string, Customer>}
|
||||
*/
|
||||
private function panelAccountingMaps(): array
|
||||
{
|
||||
$byCode = [];
|
||||
$byMobile = [];
|
||||
$rows = Customer::whereNull('deleted_at')->get(['id', 'name', 'mobile', 'moshtari_code', 'type']);
|
||||
foreach ($rows as $row) {
|
||||
$codeKey = $this->accountingCodeKey($row->moshtari_code);
|
||||
if ($codeKey !== null) {
|
||||
$byCode[$codeKey] = $row;
|
||||
}
|
||||
$mobile = Customer::normalizeMobile((string) ($row->mobile ?? ''));
|
||||
if ($mobile !== null) {
|
||||
$byMobile[$mobile] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return ['by_code' => $byCode, 'by_mobile' => $byMobile];
|
||||
}
|
||||
|
||||
/** حساب از قبل ساختهشده در زرین — با موبایل، تا addhesab دوم نسازیم */
|
||||
private function findExistingZarinHesabId(?string $mobile): ?int
|
||||
{
|
||||
@@ -1261,6 +1339,8 @@ class ZarinController extends Controller
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
$errors = [];
|
||||
/** @var list<Customer> $importedForWallet */
|
||||
$importedForWallet = [];
|
||||
|
||||
foreach ($validated['customers'] as $item) {
|
||||
$mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? '';
|
||||
@@ -1271,7 +1351,16 @@ class ZarinController extends Controller
|
||||
$fullName = 'مشتری زرین ' . $code;
|
||||
}
|
||||
|
||||
// موبایل برای خیلی از حسابهای زرین خالی است (مثل تستی هوم) — مانع import نیست
|
||||
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;
|
||||
@@ -1300,6 +1389,7 @@ class ZarinController extends Controller
|
||||
if ($changed) {
|
||||
$existing->save();
|
||||
}
|
||||
$importedForWallet[] = $existing;
|
||||
$success++;
|
||||
continue;
|
||||
}
|
||||
@@ -1318,6 +1408,7 @@ class ZarinController extends Controller
|
||||
$byMobile->password = $mobile;
|
||||
}
|
||||
$byMobile->save();
|
||||
$importedForWallet[] = $byMobile;
|
||||
$success++;
|
||||
continue;
|
||||
}
|
||||
@@ -1345,15 +1436,14 @@ class ZarinController extends Controller
|
||||
$customer->fname = $fullName;
|
||||
$customer->lname = '';
|
||||
$customer->name = $fullName;
|
||||
$customer->mobile = $mobile !== '' ? $mobile : null;
|
||||
$customer->mobile = $mobile;
|
||||
$customer->active = true;
|
||||
$customer->moshtari_code = $item['code'];
|
||||
$customer->referral_id = $referralId;
|
||||
$customer->type = 2;
|
||||
if ($mobile !== '') {
|
||||
$customer->password = $mobile;
|
||||
}
|
||||
$customer->password = $mobile;
|
||||
$customer->save();
|
||||
$importedForWallet[] = $customer;
|
||||
$success++;
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
@@ -1367,6 +1457,17 @@ class ZarinController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($importedForWallet !== []) {
|
||||
try {
|
||||
app(ZarinInboundWalletService::class)->syncAfterImport($importedForWallet);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Zarin import wallet sync batch failed', [
|
||||
'count' => count($importedForWallet),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'success' => $success,
|
||||
@@ -1375,6 +1476,260 @@ class ZarinController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function linkMatchingPhones(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$fetched = $this->fetchAllZarinCustomers();
|
||||
$rows = $fetched['rows'] ?? [];
|
||||
$groupFilter = $this->zarinCustomerGroupName();
|
||||
if ($groupFilter !== '' && $rows !== []) {
|
||||
$rows = array_values(array_filter($rows, function ($item) use ($groupFilter) {
|
||||
if (! is_array($item)) {
|
||||
return false;
|
||||
}
|
||||
$g = $this->extractZarinGroup($item);
|
||||
|
||||
return $g !== '' && $this->zarinGroupNamesMatch($g, $groupFilter);
|
||||
}));
|
||||
}
|
||||
|
||||
$maps = $this->panelAccountingMaps();
|
||||
$linked = [];
|
||||
$conflicts = [];
|
||||
$toSync = [];
|
||||
|
||||
foreach ($rows as $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$code = $this->extractZarinHesabId($item);
|
||||
$codeKey = $this->accountingCodeKey($code);
|
||||
if ($codeKey === null) {
|
||||
continue;
|
||||
}
|
||||
$phone = $this->extractZarinPhone($item);
|
||||
$mobileKey = Customer::normalizeMobile($phone);
|
||||
if ($mobileKey === null || ! isset($maps['by_mobile'][$mobileKey])) {
|
||||
continue;
|
||||
}
|
||||
$panel = $maps['by_mobile'][$mobileKey];
|
||||
$panelCodeKey = $this->accountingCodeKey($panel->moshtari_code);
|
||||
|
||||
if (isset($maps['by_code'][$codeKey]) && (int) $maps['by_code'][$codeKey]->id !== (int) $panel->id) {
|
||||
$conflicts[] = [
|
||||
'code' => (int) $codeKey,
|
||||
'name' => $this->extractZarinName($item),
|
||||
'tel' => $phone,
|
||||
'panel_id' => (int) $panel->id,
|
||||
'panel_name' => $panel->name,
|
||||
'error' => 'این کد زرین روی مشتری دیگری در پنل است',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
if ($panelCodeKey === $codeKey) {
|
||||
continue;
|
||||
}
|
||||
if ($panelCodeKey) {
|
||||
$conflicts[] = [
|
||||
'code' => (int) $codeKey,
|
||||
'name' => $this->extractZarinName($item),
|
||||
'tel' => $phone,
|
||||
'panel_id' => (int) $panel->id,
|
||||
'panel_name' => $panel->name,
|
||||
'panel_code' => $panelCodeKey,
|
||||
'error' => 'مشتری پنل کد دیگری دارد — دستی تصمیم بگیرید',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$panel->moshtari_code = (int) $codeKey;
|
||||
$panel->save();
|
||||
$maps['by_code'][$codeKey] = $panel;
|
||||
$toSync[] = $panel;
|
||||
$linked[] = [
|
||||
'code' => (int) $codeKey,
|
||||
'name' => $panel->name,
|
||||
'tel' => $phone,
|
||||
'panel_id' => (int) $panel->id,
|
||||
];
|
||||
}
|
||||
|
||||
if ($toSync !== []) {
|
||||
try {
|
||||
app(ZarinInboundWalletService::class)->syncAfterImport($toSync);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Zarin link-by-phone wallet sync failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'linked' => count($linked),
|
||||
'conflicts' => count($conflicts),
|
||||
'linked_rows' => $linked,
|
||||
'conflict_rows' => $conflicts,
|
||||
'message' => count($linked)
|
||||
? (count($linked).' کد روی مشتری پنل با شماره یکسان نشست')
|
||||
: (count($conflicts) ? 'کد یکسانی برای لینک خودکار نبود' : 'شماره یکسانی برای لینک پیدا نشد'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function previewCustomerWallet(Request $request, $customerId): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
if (! app(ConfigService::class)->isZarinAccounting()) {
|
||||
return response()->json(['ok' => false, 'message' => 'حسابداری زرین فعال نیست'], 422);
|
||||
}
|
||||
|
||||
$customer = Customer::findOrFail($customerId);
|
||||
$code = (int) $request->input('code', $customer->moshtari_code);
|
||||
$result = app(ZarinInboundWalletService::class)->previewForCustomer($customer, $code);
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
'needs_decision' => (bool) ($result['needs_decision'] ?? false),
|
||||
'message' => $result['message'] ?? '',
|
||||
'result' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function syncCustomerWallet(Request $request, $customerId)
|
||||
{
|
||||
if (! app(ConfigService::class)->isZarinAccounting()) {
|
||||
return response()->json(['ok' => false, 'message' => 'حسابداری زرین فعال نیست'], 422);
|
||||
}
|
||||
|
||||
$customer = Customer::findOrFail($customerId);
|
||||
if ((int) ($customer->moshtari_code ?? 0) <= 0) {
|
||||
return response()->json(['ok' => false, 'message' => 'این مشتری کد زرین ندارد'], 422);
|
||||
}
|
||||
|
||||
$overwrite = $request->boolean('overwrite');
|
||||
$keep = $request->boolean('keep');
|
||||
$alwaysAsk = $request->boolean('always_ask');
|
||||
$askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep && ! $alwaysAsk);
|
||||
|
||||
$result = app(ZarinInboundWalletService::class)->syncWalletFromZarin(
|
||||
$customer,
|
||||
null,
|
||||
[
|
||||
'overwrite' => $overwrite,
|
||||
'keep' => $keep,
|
||||
'ask_if_conflict' => $askIfConflict,
|
||||
'always_ask' => $alwaysAsk,
|
||||
]
|
||||
);
|
||||
|
||||
$needsDecision = (bool) ($result['needs_decision'] ?? false);
|
||||
$applied = (bool) ($result['applied'] ?? false);
|
||||
$skipped = (bool) ($result['skipped'] ?? false);
|
||||
$same = ! $applied && ! $needsDecision && ! $skipped
|
||||
&& str_contains((string) ($result['message'] ?? ''), 'یکسان');
|
||||
|
||||
return response()->json([
|
||||
'ok' => $applied || $skipped || $same,
|
||||
'needs_decision' => $needsDecision,
|
||||
'message' => $result['message'] ?? '',
|
||||
'result' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function syncAllWallets(Request $request)
|
||||
{
|
||||
if (! app(ConfigService::class)->isZarinAccounting()) {
|
||||
return response()->json(['ok' => false, 'message' => 'حسابداری زرین فعال نیست'], 422);
|
||||
}
|
||||
|
||||
$limit = max(1, min(50, (int) $request->input('limit', 20)));
|
||||
$offset = max(0, (int) $request->input('offset', 0));
|
||||
$overwrite = $request->boolean('overwrite');
|
||||
|
||||
$base = Customer::query()
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('moshtari_code')
|
||||
->where('moshtari_code', '!=', '')
|
||||
->where('moshtari_code', '!=', '0');
|
||||
|
||||
$total = (clone $base)->count();
|
||||
$customers = (clone $base)
|
||||
->orderBy('id')
|
||||
->offset($offset)
|
||||
->limit($limit)
|
||||
->get(['id', 'name', 'mobile', 'moshtari_code', 'type']);
|
||||
|
||||
$walletService = app(ZarinInboundWalletService::class);
|
||||
$codes = $customers->pluck('moshtari_code')->all();
|
||||
$mandes = $walletService->fetchMandeByCodes($codes);
|
||||
|
||||
$success = 0;
|
||||
$changed = 0;
|
||||
$needsDecision = 0;
|
||||
$failed = 0;
|
||||
$errors = [];
|
||||
$decisions = [];
|
||||
|
||||
foreach ($customers as $customer) {
|
||||
try {
|
||||
$code = (string) ((int) $customer->moshtari_code);
|
||||
$result = $walletService->syncWalletFromZarin(
|
||||
$customer,
|
||||
$mandes[$code] ?? null,
|
||||
[
|
||||
'overwrite' => $overwrite,
|
||||
'ask_if_conflict' => ! $overwrite,
|
||||
]
|
||||
);
|
||||
|
||||
if ($result['needs_decision'] ?? false) {
|
||||
$needsDecision++;
|
||||
$decisions[] = [
|
||||
'id' => $customer->id,
|
||||
'name' => $customer->name,
|
||||
'mobile' => $customer->mobile,
|
||||
'app_gold' => $result['app_gold'] ?? null,
|
||||
'app_cash' => $result['app_cash'] ?? null,
|
||||
'tahesab_gold' => $result['zarin_gold'] ?? $result['tahesab_gold'] ?? null,
|
||||
'tahesab_cash' => $result['zarin_cash'] ?? $result['tahesab_cash'] ?? null,
|
||||
'message' => $result['message'] ?? '',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$success++;
|
||||
if ($result['applied'] ?? false) {
|
||||
$changed++;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
$errors[] = [
|
||||
'id' => $customer->id,
|
||||
'name' => $customer->name,
|
||||
'mobile' => $customer->mobile,
|
||||
'error' => $e->getMessage() !== '' ? $e->getMessage() : 'خطای ناشناخته',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$processed = $customers->count();
|
||||
$nextOffset = $offset + $processed;
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'message' => $total === 0
|
||||
? 'مشتری دارای کد زرین یافت نشد'
|
||||
: "دستهٔ {$offset} تا {$nextOffset} از {$total}",
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'processed' => $processed,
|
||||
'next_offset' => $nextOffset,
|
||||
'done' => $nextOffset >= $total,
|
||||
'success' => $success,
|
||||
'changed' => $changed,
|
||||
'needs_decision' => $needsDecision,
|
||||
'failed' => $failed,
|
||||
'errors' => $errors,
|
||||
'decisions' => $decisions,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* حذف مشتریانِ خرابِ واردشده از زرین (نام خالی) برای پاکسازی import ناقص.
|
||||
* با dry_run=1 فقط تعداد و نمونه را نشان میدهد و چیزی حذف نمیکند.
|
||||
|
||||
@@ -19,6 +19,8 @@ use Rooyesh\Wallet\interfaces\WalletInterface;
|
||||
* overwrite=true → اختلاف را اعمال کن
|
||||
* keep=true → موجودی اپ را دست نزن
|
||||
* ask_if_conflict → اگر اپ مانده دارد و با تهحساب فرق دارد، بدون نوشتن برگردان
|
||||
* always_ask → هر اختلافی (حتی کیف خالی) بدون نوشتن برای تأیید ادمین برمیگردد
|
||||
* جمع دو مانده هرگز انجام نمیشود تا موجودی دو برابر نشود.
|
||||
*/
|
||||
class TahesabInboundWalletService
|
||||
{
|
||||
@@ -67,7 +69,7 @@ class TahesabInboundWalletService
|
||||
|
||||
/**
|
||||
* @param array{gold?: float, cash?: float}|null $mande
|
||||
* @param array{overwrite?: bool, keep?: bool, ask_if_conflict?: bool} $options
|
||||
* @param array{overwrite?: bool, keep?: bool, ask_if_conflict?: bool, always_ask?: bool} $options
|
||||
* @return array{
|
||||
* applied: bool,
|
||||
* needs_decision?: bool,
|
||||
@@ -86,6 +88,7 @@ class TahesabInboundWalletService
|
||||
$overwrite = (bool) ($options['overwrite'] ?? false);
|
||||
$keep = (bool) ($options['keep'] ?? false);
|
||||
$askIfConflict = (bool) ($options['ask_if_conflict'] ?? false);
|
||||
$alwaysAsk = (bool) ($options['always_ask'] ?? false);
|
||||
|
||||
$code = (int) ($customer->moshtari_code ?? 0);
|
||||
if ($code <= 0) {
|
||||
@@ -101,31 +104,11 @@ class TahesabInboundWalletService
|
||||
return ['applied' => false, 'message' => 'مانده از تهحساب دریافت نشد'];
|
||||
}
|
||||
|
||||
$goldInv = (float) ($mande['gold'] ?? 0);
|
||||
$rialInv = (float) ($mande['cash'] ?? 0);
|
||||
|
||||
$ownerType = (int) ($customer->type ?? 2);
|
||||
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
|
||||
$wallet = $walletInterface->wallet;
|
||||
|
||||
$mainGold = $this->walletBalance
|
||||
->accountingVisibleMainWalletGold((int) $customer->id, $ownerType);
|
||||
$mainCash = (float) ($wallet->cash ?? 0);
|
||||
|
||||
$goldDiff = $goldInv - $mainGold;
|
||||
$rialDiff = $rialInv - $mainCash;
|
||||
|
||||
$goldSame = abs($goldDiff) < self::EPSILON_GOLD;
|
||||
$rialSame = abs($rialDiff) < self::EPSILON_CASH;
|
||||
|
||||
$comparison = [
|
||||
'app_gold' => $mainGold,
|
||||
'app_cash' => $mainCash,
|
||||
'tahesab_gold' => $goldInv,
|
||||
'tahesab_cash' => $rialInv,
|
||||
'gold_delta' => $goldDiff,
|
||||
'rial_delta' => $rialDiff,
|
||||
];
|
||||
$compared = $this->compareWithWallet($customer, $mande);
|
||||
$comparison = $compared['comparison'];
|
||||
$goldSame = $compared['gold_same'];
|
||||
$rialSame = $compared['rial_same'];
|
||||
$appHasBalance = $compared['app_has_balance'];
|
||||
|
||||
if ($goldSame && $rialSame) {
|
||||
return array_merge($comparison, [
|
||||
@@ -142,11 +125,11 @@ class TahesabInboundWalletService
|
||||
]);
|
||||
}
|
||||
|
||||
$appHasBalance = abs($mainGold) >= self::EPSILON_GOLD
|
||||
|| abs($mainCash) >= self::EPSILON_CASH;
|
||||
|
||||
// اگر اپ مانده دارد و با تهحساب فرق دارد و ادمین هنوز تصمیم نگرفته
|
||||
if ($askIfConflict && ! $overwrite && $appHasBalance) {
|
||||
$needsAsk = ! $overwrite && (
|
||||
($alwaysAsk)
|
||||
|| ($askIfConflict && $appHasBalance)
|
||||
);
|
||||
if ($needsAsk) {
|
||||
return array_merge($comparison, [
|
||||
'applied' => false,
|
||||
'needs_decision' => true,
|
||||
@@ -154,6 +137,11 @@ class TahesabInboundWalletService
|
||||
]);
|
||||
}
|
||||
|
||||
$goldDiff = (float) $comparison['gold_delta'];
|
||||
$rialDiff = (float) $comparison['rial_delta'];
|
||||
$ownerType = (int) ($customer->type ?? 2);
|
||||
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
|
||||
|
||||
$input = [
|
||||
'title' => 'تطبیق ماندهٔ تهحساب',
|
||||
'note' => 'tahesab-mande:'.$code,
|
||||
@@ -186,8 +174,8 @@ class TahesabInboundWalletService
|
||||
Log::info('Tahesab mande applied to wallet', [
|
||||
'customer_id' => (int) $customer->id,
|
||||
'moshtari_code' => $code,
|
||||
'gold_target' => $goldInv,
|
||||
'cash_target' => $rialInv,
|
||||
'gold_target' => $comparison['tahesab_gold'],
|
||||
'cash_target' => $comparison['tahesab_cash'],
|
||||
'gold_delta' => $goldDiff,
|
||||
'rial_delta' => $rialDiff,
|
||||
'overwrite' => $overwrite,
|
||||
@@ -199,6 +187,94 @@ class TahesabInboundWalletService
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* مقایسه بدون نوشتن — برای تأیید ادمین قبل از ست کردن کد.
|
||||
*
|
||||
* @return array{
|
||||
* applied: bool,
|
||||
* needs_decision?: bool,
|
||||
* ok?: bool,
|
||||
* app_gold?: float,
|
||||
* app_cash?: float,
|
||||
* tahesab_gold?: float,
|
||||
* tahesab_cash?: float,
|
||||
* gold_delta?: float,
|
||||
* rial_delta?: float,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
public function previewForCustomer(Customer $customer, int $code): array
|
||||
{
|
||||
if ($code <= 0) {
|
||||
return ['applied' => false, 'ok' => false, 'message' => 'کد تهحساب نامعتبر است'];
|
||||
}
|
||||
|
||||
$all = $this->fetchMandeByCodes([$code]);
|
||||
$mande = $all[(string) $code] ?? null;
|
||||
if ($mande === null) {
|
||||
return ['applied' => false, 'ok' => false, 'message' => 'مانده از تهحساب دریافت نشد'];
|
||||
}
|
||||
|
||||
$compared = $this->compareWithWallet($customer, $mande);
|
||||
$same = $compared['gold_same'] && $compared['rial_same'];
|
||||
|
||||
return array_merge($compared['comparison'], [
|
||||
'applied' => false,
|
||||
'ok' => true,
|
||||
'needs_decision' => ! $same,
|
||||
'message' => $same
|
||||
? 'مانده با تهحساب یکسان است'
|
||||
: 'موجودی اپ با تهحساب فرق دارد — تصمیم ادمین لازم است',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $row */
|
||||
public function mandeFromAccountingRow(array $row): ?array
|
||||
{
|
||||
return $this->parseMandeRow($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{gold?: float, cash?: float} $mande
|
||||
* @return array{
|
||||
* comparison: array{app_gold: float, app_cash: float, tahesab_gold: float, tahesab_cash: float, gold_delta: float, rial_delta: float},
|
||||
* gold_same: bool,
|
||||
* rial_same: bool,
|
||||
* app_has_balance: bool
|
||||
* }
|
||||
*/
|
||||
private function compareWithWallet(Customer $customer, array $mande): array
|
||||
{
|
||||
$goldInv = (float) ($mande['gold'] ?? 0);
|
||||
$rialInv = (float) ($mande['cash'] ?? 0);
|
||||
|
||||
$ownerType = (int) ($customer->type ?? 2);
|
||||
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
|
||||
$wallet = $walletInterface->wallet;
|
||||
|
||||
$mainGold = $this->walletBalance
|
||||
->accountingVisibleMainWalletGold((int) $customer->id, $ownerType);
|
||||
$mainCash = (float) ($wallet->cash ?? 0);
|
||||
|
||||
$goldDiff = $goldInv - $mainGold;
|
||||
$rialDiff = $rialInv - $mainCash;
|
||||
|
||||
return [
|
||||
'comparison' => [
|
||||
'app_gold' => $mainGold,
|
||||
'app_cash' => $mainCash,
|
||||
'tahesab_gold' => $goldInv,
|
||||
'tahesab_cash' => $rialInv,
|
||||
'gold_delta' => $goldDiff,
|
||||
'rial_delta' => $rialDiff,
|
||||
],
|
||||
'gold_same' => abs($goldDiff) < self::EPSILON_GOLD,
|
||||
'rial_same' => abs($rialDiff) < self::EPSILON_CASH,
|
||||
'app_has_balance' => abs($mainGold) >= self::EPSILON_GOLD
|
||||
|| abs($mainCash) >= self::EPSILON_CASH,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* بعد از import: اگر کیف خالی باشد مانده را بنشان؛ اگر مانده دارد صبر کن تا ادمین تصمیم بگیرد.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Customer;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Rooyesh\Wallet\interfaces\TransactionInterface;
|
||||
use Rooyesh\Wallet\interfaces\WalletInterface;
|
||||
|
||||
/**
|
||||
* ماندهٔ طلا/ریال زرین → کیف اپ.
|
||||
*
|
||||
* منبع: listhesabha — فقط فیلدهای صریح طلا/ریال.
|
||||
* اگر فیلد مبهم باشد (مثلاً فقط mande) چیزی نوشته نمیشود.
|
||||
*
|
||||
* سیاست: overwrite / keep / ask_if_conflict / always_ask — مثل تهحساب.
|
||||
* جمع دو مانده هرگز انجام نمیشود.
|
||||
*/
|
||||
class ZarinInboundWalletService
|
||||
{
|
||||
public const TX_TYPE_ZARIN = 24;
|
||||
|
||||
private const EPSILON_GOLD = 0.0005;
|
||||
|
||||
private const EPSILON_CASH = 1.0;
|
||||
|
||||
/** @var array<string, array{gold: float, cash: float}>|null */
|
||||
private ?array $mandeIndex = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly ConfigService $config,
|
||||
private readonly WalletBroadcastService $walletBroadcast,
|
||||
private readonly WalletBalanceService $walletBalance,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int|string> $codes
|
||||
* @return array<string, array{gold: float, cash: float}>
|
||||
*/
|
||||
public function fetchMandeByCodes(array $codes): array
|
||||
{
|
||||
$index = $this->mandeIndex();
|
||||
$out = [];
|
||||
foreach ($codes as $code) {
|
||||
$key = (string) ((int) $code);
|
||||
if ($key !== '0' && isset($index[$key])) {
|
||||
$out[$key] = $index[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{overwrite?: bool, keep?: bool, ask_if_conflict?: bool, always_ask?: bool} $options
|
||||
* @return array{
|
||||
* applied: bool,
|
||||
* needs_decision?: bool,
|
||||
* skipped?: bool,
|
||||
* gold_delta?: float,
|
||||
* rial_delta?: float,
|
||||
* app_gold?: float,
|
||||
* app_cash?: float,
|
||||
* zarin_gold?: float,
|
||||
* zarin_cash?: float,
|
||||
* tahesab_gold?: float,
|
||||
* tahesab_cash?: float,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
public function syncWalletFromZarin(Customer $customer, ?array $mande = null, array $options = []): array
|
||||
{
|
||||
$overwrite = (bool) ($options['overwrite'] ?? false);
|
||||
$keep = (bool) ($options['keep'] ?? false);
|
||||
$askIfConflict = (bool) ($options['ask_if_conflict'] ?? false);
|
||||
$alwaysAsk = (bool) ($options['always_ask'] ?? false);
|
||||
|
||||
$code = (int) ($customer->moshtari_code ?? 0);
|
||||
if ($code <= 0) {
|
||||
return ['applied' => false, 'message' => 'کد زرین ندارد'];
|
||||
}
|
||||
|
||||
if ($mande === null) {
|
||||
$all = $this->fetchMandeByCodes([$code]);
|
||||
$mande = $all[(string) $code] ?? null;
|
||||
}
|
||||
|
||||
if ($mande === null) {
|
||||
return ['applied' => false, 'message' => 'مانده طلا/ریال زرین برای این حساب خوانده نشد'];
|
||||
}
|
||||
|
||||
$compared = $this->compareWithWallet($customer, $mande);
|
||||
$comparison = $compared['comparison'];
|
||||
$goldSame = $compared['gold_same'];
|
||||
$rialSame = $compared['rial_same'];
|
||||
$appHasBalance = $compared['app_has_balance'];
|
||||
|
||||
if ($goldSame && $rialSame) {
|
||||
return array_merge($comparison, [
|
||||
'applied' => false,
|
||||
'message' => 'مانده با زرین یکسان است',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($keep) {
|
||||
return array_merge($comparison, [
|
||||
'applied' => false,
|
||||
'skipped' => true,
|
||||
'message' => 'موجودی اپ حفظ شد — مانده زرین اعمال نشد',
|
||||
]);
|
||||
}
|
||||
|
||||
$needsAsk = ! $overwrite && ($alwaysAsk || ($askIfConflict && $appHasBalance));
|
||||
if ($needsAsk) {
|
||||
return array_merge($comparison, [
|
||||
'applied' => false,
|
||||
'needs_decision' => true,
|
||||
'message' => 'موجودی اپ با زرین فرق دارد — تصمیم ادمین لازم است',
|
||||
]);
|
||||
}
|
||||
|
||||
$goldDiff = (float) $comparison['gold_delta'];
|
||||
$rialDiff = (float) $comparison['rial_delta'];
|
||||
$ownerType = (int) ($customer->type ?? 2);
|
||||
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
|
||||
|
||||
$input = [
|
||||
'title' => 'تطبیق ماندهٔ زرین',
|
||||
'note' => 'zarin-mande:'.$code,
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
];
|
||||
if (! $goldSame) {
|
||||
$input['gold'] = abs($goldDiff);
|
||||
}
|
||||
if (! $rialSame) {
|
||||
$input['cash'] = (int) round(abs($rialDiff));
|
||||
}
|
||||
|
||||
new TransactionInterface(
|
||||
$walletInterface,
|
||||
$input,
|
||||
self::TX_TYPE_ZARIN,
|
||||
$goldDiff > 0,
|
||||
$rialDiff > 0,
|
||||
0,
|
||||
[],
|
||||
true
|
||||
);
|
||||
|
||||
$this->walletBroadcast->broadcastForCustomer((int) $customer->id, $ownerType);
|
||||
|
||||
Log::info('Zarin mande applied to wallet', [
|
||||
'customer_id' => (int) $customer->id,
|
||||
'moshtari_code' => $code,
|
||||
'gold_target' => $comparison['zarin_gold'],
|
||||
'cash_target' => $comparison['zarin_cash'],
|
||||
'gold_delta' => $goldDiff,
|
||||
'rial_delta' => $rialDiff,
|
||||
'overwrite' => $overwrite,
|
||||
]);
|
||||
|
||||
return array_merge($comparison, [
|
||||
'applied' => true,
|
||||
'message' => 'مانده با زرین همتراز شد',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* applied: bool,
|
||||
* needs_decision?: bool,
|
||||
* ok?: bool,
|
||||
* app_gold?: float,
|
||||
* app_cash?: float,
|
||||
* zarin_gold?: float,
|
||||
* zarin_cash?: float,
|
||||
* tahesab_gold?: float,
|
||||
* tahesab_cash?: float,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
public function previewForCustomer(Customer $customer, int $code): array
|
||||
{
|
||||
if ($code <= 0) {
|
||||
return ['applied' => false, 'ok' => false, 'message' => 'کد زرین نامعتبر است'];
|
||||
}
|
||||
|
||||
$mande = $this->fetchMandeByCodes([$code])[(string) $code] ?? null;
|
||||
if ($mande === null) {
|
||||
return ['applied' => false, 'ok' => false, 'message' => 'مانده طلا/ریال زرین برای این حساب خوانده نشد'];
|
||||
}
|
||||
|
||||
$compared = $this->compareWithWallet($customer, $mande);
|
||||
$same = $compared['gold_same'] && $compared['rial_same'];
|
||||
|
||||
return array_merge($compared['comparison'], [
|
||||
'applied' => false,
|
||||
'ok' => true,
|
||||
'needs_decision' => ! $same,
|
||||
'message' => $same
|
||||
? 'مانده با زرین یکسان است'
|
||||
: 'موجودی اپ با زرین فرق دارد — تصمیم ادمین لازم است',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $row */
|
||||
public function mandeFromAccountingRow(array $row): ?array
|
||||
{
|
||||
return $this->parseMandeRow($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Customer> $customers
|
||||
*/
|
||||
public function syncAfterImport(array $customers): void
|
||||
{
|
||||
$customers = array_values(array_filter($customers, fn ($c) => $c instanceof Customer));
|
||||
if ($customers === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$codes = [];
|
||||
foreach ($customers as $customer) {
|
||||
$code = (int) ($customer->moshtari_code ?? 0);
|
||||
if ($code > 0) {
|
||||
$codes[] = $code;
|
||||
}
|
||||
}
|
||||
|
||||
$mandes = $this->fetchMandeByCodes($codes);
|
||||
|
||||
foreach ($customers as $customer) {
|
||||
try {
|
||||
$code = (string) ((int) ($customer->moshtari_code ?? 0));
|
||||
$this->syncWalletFromZarin(
|
||||
$customer,
|
||||
$mandes[$code] ?? null,
|
||||
['ask_if_conflict' => true]
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Zarin wallet sync after import failed', [
|
||||
'customer_id' => $customer->id,
|
||||
'moshtari_code' => $customer->moshtari_code,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{gold?: float, cash?: float} $mande
|
||||
* @return array{
|
||||
* comparison: array{app_gold: float, app_cash: float, zarin_gold: float, zarin_cash: float, tahesab_gold: float, tahesab_cash: float, gold_delta: float, rial_delta: float},
|
||||
* gold_same: bool,
|
||||
* rial_same: bool,
|
||||
* app_has_balance: bool
|
||||
* }
|
||||
*/
|
||||
private function compareWithWallet(Customer $customer, array $mande): array
|
||||
{
|
||||
$goldInv = (float) ($mande['gold'] ?? 0);
|
||||
$rialInv = (float) ($mande['cash'] ?? 0);
|
||||
$ownerType = (int) ($customer->type ?? 2);
|
||||
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
|
||||
$wallet = $walletInterface->wallet;
|
||||
$mainGold = $this->walletBalance
|
||||
->accountingVisibleMainWalletGold((int) $customer->id, $ownerType);
|
||||
$mainCash = (float) ($wallet->cash ?? 0);
|
||||
$goldDiff = $goldInv - $mainGold;
|
||||
$rialDiff = $rialInv - $mainCash;
|
||||
|
||||
return [
|
||||
'comparison' => [
|
||||
'app_gold' => $mainGold,
|
||||
'app_cash' => $mainCash,
|
||||
'zarin_gold' => $goldInv,
|
||||
'zarin_cash' => $rialInv,
|
||||
'tahesab_gold' => $goldInv,
|
||||
'tahesab_cash' => $rialInv,
|
||||
'gold_delta' => $goldDiff,
|
||||
'rial_delta' => $rialDiff,
|
||||
],
|
||||
'gold_same' => abs($goldDiff) < self::EPSILON_GOLD,
|
||||
'rial_same' => abs($rialDiff) < self::EPSILON_CASH,
|
||||
'app_has_balance' => abs($mainGold) >= self::EPSILON_GOLD
|
||||
|| abs($mainCash) >= self::EPSILON_CASH,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{gold: float, cash: float}>
|
||||
*/
|
||||
private function mandeIndex(): array
|
||||
{
|
||||
if ($this->mandeIndex !== null) {
|
||||
return $this->mandeIndex;
|
||||
}
|
||||
|
||||
$this->mandeIndex = [];
|
||||
foreach ($this->fetchListHesabha() as $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$id = $this->extractHesabId($item);
|
||||
if ($id === null) {
|
||||
continue;
|
||||
}
|
||||
$parsed = $this->parseMandeRow($item);
|
||||
if ($parsed !== null) {
|
||||
$this->mandeIndex[(string) $id] = $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->mandeIndex;
|
||||
}
|
||||
|
||||
/** @return list<array<string, mixed>> */
|
||||
private function fetchListHesabha(): array
|
||||
{
|
||||
$api = rtrim($this->config->getString('zarin_api', ''), '/');
|
||||
$token = trim($this->config->getString('zarin_token', ''));
|
||||
if ($api === '' || $token === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$url = $api.'/ZarrinService/accounting';
|
||||
|
||||
try {
|
||||
$response = Http::withoutVerifying()->timeout(45)->get($url, [
|
||||
'kind' => 'listhesabha',
|
||||
'token' => $token,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Zarin listhesabha for mande failed', ['error' => $e->getMessage()]);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$json = $response->json();
|
||||
if (! is_array($json)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = $json['data'] ?? $json['Data'] ?? [];
|
||||
if (! is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_is_list($data) ? $data : [$data];
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $item */
|
||||
private function extractHesabId(array $item): ?int
|
||||
{
|
||||
foreach (['idHesab', 'idhesab', 'Code', 'code', 'OK', 'id'] as $key) {
|
||||
if (! array_key_exists($key, $item)) {
|
||||
continue;
|
||||
}
|
||||
$value = $item[$key];
|
||||
if (is_numeric($value) && (int) $value > 0) {
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* فقط کلیدهای صریح طلا و ریال — فیلد مبهم mande را طلا فرض نمیکنیم.
|
||||
*
|
||||
* @param array<string, mixed> $row
|
||||
* @return array{gold: float, cash: float}|null
|
||||
*/
|
||||
private function parseMandeRow(array $row): ?array
|
||||
{
|
||||
$gold = $this->pickNumber($row, [
|
||||
'GoldInv', 'goldInv', 'tala', 'Tala', 'mandeTala', 'MandeTala',
|
||||
'mojodiTala', 'MojodiTala', 'vaznTala', 'VaznTala',
|
||||
]);
|
||||
$cash = $this->pickNumber($row, [
|
||||
'RialInv', 'rialInv', 'pool', 'Pool', 'mandeRial', 'MandeRial',
|
||||
'mojodiRial', 'MojodiRial', 'mandePool', 'MandePool',
|
||||
]);
|
||||
|
||||
$hasGoldKey = $this->hasAnyKey($row, [
|
||||
'GoldInv', 'goldInv', 'tala', 'Tala', 'mandeTala', 'MandeTala',
|
||||
'mojodiTala', 'MojodiTala', 'vaznTala', 'VaznTala',
|
||||
]);
|
||||
$hasCashKey = $this->hasAnyKey($row, [
|
||||
'RialInv', 'rialInv', 'pool', 'Pool', 'mandeRial', 'MandeRial',
|
||||
'mojodiRial', 'MojodiRial', 'mandePool', 'MandePool',
|
||||
]);
|
||||
|
||||
if (! $hasGoldKey && ! $hasCashKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'gold' => $gold,
|
||||
'cash' => $cash,
|
||||
];
|
||||
}
|
||||
|
||||
/** @param list<string> $keys */
|
||||
private function hasAnyKey(array $row, array $keys): bool
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (array_key_exists($key, $row) && $row[$key] !== null && $row[$key] !== '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param list<string> $keys */
|
||||
private function pickNumber(array $row, array $keys): float
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (! array_key_exists($key, $row) || $row[$key] === null || $row[$key] === '') {
|
||||
continue;
|
||||
}
|
||||
$s = str_replace([',', ' ', '٫', '/'], ['', '', '.', '.'], trim((string) $row[$key]));
|
||||
if (is_numeric($s)) {
|
||||
return (float) $s;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,37 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de
|
||||
SELECT 'montzar_last_product_log', '{}', 'لاگ محصول همکار', 1, 1, NULL, 'longtext'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'montzar_last_product_log' LIMIT 1);
|
||||
|
||||
-- هیرو لاگین دسکتاپ (تم VIP) — متنهای ستون چپ
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'login_hero_eyebrow', 'سرمایهگذاری هوشمند در', 'خط اول هیرو لاگین', 1, 1,
|
||||
'متن بالای عنوان طلا در ستون چپ صفحه ورود دسکتاپ (تم VIP).', 'text'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'login_hero_eyebrow' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'login_hero_title', 'طلای جهانی', 'عنوان هیرو لاگین', 1, 1,
|
||||
'عنوان طلایی ستون چپ صفحه ورود دسکتاپ (تم VIP).', 'text'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'login_hero_title' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'login_hero_feature_1', 'خرید و فروش آنلاین طلا', 'ویژگی ۱ هیرو لاگین', 1, 1,
|
||||
'اولین آیتم لیست ویژگیهای ستون چپ لاگین دسکتاپ.', 'text'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'login_hero_feature_1' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'login_hero_feature_2', 'قیمت لحظهای و دقیق', 'ویژگی ۲ هیرو لاگین', 1, 1,
|
||||
'دومین آیتم لیست ویژگیهای ستون چپ لاگین دسکتاپ.', 'text'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'login_hero_feature_2' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'login_hero_feature_3', 'امنیت بالا و قابل اطمینان', 'ویژگی ۳ هیرو لاگین', 1, 1,
|
||||
'سومین آیتم لیست ویژگیهای ستون چپ لاگین دسکتاپ.', 'text'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'login_hero_feature_3' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'login_hero_feature_4', 'پشتیبانی ۲۴/۷', 'ویژگی ۴ هیرو لاگین', 1, 1,
|
||||
'چهارمین آیتم لیست ویژگیهای ستون چپ لاگین دسکتاپ.', 'text'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'login_hero_feature_4' LIMIT 1);
|
||||
|
||||
-- قیمت zaarbox (اگر از قبل دارید value دست نخورده)
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'chart_use_local_data', 'false', 'نمودار از داده خود سایت', 1, 1,
|
||||
@@ -1708,6 +1739,12 @@ UPDATE `settings` SET `description` = 'تنظیم legacy — از price_provider
|
||||
|
||||
UPDATE `settings` SET `description` = 'متن کامل قوانین و مقررات استفاده از اپ که در صفحه قوانین به کاربر نشان داده میشود. HTML ساده مجاز است.' WHERE `key` = 'text_rule';
|
||||
UPDATE `settings` SET `description` = 'متن «درباره ما» شامل معرفی مجموعه، راههای ارتباطی و توضیح خدمات. در صفحه مربوطه در اپ کاربر نمایش داده میشود.' WHERE `key` = 'text_about';
|
||||
UPDATE `settings` SET `description` = 'متن بالای عنوان طلا در ستون چپ صفحه ورود دسکتاپ (تم VIP).' WHERE `key` = 'login_hero_eyebrow';
|
||||
UPDATE `settings` SET `description` = 'عنوان طلایی ستون چپ صفحه ورود دسکتاپ (تم VIP).' WHERE `key` = 'login_hero_title';
|
||||
UPDATE `settings` SET `description` = 'اولین آیتم لیست ویژگیهای ستون چپ لاگین دسکتاپ.' WHERE `key` = 'login_hero_feature_1';
|
||||
UPDATE `settings` SET `description` = 'دومین آیتم لیست ویژگیهای ستون چپ لاگین دسکتاپ.' WHERE `key` = 'login_hero_feature_2';
|
||||
UPDATE `settings` SET `description` = 'سومین آیتم لیست ویژگیهای ستون چپ لاگین دسکتاپ.' WHERE `key` = 'login_hero_feature_3';
|
||||
UPDATE `settings` SET `description` = 'چهارمین آیتم لیست ویژگیهای ستون چپ لاگین دسکتاپ.' WHERE `key` = 'login_hero_feature_4';
|
||||
UPDATE `settings` SET `description` = 'متن معرفی نرمافزار — در تنظیمات پنل ذخیره میشود (HTML یا چند خط متن).' WHERE `key` = 'about_software';
|
||||
UPDATE `settings` SET `description` = 'لینک اعتماد اینماد (مثلاً trustseal.enamad.ir/?id=...&Code=...).' WHERE `key` = 'enamad_url';
|
||||
UPDATE `settings` SET `description` = 'آدرس تصویر لوگوی اینماد (مثلاً trustseal.enamad.ir/logo.aspx?id=...&Code=...).' WHERE `key` = 'enamad_logo';
|
||||
@@ -2046,6 +2083,12 @@ UPDATE `settings` SET `title_fa` = 'متن دعوتنامه اشتراکگذ
|
||||
-- UPDATE `settings` SET `title_fa` = 'حالت فروشگاهی' WHERE `key` = 'is_store';
|
||||
UPDATE `settings` SET `title_fa` = 'تأیید کارت بانکی با جیبیت' WHERE `key` = 'jibit_verification';
|
||||
UPDATE `settings` SET `title_fa` = 'کلید داخلی اپ' WHERE `key` = 'key';
|
||||
UPDATE `settings` SET `title_fa` = 'خط اول هیرو لاگین' WHERE `key` = 'login_hero_eyebrow';
|
||||
UPDATE `settings` SET `title_fa` = 'عنوان هیرو لاگین' WHERE `key` = 'login_hero_title';
|
||||
UPDATE `settings` SET `title_fa` = 'ویژگی ۱ هیرو لاگین' WHERE `key` = 'login_hero_feature_1';
|
||||
UPDATE `settings` SET `title_fa` = 'ویژگی ۲ هیرو لاگین' WHERE `key` = 'login_hero_feature_2';
|
||||
UPDATE `settings` SET `title_fa` = 'ویژگی ۳ هیرو لاگین' WHERE `key` = 'login_hero_feature_3';
|
||||
UPDATE `settings` SET `title_fa` = 'ویژگی ۴ هیرو لاگین' WHERE `key` = 'login_hero_feature_4';
|
||||
-- [super-admin can_edit=0]
|
||||
-- UPDATE `settings` SET `title_fa` = 'لوگوی صفحه ورود' WHERE `key` = 'logo_login';
|
||||
-- [super-admin can_edit=0]
|
||||
|
||||
@@ -250,12 +250,15 @@ Route::middleware('auth:user')->group(function (){
|
||||
Route::get('tahesab/customer-sync/status', [TahesabController::class, 'customerSyncStatus']);
|
||||
Route::post('tahesab/customer-sync/panel-to-tahesab', [TahesabController::class, 'syncPanelToTahesab']);
|
||||
Route::post('tahesab/customer-sync/import-from-tahesab', [TahesabController::class, 'importFromTahesab']);
|
||||
Route::post('tahesab/customer-sync/link-by-phone', [TahesabController::class, 'linkMatchingPhones']);
|
||||
Route::post('tahesab/customer-sync/rollback-import', [TahesabController::class, 'rollbackImport']);
|
||||
Route::post('tahesab/mande-by-codes', [TahesabController::class, 'mandeByCodes']);
|
||||
Route::post('tahesab/wallet-sync/all', [TahesabController::class, 'syncAllWallets']);
|
||||
Route::get('tahesab/{id}', [TahesabController::class, 'show']);
|
||||
Route::post('tahesab/{id}/retry', [TahesabController::class, 'retrySanad']);
|
||||
Route::post('customer/{id}/tahesab/register', [TahesabController::class, 'registerCustomer']);
|
||||
Route::post('customer/{id}/tahesab/sync-wallet', [TahesabController::class, 'syncCustomerWallet']);
|
||||
Route::post('customer/{id}/tahesab/preview-wallet', [TahesabController::class, 'previewCustomerWallet']);
|
||||
|
||||
Route::get('zarin/health', [ZarinController::class, 'healthCheck']);
|
||||
Route::post('wallet/zarin', [ZarinController::class, 'wallet']);
|
||||
@@ -269,10 +272,14 @@ Route::middleware('auth:user')->group(function (){
|
||||
Route::match(['get', 'post'], '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/link-by-phone', [ZarinController::class, 'linkMatchingPhones']);
|
||||
Route::post('zarin/customer-sync/rollback-import', [ZarinController::class, 'rollbackImport']);
|
||||
Route::post('zarin/wallet-sync/all', [ZarinController::class, 'syncAllWallets']);
|
||||
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('customer/{id}/zarin/sync-wallet', [ZarinController::class, 'syncCustomerWallet']);
|
||||
Route::post('customer/{id}/zarin/preview-wallet', [ZarinController::class, 'previewCustomerWallet']);
|
||||
|
||||
Route::post('pargar/table', [PargarController::class, 'table']);
|
||||
Route::get('pargar/customer-sync/status', [PargarController::class, 'customerSyncStatus']);
|
||||
|
||||
Reference in New Issue
Block a user