Files
boursecu_server/app/Services/TahesabSyncService.php
T
mohmdsdgandCursor 3b7db1eb72 feat(tahesab): reset local success flags when remote docs were wiped
Clear tahesab_id on locally successful rows so they re-enter the send queue without touching panel customer codes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 13:43:16 +03:30

825 lines
30 KiB
PHP

<?php
namespace App\Services;
use App\Http\Controllers\Admin\TahesabController;
use App\Models\Customer;
use App\Models\Tahesab;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
class TahesabSyncService
{
public const STATUS_PENDING = 'pending';
public const STATUS_SUCCESS = 'success';
public const STATUS_FAILED = 'failed';
public function responseOk(mixed $response): bool
{
return is_array($response) && ! empty($response['OK']);
}
public function buildLog(mixed $response = null, ?string $extra = null): string
{
$parts = array_filter([
$extra,
is_array($response) ? json_encode($response, JSON_UNESCAPED_UNICODE) : (is_string($response) ? $response : null),
]);
return implode("\n", $parts);
}
public function markSuccess(Tahesab $tahesab, array $response, ?array $payload = null): void
{
$update = [
'sh_factor' => $response['Sh_factor'] ?? null,
'tahesab_id' => $response['OK'],
'factor_number' => $response['Factor_Number'] ?? null,
'tah_id' => $response['ID'] ?? null,
'res' => json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
'sync_status' => self::STATUS_SUCCESS,
'sync_log' => null,
];
if ($payload !== null) {
$update['data'] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
if ($this->hasSyncColumns()) {
Tahesab::where('id', $tahesab->id)->update($update);
} else {
unset($update['sync_status'], $update['sync_log']);
Tahesab::where('id', $tahesab->id)->update($update);
}
}
public function markFailed(Tahesab $tahesab, mixed $response = null, ?string $extra = null): void
{
$update = [
'res' => is_array($response)
? json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: (is_string($response) ? $response : null),
];
if ($this->hasSyncColumns()) {
$update['sync_status'] = self::STATUS_FAILED;
$update['sync_log'] = $this->buildLog($response, $extra);
}
Tahesab::where('id', $tahesab->id)->update($update);
}
public function isFailed(Tahesab $tahesab): bool
{
if ($this->hasSyncColumns()) {
return ($tahesab->sync_status ?? null) === self::STATUS_FAILED
|| (empty($tahesab->tahesab_id) && ($tahesab->sync_status ?? null) !== self::STATUS_SUCCESS);
}
return empty($tahesab->tahesab_id);
}
public function registerOnCreate(Customer $customer): void
{
if (! empty($customer->moshtari_code) || ! $this->isSyncEnabled()) {
return;
}
$customerId = (int) $customer->id;
dispatch(function () use ($customerId) {
$service = app(TahesabSyncService::class);
$customer = Customer::find($customerId);
if ($customer === null || ! empty($customer->moshtari_code) || ! $service->isSyncEnabled()) {
return;
}
try {
$result = $service->registerCustomer($customer);
if (! ($result['ok'] ?? false)) {
Log::warning('Tahesab auto-register on customer create failed', [
'customer_id' => $customerId,
'message' => $result['message'] ?? null,
]);
}
} catch (\Throwable $e) {
Log::warning('Tahesab auto-register on customer create threw', [
'customer_id' => $customerId,
'error' => $e->getMessage(),
]);
}
})->afterResponse();
}
public function registerCustomer(Customer $customer): array
{
$customer->refresh();
if (! empty($customer->moshtari_code)) {
return [
'ok' => true,
'message' => 'مشتری قبلاً در ته‌حساب ثبت شده است',
'moshtari_code' => $customer->moshtari_code,
];
}
$controller = app(TahesabController::class);
$result = $controller->createCustomerInTahesab($customer->id);
if (is_string($result)) {
return ['ok' => false, 'message' => $result];
}
$customer->refresh();
if (empty($customer->moshtari_code)) {
return ['ok' => false, 'message' => 'ثبت مشتری در ته‌حساب ناموفق بود'];
}
return [
'ok' => true,
'message' => 'مشتری با موفقیت در ته‌حساب ثبت شد',
'moshtari_code' => $customer->moshtari_code,
];
}
public function retry(Tahesab $tahesab): array
{
if (! $this->isFailed($tahesab) && ! empty($tahesab->tahesab_id)) {
return ['ok' => false, 'message' => 'این سند قبلاً در ته‌حساب ثبت شده است'];
}
$payload = json_decode((string) $tahesab->data, true) ?: [];
$handler = $payload['handler'] ?? $this->inferHandler($tahesab);
$controller = app(TahesabController::class);
$result = match ($handler) {
'wallet' => $controller->wallet(array_merge($payload, ['_retry_tahesab_id' => $tahesab->id]), $tahesab),
'walletGold' => $controller->walletGold(array_merge($payload, ['_retry_tahesab_id' => $tahesab->id]), $tahesab),
'storeTrade' => $controller->storeTrade(array_merge($payload, ['_retry_tahesab_id' => $tahesab->id]), $tahesab),
'goldReward' => $controller->goldReward(array_merge($payload, ['_retry_tahesab_id' => $tahesab->id]), $tahesab),
default => false,
};
$tahesab->refresh();
if ($result !== false && ! empty($tahesab->tahesab_id)) {
return ['ok' => true, 'message' => 'سند با موفقیت در ته‌حساب ثبت شد', 'tahesab' => $tahesab];
}
return [
'ok' => false,
'message' => $tahesab->sync_log ?: 'ارسال مجدد به ته‌حساب ناموفق بود',
'tahesab' => $tahesab,
];
}
private function inferHandler(Tahesab $tahesab): string
{
if ($tahesab->order_id) {
return 'storeTrade';
}
if ($tahesab->type === 'ريال') {
return 'wallet';
}
$desc = (string) $tahesab->desc;
if (str_contains($desc, 'شارژ کیف') || str_contains($desc, 'برداشت از کیف')) {
return 'walletGold';
}
return 'goldReward';
}
public function isSyncEnabled(): bool
{
$config = app(ConfigService::class);
return $config->isTahesabAccounting()
&& trim($config->getString('tahesab_host', '')) !== ''
&& trim($config->getString('tahesab_port', '')) !== ''
&& trim($config->getString('tahesab_token', '')) !== ''
&& trim($config->getString('tahesab_db', '')) !== '';
}
/** ردیف‌های محلی که هنوز در ته‌حساب ثبت نشده‌اند */
public function unsyncedRowsQuery()
{
return Tahesab::query()->where(function ($q) {
$q->where(function ($inner) {
$inner->whereNull('tahesab_id')->orWhere('tahesab_id', '');
})->orWhere('sync_status', self::STATUS_FAILED);
});
}
/**
* خلاصهٔ سندهای جاافتاده / ناموفق برای بنر ادمین.
*
* @return array{failed_rows:int,missing_withdraws:int,missing_orders:int,total:int,doc_sync:array}
*/
public function pendingSummary(?int $customerId = null): array
{
$hasTable = $this->ensureLocalTable();
$failedRows = $hasTable
? (int) $this->unsyncedRowsQuery()
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->count()
: 0;
$missingWithdraws = count($this->missingWithdrawIds(200, $customerId));
$missingOrders = count($this->missingOrderIds(200, $customerId));
return [
'failed_rows' => $failedRows,
'missing_withdraws' => $missingWithdraws,
'missing_orders' => $missingOrders,
'total' => $failedRows + $missingWithdraws + $missingOrders,
'doc_sync' => app(TahesabDocSyncService::class)->snapshot(),
];
}
/**
* تعداد واقعی جاافتاده‌ها بدون سقف ۲۰۰ — برای جاب ارسال همه.
*
* @return array{failed_rows:int,missing_withdraws:int,missing_orders:int,total:int}
*/
public function pendingTotals(?int $customerId = null): array
{
$hasTable = $this->ensureLocalTable();
$failedRows = $hasTable
? (int) $this->unsyncedRowsQuery()
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->count()
: 0;
$missingWithdraws = $this->countMissingWithdraws($customerId);
$missingOrders = $this->countMissingOrders($customerId);
return [
'failed_rows' => $failedRows,
'missing_withdraws' => $missingWithdraws,
'missing_orders' => $missingOrders,
'total' => $failedRows + $missingWithdraws + $missingOrders,
];
}
/**
* ته‌حساب خالی شده ولی ردیف محلی هنوز موفق است — کد ته‌حساب را پاک کن تا دوباره در صف بیاید.
*
* @return array{ok:bool,message:string,cleared:int,summary:array}
*/
public function resetLocalSyncedDocs(): array
{
$pump = app(TahesabDocSyncService::class);
if ($pump->isRunning()) {
return [
'ok' => false,
'message' => 'اول ارسال جاری را متوقف کنید',
'cleared' => 0,
'summary' => $this->pendingSummary(),
];
}
if (! $this->ensureLocalTable()) {
return [
'ok' => false,
'message' => 'جدول محلی ته‌حساب نیست',
'cleared' => 0,
'summary' => $this->pendingSummary(),
];
}
$query = Tahesab::query()->where(function ($q) {
$q->where(function ($inner) {
$inner->whereNotNull('tahesab_id')->where('tahesab_id', '!=', '');
});
if ($this->hasSyncColumns()) {
$q->orWhere('sync_status', self::STATUS_SUCCESS);
}
});
$cleared = (int) (clone $query)->count();
$update = ['tahesab_id' => null];
if ($this->hasSyncColumns()) {
$update['sync_status'] = self::STATUS_PENDING;
$update['sync_log'] = 'بازنشانی محلی — سند در ته‌حساب پاک شده';
}
if ($cleared > 0) {
(clone $query)->update($update);
}
$pump->clearIdleSnapshot();
return [
'ok' => true,
'message' => $cleared > 0
? ($cleared.' سند محلی دوباره در صف ارسال است')
: 'سند موفق محلی برای بازنشانی نبود',
'cleared' => $cleared,
'summary' => $this->pendingSummary(),
];
}
/**
* @param array{row?:list<int>,withdraw?:list<int>,order?:list<int>} $skip
* @return array{type:string,id:int,label:string}|null
*/
public function peekNextPending(array $skip = []): ?array
{
if ($this->ensureLocalTable()) {
$skipRow = array_values(array_filter($skip['row'] ?? []));
$row = $this->unsyncedRowsQuery()
->with('customer')
->when($skipRow !== [], fn ($q) => $q->whereNotIn('id', $skipRow))
->orderBy('id')
->first();
if ($row) {
$name = optional($row->customer)->name;
return [
'type' => 'row',
'id' => (int) $row->id,
'label' => trim('سند محلی #'.$row->id.($name ? ' — '.$name : '')),
];
}
}
$wIds = $this->missingWithdrawIds(1, null, array_values(array_filter($skip['withdraw'] ?? [])));
if ($wIds !== []) {
$id = (int) $wIds[0];
$w = \App\Models\Withdraw::query()->with('customer')->find($id);
$name = optional(optional($w)->customer)->name;
return [
'type' => 'withdraw',
'id' => $id,
'label' => trim('شارژ/برداشت #'.$id.($name ? ' — '.$name : '')),
];
}
$oIds = $this->missingOrderIds(1, null, array_values(array_filter($skip['order'] ?? [])));
if ($oIds !== []) {
$id = (int) $oIds[0];
$order = \App\Models\Order::query()->with('customer')->find($id);
$name = optional(optional($order)->customer)->name;
return [
'type' => 'order',
'id' => $id,
'label' => trim('سفارش #'.$id.($name ? ' — '.$name : '')),
];
}
return null;
}
/**
* لیست یکپارچهٔ خطا خورده + جاافتاده برای تب ادمین.
*
* @return list<array<string, mixed>>
*/
public function pendingDocs(?int $customerId = null, int $limit = 200): array
{
$limit = max(1, min(500, $limit));
$rows = [];
if ($this->ensureLocalTable()) {
$local = $this->unsyncedRowsQuery()
->with('customer')
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->orderByDesc('id')
->limit($limit)
->get();
foreach ($local as $row) {
$arr = $row->toArray();
$arr['issue_kind'] = 'failed';
$arr['sync_target'] = ['type' => 'row', 'id' => (int) $row->id];
$rows[] = $arr;
}
}
$remaining = $limit - count($rows);
if ($remaining > 0) {
$withdrawIds = $this->missingWithdrawIds($remaining, $customerId);
if ($withdrawIds !== []) {
$withdraws = \App\Models\Withdraw::query()
->with('customer')
->whereIn('id', $withdrawIds)
->orderByDesc('id')
->get();
foreach ($withdraws as $w) {
$cash = (float) ($w->cash ?? 0);
$gold = (float) ($w->gold ?? 0);
$isGold = abs($gold) > 1e-9;
$rows[] = [
'id' => null,
'customer_id' => $w->customer_id,
'customer' => $w->customer,
'amount' => $isGold ? abs($gold) : abs($cash),
'type' => $isGold ? 'طلا' : 'ريال',
'desc' => 'شارژ/برداشت بدون سند محلی #'.$w->id.($w->note ? ' — '.$w->note : ''),
'withdraw_id' => (int) $w->id,
'order_id' => null,
'sync_status' => 'missing',
'sync_log' => 'سند در ته‌حساب ساخته نشده (جاافتاده)',
'tahesab_id' => null,
'created_at' => optional($w->created_at)?->toDateTimeString(),
'issue_kind' => 'missing',
'sync_target' => ['type' => 'withdraw', 'id' => (int) $w->id],
];
}
}
}
$remaining = $limit - count($rows);
if ($remaining > 0) {
$orderIds = $this->missingOrderIds($remaining, $customerId);
if ($orderIds !== []) {
$orders = \App\Models\Order::query()
->with(['customer', 'product'])
->whereIn('id', $orderIds)
->orderByDesc('id')
->get();
foreach ($orders as $order) {
$direction = (int) ($order->direction ?? 0);
$dirLabel = $direction === 1 ? 'خرید' : ($direction === 2 ? 'فروش' : 'سفارش');
$productTitle = $order->product->title ?? 'محصول';
$rows[] = [
'id' => null,
'customer_id' => $order->customer_id,
'customer' => $order->customer,
'amount' => (float) ($order->amount ?? 0),
'type' => 'طلا',
'desc' => $dirLabel.' '.$productTitle.' بدون سند محلی #'.$order->id,
'withdraw_id' => null,
'order_id' => (int) $order->id,
'sync_status' => 'missing',
'sync_log' => 'سند در ته‌حساب ساخته نشده (جاافتاده)',
'tahesab_id' => null,
'created_at' => optional($order->pay_at ?? $order->created_at)?->toDateTimeString(),
'issue_kind' => 'missing',
'sync_target' => ['type' => 'order', 'id' => (int) $order->id],
];
}
}
}
usort($rows, function ($a, $b) {
return strcmp((string) ($b['created_at'] ?? ''), (string) ($a['created_at'] ?? ''));
});
return $rows;
}
/**
* ثبت یک مورد: ردیف محلی / شارژ جاافتاده / سفارش جاافتاده.
*
* @return array{ok:bool,message:string}
*/
public function syncOne(string $type, int $id): array
{
if (! $this->isSyncEnabled()) {
return ['ok' => false, 'message' => 'همگام‌سازی ته‌حساب فعال نیست یا تنظیمات ناقص است'];
}
$controller = app(TahesabController::class);
try {
if ($type === 'row') {
$row = Tahesab::with('customer')->findOrFail($id);
return $this->retry($row);
}
if ($type === 'withdraw') {
return $this->backfillWithdraw($controller, $id);
}
if ($type === 'order') {
return $this->backfillOrder($controller, $id);
}
return ['ok' => false, 'message' => 'نوع نامعتبر است'];
} catch (\Throwable $e) {
report($e);
return ['ok' => false, 'message' => $e->getMessage()];
}
}
/**
* ارسال مجدد همهٔ ناموفق‌ها + ساخت و ارسال سندهای جاافتاده از شارژ/سفارش.
*
* @return array{ok:bool,retried:int,backfilled_withdraws:int,backfilled_orders:int,failed:int,errors:list<string>,summary:array}
*/
public function syncAllPending(int $limit = 80, ?int $customerId = null): array
{
if (! $this->isSyncEnabled()) {
return [
'ok' => false,
'retried' => 0,
'backfilled_withdraws' => 0,
'backfilled_orders' => 0,
'failed' => 0,
'errors' => ['همگام‌سازی ته‌حساب فعال نیست یا تنظیمات ناقص است'],
'summary' => $this->pendingSummary($customerId),
];
}
$retried = 0;
$backfilledWithdraws = 0;
$backfilledOrders = 0;
$failed = 0;
$errors = [];
$controller = app(TahesabController::class);
$used = 0;
$rows = $this->unsyncedRowsQuery()
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->orderBy('id')
->limit($limit)
->get();
foreach ($rows as $row) {
$used++;
try {
$result = $this->retry($row);
if ($result['ok'] ?? false) {
$retried++;
} else {
$failed++;
$errors[] = 'سند #'.$row->id.': '.($result['message'] ?? 'ناموفق');
}
} catch (\Throwable $e) {
$failed++;
$errors[] = 'سند #'.$row->id.': '.$e->getMessage();
report($e);
}
}
foreach ($this->missingWithdrawIds(max(0, $limit - $used), $customerId) as $withdrawId) {
$used++;
try {
$sent = $this->backfillWithdraw($controller, (int) $withdrawId);
if ($sent['ok'] ?? false) {
$backfilledWithdraws++;
} else {
$failed++;
$errors[] = 'شارژ/برداشت #'.$withdrawId.': '.($sent['message'] ?? 'ارسال به ته‌حساب ناموفق');
}
} catch (\Throwable $e) {
$failed++;
$errors[] = 'شارژ/برداشت #'.$withdrawId.': '.$e->getMessage();
report($e);
}
}
foreach ($this->missingOrderIds(max(0, $limit - $used), $customerId) as $orderId) {
$used++;
try {
$sent = $this->backfillOrder($controller, (int) $orderId);
if ($sent['ok'] ?? false) {
$backfilledOrders++;
} else {
$failed++;
$errors[] = 'سفارش #'.$orderId.': '.($sent['message'] ?? 'ارسال به ته‌حساب ناموفق');
}
} catch (\Throwable $e) {
$failed++;
$errors[] = 'سفارش #'.$orderId.': '.$e->getMessage();
report($e);
}
}
return [
'ok' => $failed === 0,
'retried' => $retried,
'backfilled_withdraws' => $backfilledWithdraws,
'backfilled_orders' => $backfilledOrders,
'failed' => $failed,
'errors' => array_slice($errors, 0, 20),
'summary' => $this->pendingSummary($customerId),
];
}
/** @return list<int> */
private function missingWithdrawIds(int $limit, ?int $customerId = null, array $exclude = []): array
{
$query = $this->missingWithdrawQuery($customerId, $exclude);
if ($query === null || $limit <= 0) {
return [];
}
return $query
->orderBy('id')
->limit($limit)
->pluck('id')
->map(fn ($id) => (int) $id)
->all();
}
public function countMissingWithdraws(?int $customerId = null): int
{
$query = $this->missingWithdrawQuery($customerId);
return $query ? (int) $query->count() : 0;
}
private function missingWithdrawQuery(?int $customerId = null, array $exclude = [])
{
if (! Schema::hasTable('withdraws')) {
return null;
}
$query = \App\Models\Withdraw::query()
->where('sts', 4)
->where(function ($q) {
$q->where('cash', '!=', 0)->orWhere('gold', '!=', 0);
})
->where(function ($q) {
$q->whereNull('note')
->orWhere('note', 'not like', 'fix:intval-gold-loss%');
})
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->when($exclude !== [], fn ($q) => $q->whereNotIn('id', $exclude));
if (Schema::hasTable('tahesab')) {
$query->whereNotExists(function ($sub) {
$sub->from('tahesab')->selectRaw('1')->whereColumn('tahesab.withdraw_id', 'withdraws.id');
});
}
return $query;
}
/** @return list<int> */
private function missingOrderIds(int $limit, ?int $customerId = null, array $exclude = []): array
{
$query = $this->missingOrderQuery($customerId, $exclude);
if ($query === null || $limit <= 0) {
return [];
}
return $query
->orderBy('id')
->limit($limit)
->pluck('id')
->map(fn ($id) => (int) $id)
->all();
}
public function countMissingOrders(?int $customerId = null): int
{
$query = $this->missingOrderQuery($customerId);
return $query ? (int) $query->count() : 0;
}
private function missingOrderQuery(?int $customerId = null, array $exclude = [])
{
if (! Schema::hasTable('orders')) {
return null;
}
$query = \App\Models\Order::query()
->where('sts', 2)
->where('payment_sts', 4)
->whereIn('direction', [1, 2])
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->when($exclude !== [], fn ($q) => $q->whereNotIn('id', $exclude));
if (Schema::hasTable('tahesab')) {
$query->whereNotExists(function ($sub) {
$sub->from('tahesab')->selectRaw('1')->whereColumn('tahesab.order_id', 'orders.id');
});
}
return $query;
}
private function outcomeFromController($result, string $okMessage, string $failMessage): array
{
if (is_array($result)) {
return ['ok' => true, 'message' => $okMessage];
}
if (is_string($result) && $result !== '') {
return ['ok' => false, 'message' => $result];
}
return ['ok' => false, 'message' => $failMessage];
}
private function backfillWithdraw(TahesabController $controller, int $withdrawId): array
{
$withdraw = \App\Models\Withdraw::query()->find($withdrawId);
if (! $withdraw || ! $withdraw->customer_id) {
return ['ok' => false, 'message' => 'شارژ بدون مشتری است'];
}
$cash = (float) ($withdraw->cash ?? 0);
$gold = (float) ($withdraw->gold ?? 0);
if (abs($cash) > 1e-9) {
return $this->outcomeFromController(
$controller->wallet([
'customer_id' => (int) $withdraw->customer_id,
'value' => abs($cash),
'direction' => $cash >= 0 ? 0 : 1,
'withdraw_id' => $withdrawId,
'note' => $withdraw->note,
]),
'سند شارژ/برداشت ثبت شد',
'ارسال شارژ/برداشت ناموفق بود'
);
}
if (abs($gold) > 1e-9) {
return $this->outcomeFromController(
$controller->walletGold([
'customer_id' => (int) $withdraw->customer_id,
'value' => abs($gold),
'direction' => $gold >= 0 ? 1 : 0,
'withdraw_id' => $withdrawId,
'note' => $withdraw->note,
]),
'سند شارژ/برداشت ثبت شد',
'ارسال شارژ/برداشت ناموفق بود'
);
}
return ['ok' => false, 'message' => 'مبلغ شارژ صفر است'];
}
private function backfillOrder(TahesabController $controller, int $orderId): array
{
$order = \App\Models\Order::query()->with('product')->find($orderId);
if (! $order || ! $order->customer_id) {
return ['ok' => false, 'message' => 'سفارش بدون مشتری است'];
}
$product = $order->product;
$payload = [
'customer_id' => (int) $order->customer_id,
'amount' => (float) $order->amount,
'direction' => (int) $order->direction,
'price' => (float) $order->price,
'total_price' => (float) $order->total_price,
'pay_at' => $order->pay_at ?? $order->created_at,
'product_id' => $order->product_id ? (int) $order->product_id : null,
'product' => $product ? [
'id' => (int) $product->id,
'title' => $product->title,
'group_type' => $product->group_type,
'metal_id' => $product->metal_id,
'amount' => $product->amount,
'unit' => $product->unit,
'trade_by_quantity' => $product->trade_by_quantity,
] : ['metal_id' => 1],
'order' => ['id' => $orderId, 'product_id' => $order->product_id],
];
return $this->outcomeFromController(
$controller->storeTrade($payload),
'سند سفارش ثبت شد',
'ارسال سفارش ناموفق بود'
);
}
public function ensureLocalTable(): bool
{
if (Schema::hasTable('tahesab')) {
return true;
}
try {
Schema::create('tahesab', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('customer_id')->nullable()->index();
$table->unsignedBigInteger('order_id')->nullable()->index();
$table->unsignedBigInteger('withdraw_id')->nullable()->index();
$table->decimal('amount', 18, 4)->nullable();
$table->string('desc')->nullable();
$table->string('type', 32)->nullable();
$table->timestamp('date')->nullable();
$table->string('tahesab_id')->nullable()->index();
$table->string('sh_factor')->nullable();
$table->string('factor_number')->nullable();
$table->string('tah_id')->nullable();
$table->longText('data')->nullable();
$table->longText('res')->nullable();
$table->string('sync_status', 20)->default('pending');
$table->text('sync_log')->nullable();
$table->timestamps();
});
} catch (\Throwable $e) {
Log::warning('Could not create tahesab table', ['error' => $e->getMessage()]);
}
return Schema::hasTable('tahesab');
}
private function hasSyncColumns(): bool
{
return Schema::hasTable('tahesab') && Schema::hasColumn('tahesab', 'sync_status');
}
}