Files
boursecu_server/app/Http/Controllers/Admin/TahesabController.php
T
mohmdsdgandCursor 00b23ac404 feat(tahesab,colleague): push app balance to Tahesab and always wait callback
Add admin decision to write app wallet onto Tahesab even when zero, always await colleague callback regardless of the wait switch, and SMS admins on colleague reject.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:30:56 +03:30

1897 lines
72 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Site\ReportController;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use App\Models\Tahesab;
use App\Models\Customer;
use App\Services\AccountingProductDocMapper;
use App\Services\ConfigService;
use App\Services\TahesabDocSyncService;
use App\Services\TahesabInboundWalletService;
use App\Services\TahesabSyncService;
use Morilog\Jalali\Jalalian;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Hekmatinasser\Verta\Verta;
class TahesabController extends Controller
{
protected function sync(): TahesabSyncService
{
return app(TahesabSyncService::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;
}
public function table(Request $request)
{
app(TahesabSyncService::class)->ensureLocalTable();
if (! Schema::hasTable('tahesab')) {
return response()->json([]);
}
$view = (string) $request->input('view', 'all');
$query = Tahesab::with('customer')->orderByDesc('id');
if ($view === 'ok') {
$query->whereNotNull('tahesab_id')->where('tahesab_id', '!=', '');
} elseif ($view === 'failed') {
$query->where(function ($q) {
$q->where(function ($inner) {
$inner->whereNull('tahesab_id')->orWhere('tahesab_id', '');
})->orWhere('sync_status', 'failed');
});
}
\App\DB\Te\TableEngine::create($query, $request)
->initSearch(['desc', 'type'])
->select(['*'])
->export($query);
return response()->json($query);
}
public function show($id)
{
$tahesab = Tahesab::with('customer')->findOrFail($id);
return response()->json($tahesab);
}
/** همه مشتریان ته‌حساب را از API می‌گیرد و برمی‌گرداند (Code => [Code,Name,Tel]) */
private function fetchAllTahesabCustomers(): array
{
try {
$response = Http::withoutVerifying()->timeout(30)->withHeaders([
'DBName' => $this->Config('tahesab_db'),
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->Config('tahesab_token'),
])->post($this->tahesabApiUrl(), [
'DoListMoshtari' => [1, 999999],
])->json();
if (! is_array($response)) return [];
return $this->tahesabCustomerRows($response);
} catch (\Exception $e) {
return [];
}
}
public function customerSyncStatus(Request $request): \Illuminate\Http\JsonResponse
{
app(TahesabDocSyncService::class)->tickIfStale();
// مشتریان پنل
$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);
// مشتریان ته‌حساب — فقط وقتی include_tahesab=true ارسال شده (چون کند است)
$tahesabTotal = null;
$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);
$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 = $this->extractTahesabCode($item);
$codeKey = $this->accountingCodeKey($code);
$phone = $this->extractTahesabPhone($item);
$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' => $code !== null ? (int) $code : null,
'name' => trim((string) ($item['Name'] ?? $item['name'] ?? '')),
'tel' => $phone,
'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);
$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,
'current_page' => 1,
'last_page' => 1,
];
}
$payload = [
'panel_total' => $panelTotal,
'panel_registered' => $panelRegistered,
'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,
'doc_sync' => app(TahesabDocSyncService::class)->snapshot(),
];
// برای تشخیص نام دقیق فیلدها: ?include_tahesab=1&raw=1
if ($request->boolean('include_tahesab') && $request->boolean('raw')) {
$payload['tahesab_debug_sample'] = $debugSample ?? [];
}
return response()->json($payload);
}
/** شماره تماس را از هر کلید محتمل در رکورد مشتری ته‌حساب استخراج می‌کند */
private function extractTahesabPhone(array $item): string
{
$candidates = ['Mobile', 'CellPhone', 'Cell', 'Tel', 'Tell', 'Phone', 'MobileNo', 'Telephone', 'Mobile1'];
foreach ($candidates as $key) {
if (isset($item[$key]) && trim((string) $item[$key]) !== '') {
return trim((string) $item[$key]);
}
}
// جستجوی عمومی: هر کلیدی که شامل mobile/tel/phone/cell باشد و مقدار داشته باشد
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 '';
}
/**
* @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];
}
/**
* مشتریان ته‌حساب که در پنل نیستند و شماره دارند — برای ورود تکی‌تکی سمت سرور.
*
* @return list<array{code:int,name:string,tel:string}>
*/
public function collectImportableTahesabCustomers(): array
{
$tahesabCustomers = $this->fetchAllTahesabCustomers();
$maps = $this->panelAccountingMaps();
$out = [];
foreach ($tahesabCustomers as $item) {
if (! is_array($item)) {
continue;
}
$code = $this->extractTahesabCode($item);
$codeKey = $this->accountingCodeKey($code);
$phone = $this->extractTahesabPhone($item);
if (Customer::normalizeMobile($phone) === null) {
continue;
}
if ($codeKey !== null && isset($maps['by_code'][$codeKey])) {
continue;
}
$mobileKey = Customer::normalizeMobile($phone);
if ($mobileKey && isset($maps['by_mobile'][$mobileKey])) {
continue;
}
if ($codeKey === null) {
continue;
}
$out[] = [
'code' => (int) $code,
'name' => trim((string) ($item['Name'] ?? $item['name'] ?? '')),
'tel' => $phone,
];
}
return $out;
}
/** لینک کد ته‌حساب روی مشتری پنل وقتی شماره یکسان است */
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')
->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 importFromTahesab(Request $request): \Illuminate\Http\JsonResponse
{
$validated = $request->validate([
'customers' => 'required|array',
'customers.*.code' => 'required|integer',
'customers.*.name' => 'required|string',
'customers.*.tel' => 'nullable|string',
]);
$success = 0;
$failed = 0;
$errors = [];
/** @var list<Customer> $importedForWallet */
$importedForWallet = [];
foreach ($validated['customers'] as $item) {
$mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? '';
$fullName = trim($item['name']);
$code = (int) $item['code'];
// بدون موبایل ردیف placeholder نساز
if ($mobile === '') {
$failed++;
$errors[] = [
'name' => $item['name'],
'code' => $item['code'],
'error' => 'شماره موبایل خالی است — وارد نشد',
];
continue;
}
$existing = Customer::whereNull('deleted_at')->where('moshtari_code', $code)->first();
if ($existing) {
$changed = false;
if (($existing->name === null || $existing->name === '') && $fullName !== '') {
$existing->fname = $fullName;
$existing->lname = (string) ($existing->lname ?? '');
$existing->name = $fullName;
$changed = true;
}
if ($mobile !== '' && ($existing->mobile === null || $existing->mobile === '')) {
if (Customer::isMobileTaken($mobile, (int) $existing->id)) {
$failed++;
$errors[] = [
'name' => $item['name'],
'code' => $item['code'],
'error' => 'شماره موبایل تکراری است',
];
continue;
}
$existing->mobile = $mobile;
if (empty($existing->password)) {
$existing->password = $mobile;
}
$changed = true;
}
if ($changed) {
$existing->save();
}
$importedForWallet[] = $existing;
$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();
$importedForWallet[] = $byMobile;
$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 یک mutator روی name دارد که name را از fname+lname می‌سازد
// و مقدار مستقیم name را نادیده می‌گیرد. پس باید fname را قبل از name ست کنیم.
$customer = new Customer();
$customer->fname = $fullName;
$customer->lname = '';
$customer->name = $fullName; // mutator: name = fname (+ lname)
$customer->mobile = $mobile;
$customer->active = true;
$customer->moshtari_code = $item['code'];
$customer->referral_id = $referralId;
$customer->type = 2;
// رمز عبور = شماره موبایل (فقط وقتی موبایل موجود است)
if ($mobile !== '') {
$customer->password = $mobile;
}
$customer->save();
$importedForWallet[] = $customer;
$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];
}
}
// مانده طلا/ریال ته‌حساب را روی کیف مشتریان واردشده بنشان
if ($importedForWallet !== []) {
try {
app(TahesabInboundWalletService::class)->syncAfterImport($importedForWallet);
} catch (\Throwable $e) {
\Log::warning('Tahesab import wallet sync batch failed', [
'count' => count($importedForWallet),
'error' => $e->getMessage(),
]);
}
}
return response()->json([
'ok' => true,
'success' => $success,
'failed' => $failed,
'errors' => $errors,
]);
}
/**
* حذف مشتریانِ خرابِ واردشده از ته‌حساب (نام خالی) برای پاک‌سازی import ناقص.
* با dry_run=1 فقط تعداد و نمونه را نشان می‌دهد و چیزی حذف نمی‌کند.
*/
public function rollbackImport(Request $request): \Illuminate\Http\JsonResponse
{
// مشتریانِ ساخته‌شده توسط import که name آن‌ها خالی مانده (به‌خاطر mutator)
$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)
{
$tahesab = Tahesab::with('customer')->findOrFail($id);
$result = $this->sync()->retry($tahesab);
return response()->json($result, ($result['ok'] ?? false) ? 200 : 422);
}
public function pendingSummary(Request $request)
{
$docs = app(TahesabDocSyncService::class);
$docs->tickIfStale();
$customerId = $request->filled('customer_id') ? (int) $request->input('customer_id') : null;
return response()->json($this->sync()->pendingSummary($customerId));
}
public function startDocSync()
{
$result = app(TahesabDocSyncService::class)->start();
return response()->json($result, ($result['ok'] ?? false) ? 200 : 422);
}
public function stopDocSync()
{
$result = app(TahesabDocSyncService::class)->stop();
return response()->json($result);
}
public function resetLocalDocSync()
{
$result = $this->sync()->resetLocalSyncedDocs();
return response()->json($result, ($result['ok'] ?? false) ? 200 : 422);
}
public function startCustomerOutbound()
{
$result = app(TahesabDocSyncService::class)->startCustomerOutbound();
return response()->json($result, ($result['ok'] ?? false) ? 200 : 422);
}
public function startCustomerInbound()
{
$result = app(TahesabDocSyncService::class)->startCustomerInbound();
return response()->json($result, ($result['ok'] ?? false) ? 200 : 422);
}
public function pendingDocs(Request $request)
{
$customerId = $request->filled('customer_id') ? (int) $request->input('customer_id') : null;
$limit = max(1, min(500, (int) $request->input('limit', 200)));
return response()->json([
'rows' => $this->sync()->pendingDocs($customerId, $limit),
'summary' => $this->sync()->pendingSummary($customerId),
]);
}
public function syncOne(Request $request)
{
$type = (string) $request->input('type', '');
$id = (int) $request->input('id', 0);
if ($id <= 0 || ! in_array($type, ['row', 'withdraw', 'order'], true)) {
return response()->json(['ok' => false, 'message' => 'پارامتر نامعتبر'], 422);
}
$result = $this->sync()->syncOne($type, $id);
return response()->json($result, ($result['ok'] ?? false) ? 200 : 422);
}
public function syncAllPending(Request $request)
{
$limit = max(1, min(200, (int) $request->input('limit', 80)));
$customerId = $request->filled('customer_id') ? (int) $request->input('customer_id') : null;
$result = $this->sync()->syncAllPending($limit, $customerId);
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);
}
/**
* همگام‌سازی مانده یک مشتری با ته‌حساب.
* body: overwrite=1 | keep=1 | push_to_tahesab=1 | ask_if_conflict=1
*/
public function syncCustomerWallet(Request $request, $customerId)
{
if (! app(ConfigService::class)->isTahesabAccounting()) {
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);
}
$walletService = app(TahesabInboundWalletService::class);
if ($request->boolean('push_to_tahesab')) {
$result = $walletService->pushWalletToTahesab($customer);
$applied = (bool) ($result['applied'] ?? false);
return response()->json([
'ok' => $applied,
'needs_decision' => false,
'message' => $result['message'] ?? '',
'result' => $result,
], $applied ? 200 : 422);
}
$overwrite = $request->boolean('overwrite');
$keep = $request->boolean('keep');
$alwaysAsk = $request->boolean('always_ask');
$askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep && ! $alwaysAsk);
$result = $walletService->syncWalletFromTahesab(
$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,
]);
}
/**
* همگام‌سازی مانده همه مشتریان دارای کد ته‌حساب — تکه‌تکه.
* body: overwrite=1 برای اعمال اجباری اختلاف‌ها؛ بدون آن فقط کیف خالی یا یکسان پردازش می‌شود و تداخل‌ها در needs_decision می‌آیند.
*/
public function syncAllWallets(Request $request)
{
if (! app(ConfigService::class)->isTahesabAccounting()) {
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(TahesabInboundWalletService::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->syncWalletFromTahesab(
$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['tahesab_gold'] ?? null,
'tahesab_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,
]);
}
/**
* لیست مشتریان لینک‌شده با مقایسهٔ کیف و ماندهٔ ته‌حساب.
*/
public function walletLinkedList(Request $request): \Illuminate\Http\JsonResponse
{
if (! app(ConfigService::class)->isTahesabAccounting()) {
return response()->json([
'ok' => false,
'message' => 'حسابداری ته‌حساب فعال نیست',
], 422);
}
$search = trim((string) $request->input('search', ''));
$page = max(1, (int) $request->input('page', 1));
$perPage = max(1, min(50, (int) $request->input('per_page', 20)));
$base = Customer::query()
->whereNull('deleted_at')
->whereNotNull('moshtari_code')
->where('moshtari_code', '!=', '')
->where('moshtari_code', '!=', '0');
if ($search !== '') {
$base->where(function ($q) use ($search) {
$q->where('name', 'like', '%'.$search.'%')
->orWhere('mobile', 'like', '%'.$search.'%')
->orWhere('moshtari_code', 'like', '%'.$search.'%');
});
}
$total = (clone $base)->count();
$customers = (clone $base)
->orderBy('id')
->offset(($page - 1) * $perPage)
->limit($perPage)
->get(['id', 'name', 'mobile', 'moshtari_code', 'type']);
$rows = app(TahesabInboundWalletService::class)->compareCustomers($customers);
return response()->json([
'ok' => true,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'last_page' => max(1, (int) ceil($total / max(1, $perPage))),
'rows' => $rows,
]);
}
private function tahesabApiUrl(): string
{
$host = trim((string) $this->Config('tahesab_host'));
$port = trim((string) $this->Config('tahesab_port'));
if ($host !== '' && $port !== '') {
$host = rtrim(preg_replace('#^https?://#i', '', $host), '/');
return 'httpS://' . $host . ':' . $port . '/';
}
$legacy = trim((string) env('TAHESAB', ''));
return $legacy !== '' ? $legacy : '';
}
public function storeTrade($data, ?Tahesab $existing = null)
{
// اگر payload خالی است اما Tahesab موجود و order_id دارد، از Order بازسازی کن
if (empty($data['customer_id']) && $existing && $existing->order_id) {
$order = \App\Models\Order::find($existing->order_id);
if ($order) {
$productPayload = ['metal_id' => $existing->type === 'طلا' ? 1 : 2];
if ($order->product_id) {
$productPayload['id'] = (int) $order->product_id;
}
$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' => $productPayload,
'product_id' => $order->product_id ? (int) $order->product_id : null,
'order' => ['id' => $existing->order_id, 'product_id' => $order->product_id],
]);
}
}
if (!isset($data['customer_id']) || !isset($data['amount'])) {
return request()->expectsJson()
? response()->json(['error' => 'Data missing: customer_id or amount'], 422)
: false;
}
$mapper = app(AccountingProductDocMapper::class);
$product = $mapper->resolveProductFromPayload($data);
if ($product && $mapper->shouldSkip($product)) {
return false;
}
$customer = Customer::where('id', $data['customer_id'])->first();
$accountOverride = trim((string) ($data['accounting_account_code'] ?? ''));
$isColleagueLeg = ! empty($data['colleague_leg']) && $accountOverride !== '';
if (! $isColleagueLeg) {
$ensured = $this->ensureCustomerHasTahesabCode($customer);
if (! ($ensured['ok'] ?? false)) {
return $ensured['message'] ?? 'مشتری کد ته‌حساب ندارد';
}
$customer = $ensured['customer'];
}
if (! $customer) {
return 'مشتری سند پیدا نشد';
}
$hesabCode = $isColleagueLeg
? (int) $accountOverride
: (int) ($customer->moshtari_code ?? 0);
if ($hesabCode <= 0) {
return 'مشتری کد ته‌حساب ندارد';
}
$kind = $mapper->resolveKind($product);
$rawAmount = (float) $data['amount'];
$count = $mapper->accountingQty($product, $rawAmount, $data);
$vazn = $mapper->accountingWeight($product, $rawAmount, $data);
$jens = $mapper->jensFelez($product) ?? 0;
$title = $mapper->productTitle($product);
$isBuy = (int) ($data['direction'] ?? 0) === 1;
$price = (float) ($data['price'] ?? 0);
$mablaghKol = $isBuy
? (int) round($vazn * $price)
: (int) ($data['total_price'] ?? round($vazn * $price));
// سند تعدادی: مظنه = فی هر عدد (نه لزوماً قیمت هر گرم)
$mazaneh = (int) $price;
if ($mapper->isCountDoc($product) && $count > 0) {
if (!empty($data['total_price'])) {
$mazaneh = (int) round(((float) $data['total_price']) / $count);
} elseif ($price > 0 && abs($vazn - $count) > 0.0001) {
// price احتمالاً هر گرم است
$unitW = $vazn / $count;
$mazaneh = (int) round($price * $unitW);
}
if ($mazaneh <= 0 && $mablaghKol > 0) {
$mazaneh = (int) round($mablaghKol / $count);
}
}
$date = Verta::instance(Carbon::parse($data['pay_at'] ?? now()))->format('Y-m-d');
[$year, $month, $day] = explode('-', $date);
$action = $isBuy ? 'خرید' : 'فروش';
$kindFa = $mapper->kindLabel($kind);
$sharh = trim((string) ($data['sharh_override'] ?? ''));
if ($sharh === '') {
$sharh = trim($title !== '' ? "{$action} {$title}" : "{$action} {$kindFa}");
}
$sharh = $this->sharhWithCustomer($sharh, $customer);
$orderId = is_object($data['order'] ?? null)
? ($data['order']->id ?? null)
: ($data['order']['id'] ?? null);
$data['handler'] = 'storeTrade';
if ($existing && empty($existing->data)) {
$existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]);
}
$tahesab = $existing ?? Tahesab::create([
'customer_id' => $customer->id,
'desc' => $sharh,
'amount' => $mapper->isCountDoc($product) ? $count : $vazn,
'order_id' => $orderId,
'date' => $data['pay_at'] ?? Carbon::now(),
'type' => $mapper->syncTypeLabel($product),
'created_at' => Carbon::now(),
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
]);
if ($mapper->isCountDoc($product) && $title === '' && ! $isColleagueLeg) {
$this->sync()->markFailed($tahesab, null, 'نام محصول برای سند تعدادی ته‌حساب خالی است');
return false;
}
$body = $this->buildBuySaleBody(
$kind,
$hesabCode,
(int) $year,
(int) $month,
(int) $day,
$vazn,
$count,
$title,
$isBuy ? 1 : 0,
$mazaneh,
$mablaghKol,
$sharh,
$jens,
(string) Verta::instance($data['pay_at'] ?? now())
);
try {
$response = $this->postTahesab($body);
} catch (\Exception $e) {
$this->sync()->markFailed($tahesab, $e->getMessage());
return $e->getMessage();
}
if ($this->sync()->responseOk($response)) {
$this->sync()->markSuccess($tahesab, $response, $data);
return $data;
}
$this->sync()->markFailed($tahesab, $response, 'ثبت سند معامله در ته‌حساب ناموفق بود');
return false;
}
// directon: 0 = daryaft az moshtari
// directon: 1 = pardakht be moshtari
public function wallet($data, ?Tahesab $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 = auth()->user();
// if (!$customer) {
// throw new UnprocessableEntityHttpException("Auth needed");
// }
$customer = Customer::where('id', $data['customer_id'])->first();
$ensured = $this->ensureCustomerHasTahesabCode($customer);
if (! ($ensured['ok'] ?? false)) {
return $ensured['message'] ?? 'مشتری کد ته‌حساب ندارد';
}
$customer = $ensured['customer'];
$data['handler'] = 'wallet';
$isDeposit = (int) ($data['direction'] ?? 0) === 0;
$sharh = $this->sharhWithCustomer(
self::accountingWalletDesc($isDeposit, $data['note'] ?? null),
$customer
);
$tahesab = $existing ?? Tahesab::create([
"customer_id" => $customer->id,
"desc" => $sharh,
"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)]);
}
$dateSource = ($existing !== null && !empty($existing->date)) ? Carbon::parse($existing->date) : Carbon::now();
$date = Verta::instance($dateSource)->format('Y-m-d');
[$year, $month, $day] = explode('-', $date);
try {
$response = Http::withoutVerifying()->timeout(10)->withHeaders([
'DBName' => $this->Config('tahesab_db'),
'Content-Type' => 'application/json',
'Authorization' => "Bearer " . $this->Config('tahesab_token'),
])->post($this->tahesabApiUrl(), [
"DoNewSanadVKHBank" => [
1,//Sabte_Kol_Or_Movaghat_1_0
$customer->moshtari_code,//Moshtari_Code
-1,//Factor_Number
1,//Radif_Number
(int)$year,//Shamsi_Year
(int)$month,//Shamsi_Month
(int)$day,//Shamsi_Day
(int)$data['direction'],//IsDayaftAzMoshtariOrPardakhtBeMoshtari_0_1
(int)$data['value'],//Mablagh
$sharh,//Sharh
(string)$this->Config('tahesab_bank_account'), //Name_Bank
"$tahesab->id",//Code_Rahgiri
-1,//Factor_Code
-1,
//MultiRadif
]
])->json();
\Log::info('Tahesab wallet response', [
'data' => $data,
'response' => $response ?? null,
]);
} catch (\Exception $e) {
$this->sync()->markFailed($tahesab, $e->getMessage());
return $e->getMessage();
}
if ($this->sync()->responseOk($response)) {
$this->sync()->markSuccess($tahesab, $response, $data);
return $data;
}
$this->sync()->markFailed($tahesab, $response, 'ثبت سند ریالی در ته‌حساب ناموفق بود');
return false;
// if($data['bank'] == true) {
// try {
// $response = Http::withoutVerifying()->timeout(10)->withHeaders([
// 'DBName' => $this->ConfigService('tahesab_db'),
// 'Content-Type' => 'application/json',
// 'Authorization' => "Bearer " . $this->ConfigService('tahesab_token'),
// ])->post($this->tahesabApiUrl(), [
// "DoNewSanadVKHBank" => [
// 1,//Sabte_Kol_Or_Movaghat_1_0
// $customer->moshtari_code,//Moshtari_Code
// -1,//Factor_Number
// 1,//Radif_Number
// (int)$year,//Shamsi_Year
// (int)$month,//Shamsi_Month
// (int)$day,//Shamsi_Day
// (int)$data['direction'],//IsDayaftAzMoshtariOrPardakhtBeMoshtari_0_1
// (int)$data['value'],//Mablagh
// $data['direction'] == 0 ? "شارژ حساب" : "برداشت از حساب",//Sharh
// (string)$this->ConfigService('tahesab_bank_account'), //Name_Bank
// "$tahesab->id",//Code_Rahgiri
// -1,//Factor_Code
// -1,
// //MultiRadif
// ]
// ])->json();
// } catch (\Exception $e) {
// return $e->getMessage();
// }
// if ($response['OK']) {
// Tahesab::where('id', $tahesab->id)->update([
// "sh_factor" => $response['Sh_factor'],
// "tahesab_id" => $response['OK'],
// "tah_id" => $response['ID'],
// "factor_number" => $response['Factor_Number'],
// "data" => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
// "res" => json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
// ]);
//
// return $data;
// }
// }else{
// try {
// $response = Http::withoutVerifying()->timeout(10)->withHeaders([
// 'DBName' => $this->ConfigService('tahesab_db'),
// 'Content-Type' => 'application/json',
// 'Authorization' => "Bearer " . $this->ConfigService('tahesab_token'),
// ])->post($this->tahesabApiUrl(), [
// "DoNewSanadTalabBedehi" => [
// 1,//Sabte_Kol_Or_Movaghat_1_0,
// $customer->moshtari_code,//Moshtari_Code
// -1,//Factor_Number
// 1,//Radif_Number
// (int)$year,//Shamsi_Year
// (int)$month,//Shamsi_Month
// (int)$day,//Shamsi_Day
// $data['value'] < 0 ? 0 : 1,//IsBedehiOrTalab_0_1,
// (int)abs($data['value']),//Mablagh
// $data['value'] < 0 ? "بدهکار" : "بستانکار",//Sharh
// $data['note'],//"Baabat",
// 0,//Vazn,
// -1,//Factor_Code
// -1,//HavalehBe_Mcode,
// null,//MultiRadif
// 0,//Jens_Felez,
// "ArzName"
// ]
// ])->json();
// } catch (\Exception $e) {
// return $e->getMessage();
// }
// if ($response['OK']) {
// Tahesab::where('id', $tahesab->id)->update([
// "sh_factor" => $response['Sh_factor'],
// "tahesab_id" => $response['OK'],
// "data" => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
// "res" => json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
// ]);
//
// return $data;
// }
// }
}
/**
* پاداش معرفی — همیشه روی حساب معرف (نه کاربر تازه‌ثبت‌نام‌شده).
*/
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,
]);
}
/**
* پاداش/هدیه طلایی — سند بدهی وزنی (مثل ثبت دستی «بدهی / بابت معرفی» در ته‌حساب).
*/
public function goldReward($data, ?Tahesab $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();
$ensured = $this->ensureCustomerHasTahesabCode($customer);
if (! ($ensured['ok'] ?? false)) {
return $ensured['message'] ?? 'مشتری کد ته‌حساب ندارد';
}
$customer = $ensured['customer'];
$amount = abs((float) $data['value']);
$desc = $this->sharhWithCustomer(
! empty($data['desc']) ? (string) $data['desc'] : 'پاداش',
$customer
);
$data['handler'] = 'goldReward';
$tahesab = $existing ?? Tahesab::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)]);
}
$dateSource = ($existing !== null && !empty($existing->date)) ? Carbon::parse($existing->date) : Carbon::now();
$date = Verta::instance($dateSource)->format('Y-m-d');
[$year, $month, $day] = explode('-', $date);
try {
$response = Http::withoutVerifying()->timeout(10)->withHeaders([
'DBName' => $this->Config('tahesab_db'),
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->Config('tahesab_token'),
])->post($this->tahesabApiUrl(), [
'DoNewSanadTalabBedehi' => [
1, // Sabte_Kol
(int) $customer->moshtari_code,
-1, // Factor_Number
1, // Radif_Number
(int) $year,
(int) $month,
(int) $day,
0, // IsBedehiOrTalab — بدهی (فروشگاه به مشتری طلا بدهکار است)
0, // MablaghKol — بدون مبلغ ریالی
$desc,
$desc, // Baabat
(float) $amount,
-1, // Factor_Code
-1, // HavalehBe_Mcode
-1, // MultiRadif
0, // Jens_Felez — طلا
'IRR',
],
])->json();
} catch (\Exception $e) {
$this->sync()->markFailed($tahesab, $e->getMessage());
return $e->getMessage();
}
if ($this->sync()->responseOk($response)) {
$this->sync()->markSuccess($tahesab, $response, $data);
return $data;
}
$this->sync()->markFailed($tahesab, $response, 'ثبت پاداش طلایی در ته‌حساب ناموفق بود');
return false;
}
// direction: 1 = واریز طلا (ورود)، 0 = برداشت طلا (خروج) — VKH نه BuySale
public function walletGold($data, ?Tahesab $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,
]);
$payload = json_decode((string) ($existing->data ?? ''), true);
if (is_array($payload)) {
if (!empty($payload['product_id'])) {
$data['product_id'] = (int) $payload['product_id'];
}
if (!empty($payload['product']) && is_array($payload['product'])) {
$data['product'] = $payload['product'];
}
}
}
if (!isset($data['customer_id'], $data['value'])) {
return request()->expectsJson()
? response()->json(['error' => 'Data missing: customer_id or value'], 422)
: false;
}
$mapper = app(AccountingProductDocMapper::class);
$product = $mapper->resolveProductFromPayload($data);
if ($product && $mapper->shouldSkip($product)) {
return false;
}
$customer = Customer::where('id', $data['customer_id'])->first();
$ensured = $this->ensureCustomerHasTahesabCode($customer);
if (! ($ensured['ok'] ?? false)) {
return $ensured['message'] ?? 'مشتری کد ته‌حساب ندارد';
}
$customer = $ensured['customer'];
$kind = $mapper->resolveKind($product);
$rawValue = (float) $data['value'];
$count = $mapper->accountingQty($product, $rawValue, $data);
$vazn = $mapper->accountingWeight($product, $rawValue, $data);
$jens = $mapper->jensFelez($product) ?? 0;
$title = $mapper->productTitle($product);
$isDeposit = (int) ($data['direction'] ?? 1) === 1;
$isVorood = $isDeposit ? 1 : 0;
$sharh = $this->sharhWithCustomer(
self::accountingWalletDesc(
$isDeposit,
! empty($data['note']) ? (string) $data['note'] : (! empty($data['desc']) ? (string) $data['desc'] : null)
),
$customer
);
$data['note'] = $sharh;
$data['handler'] = 'walletGold';
$tahesab = $existing ?? Tahesab::create([
'customer_id' => $customer->id,
'desc' => $sharh,
'amount' => $mapper->isCountDoc($product) ? $count : $vazn,
'date' => Carbon::now(),
'type' => $mapper->syncTypeLabel($product),
'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)]);
}
if ($mapper->isCountDoc($product) && $title === '') {
$this->sync()->markFailed($tahesab, null, 'نام محصول برای سند تعدادی ته‌حساب خالی است');
return false;
}
$dateSource = ($existing !== null && !empty($existing->date)) ? Carbon::parse($existing->date) : Carbon::now();
$date = Verta::instance($dateSource)->format('Y-m-d');
[$year, $month, $day] = explode('-', $date);
$body = $this->buildVkhBody(
$kind,
(int) $customer->moshtari_code,
(int) $year,
(int) $month,
(int) $day,
$vazn,
$count,
$title,
$isVorood,
$sharh,
$jens
);
try {
$response = $this->postTahesab($body);
} catch (\Exception $e) {
$this->sync()->markFailed($tahesab, $e->getMessage());
return $e->getMessage();
}
if ($this->sync()->responseOk($response)) {
$this->sync()->markSuccess($tahesab, $response, $data);
return $data;
}
$this->sync()->markFailed($tahesab, $response, 'ثبت سند طلایی کیف پول در ته‌حساب ناموفق بود');
return false;
}
private function postTahesab(array $body): array
{
$response = Http::withoutVerifying()->timeout(10)->withHeaders([
'DBName' => $this->Config('tahesab_db'),
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->Config('tahesab_token'),
])->post($this->tahesabApiUrl(), $body)->json();
return is_array($response) ? $response : [];
}
/**
* سند خرید/فروش — معامله.
*/
private function buildBuySaleBody(
string $kind,
int $moshtariCode,
int $year,
int $month,
int $day,
float $vazn,
float $count,
string $name,
int $buyOrSale,
int $mazaneh,
int $mablaghKol,
string $sharh,
int $jens,
string $zamanTasvie
): array {
$ayar = $jens === 0 ? 750 : '';
if ($kind === AccountingProductDocMapper::KIND_SEKEH) {
return [
'DoNewSanadBuySaleSEKEH' => [
1, $moshtariCode, -1, 1, $year, $month, $day,
(float) $vazn, $ayar, (float) $count, $name,
$buyOrSale, $mazaneh, $mablaghKol, $sharh,
-1, -1, -1, $jens, '',
],
];
}
if ($kind === AccountingProductDocMapper::KIND_SHEMSH) {
return [
'DoNewSanadBuySaleShemsh' => [
1, $moshtariCode, -1, 1, $year, $month, $day,
(float) $vazn, $ayar, (float) $count, $name,
$buyOrSale, $mazaneh, $mablaghKol, $sharh,
-1, -1, -1, $jens, '',
],
];
}
if ($kind === AccountingProductDocMapper::KIND_KAR) {
return [
'DoNewSanadBuySaleKar' => [
1, $moshtariCode, -1, 1, $year, $month, $day,
(float) $vazn, $ayar ?: 750, (float) $count, $name,
$buyOrSale, $mazaneh, 1, $mablaghKol,
0, 0, 0, 0, 0, 0, 0, '', 0, $sharh,
-1, -1, -1, $jens, '',
],
];
}
// وزن / آبشده — همان payload قبلی با Jens_Felez و Sharh دقیق‌تر
return [
'DoNewSanadBuySaleGOLD' => [
1, $moshtariCode, -1, 1, $year, $month, $day,
(float) $vazn, $ayar, 0, '',
$buyOrSale, $mazaneh, 1, 1, $mablaghKol, $sharh,
-1, -1, -1, $jens, $zamanTasvie,
],
];
}
/**
* سند ورود/خروج — شارژ، دشارژ، برداشت فیزیکی.
*/
private function buildVkhBody(
string $kind,
int $moshtariCode,
int $year,
int $month,
int $day,
float $vazn,
float $count,
string $name,
int $isVorood,
string $sharh,
int $jens
): array {
$ayar = $jens === 0 ? 750 : '';
if ($kind === AccountingProductDocMapper::KIND_SEKEH) {
return [
'DoNewSanadVKHSEKEH' => [
1, $moshtariCode, -1, 1, $year, $month, $day,
(float) $vazn, $ayar, (float) $count, $name,
$isVorood, $sharh, -1, -1, -1, $jens,
],
];
}
if ($kind === AccountingProductDocMapper::KIND_SHEMSH) {
return [
'DoNewSanadVKHShemsh' => [
1, $moshtariCode, -1, 1, $year, $month, $day,
(float) $vazn, $ayar, (float) $count, $name,
$isVorood, $sharh, -1, -1, -1, $jens,
],
];
}
if ($kind === AccountingProductDocMapper::KIND_KAR) {
return [
'DoNewSanadVKHKar' => [
1, $moshtariCode, -1, 1, $year, $month, $day,
(float) $vazn, $ayar ?: 750, (float) $count, $name,
$isVorood, 0, 0, 0, 0, 0, 0, '', 0, 0, $sharh,
-1, -1, -1, $jens,
],
];
}
return [
'DoNewSanadVKHGOLD' => [
1, $moshtariCode, -1, 1, $year, $month, $day,
(float) $vazn, $ayar ?: 750, '', '',
$isVorood, 1, $sharh, -1, -1, -1, $jens, 0,
],
];
}
private function ensureCustomerHasTahesabCode(?Customer $customer): array
{
if (! $customer) {
return ['ok' => false, 'message' => 'مشتری سند پیدا نشد'];
}
if ((int) ($customer->moshtari_code ?? 0) > 0) {
return ['ok' => true, 'customer' => $customer];
}
$reg = $this->createCustomerInTahesab($customer->id);
$customer->refresh();
if ((int) ($customer->moshtari_code ?? 0) > 0) {
return ['ok' => true, 'customer' => $customer];
}
$message = is_string($reg) && $reg !== ''
? $reg
: 'ثبت مشتری در ته‌حساب ناموفق بود';
return ['ok' => false, 'message' => $message];
}
private function sharhWithCustomer(string $sharh, Customer $customer): string
{
$name = trim((string) ($customer->name ?? ''));
if ($name === '' || str_contains($sharh, $name)) {
return $sharh;
}
return trim($sharh.' — '.$name);
}
public function createCustomerInTahesab($customer_id)
{
$customer = Customer::findOrFail($customer_id);
$response = null;
if ((int) ($customer->moshtari_code ?? 0) <= 0) {
$lockKey = 'tahesab:register-customer:'.$customer->id;
if (! Cache::add($lockKey, 1, 30)) {
$customer->refresh();
if ((int) ($customer->moshtari_code ?? 0) > 0) {
return $customer->moshtari_code;
}
return 'ثبت مشتری در ته‌حساب در حال انجام است';
}
try {
$customer->refresh();
if ((int) ($customer->moshtari_code ?? 0) > 0) {
return $customer->moshtari_code;
}
try {
$response = Http::withoutVerifying()->timeout(10)->withHeaders([
'DBName' => $this->Config('tahesab_db'),
'Content-Type' => 'application/json',
'Authorization' => "Bearer " . $this->Config('tahesab_token'),
])->post($this->tahesabApiUrl(), [
"DoNewMoshtari" => [
(string)$customer->name,
$this->Config('name_fa'),
(string)$customer->mobile,
"",
"",
"",
"",
"",
-1,
0
]
])->json();
if ($this->sync()->responseOk($response)) {
$customer->moshtari_code = $response['OK'];
$customer->save();
return $response['OK'];
}
} catch (\Exception $e) {
return $e->getMessage();
}
return is_array($response)
? ($this->sync()->buildLog($response, 'ثبت مشتری در ته‌حساب ناموفق بود') ?: 'ثبت مشتری در ته‌حساب ناموفق بود')
: 'ثبت مشتری در ته‌حساب ناموفق بود';
} finally {
Cache::forget($lockKey);
}
}
$birth_date = '';
if ($customer->birth_date !== null) {
$birth_date = Verta::instance(Carbon::parse($customer->birth_date))->format('Y-m-d');
}
try {
$response = Http::withoutVerifying()->timeout(10)->withHeaders([
'DBName' => $this->Config('tahesab_db'),
'Content-Type' => 'application/json',
'Authorization' => "Bearer " . $this->Config('tahesab_token'),
])->post($this->tahesabApiUrl(), [
"DoEditMoshtari" => [
$customer->name,
$this->Config('name_fa'),
$customer->mobile,
"",
$customer->n_code ?? "",
$birth_date,
"",
"",
(int)$customer->moshtari_code,
0
]
])->json();
} catch (\Exception $e) {
return $e->getMessage();
}
if (! $this->sync()->responseOk($response)) {
return 'tahesab api error!!!';
}
return true;
}
public function healthCheck()
{
$tahesab = Http::withoutVerifying()->withHeaders([
'Content-Type' => 'application/json',
'DBName' => $this->Config('tahesab_db'),
'Authorization' => "Bearer " . $this->Config('tahesab_token'),
])->post($this->tahesabApiUrl(), [
"CheckHealth" => []
])->json();
return $tahesab;
// if($tahesab['Api_Status'] === "OK" && $tahesab['DB_Status'] === "OK"){
// return true;
// }else{
// return false;
// }
}
public function sanadList()
{
$tahesab = Http::withoutVerifying()->withHeaders([
'Content-Type' => 'application/json',
'DBName' => $this->Config('tahesab_db'),
'Authorization' => "Bearer " . $this->Config('tahesab_token'),
])->post($this->tahesabApiUrl(), [
"DoListAsnad" => [
10, /*Count_Last,*/
"", /*Moshtari_Code,*/
"", /*"AzTarikhShamsi"*/
(string)Verta::instance(Carbon::now()->format('Y-m-d')), /*"TaTarikhShamsi"*/
"", /*"FilterNoSanad"*/
0, /*Jens_Felez*/
]
])->json();
return $tahesab;
}
}