feat(tahesab): pump pending documents slowly from admin, not schedule

Send leftover orders and charges one-by-one with a gap so Tahesab is not rate-limited, skip already-synced rows, and expose start/stop plus progress on existing status APIs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-02 20:48:14 +03:30
co-authored by Cursor
parent 92d8f2e9b3
commit c5fd3b45fe
5 changed files with 531 additions and 23 deletions
+117 -23
View File
@@ -233,9 +233,75 @@ class TahesabSyncService
'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
{
$failedRows = (int) $this->unsyncedRowsQuery()
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->count();
$missingWithdraws = $this->countMissingWithdraws($customerId);
$missingOrders = $this->countMissingOrders($customerId);
return [
'failed_rows' => $failedRows,
'missing_withdraws' => $missingWithdraws,
'missing_orders' => $missingOrders,
'total' => $failedRows + $missingWithdraws + $missingOrders,
];
}
/**
* @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
{
$skipRow = array_values(array_filter($skip['row'] ?? []));
$row = $this->unsyncedRowsQuery()
->when($skipRow !== [], fn ($q) => $q->whereNotIn('id', $skipRow))
->orderBy('id')
->first();
if ($row) {
return [
'type' => 'row',
'id' => (int) $row->id,
'label' => 'سند محلی #'.$row->id,
];
}
$wIds = $this->missingWithdrawIds(1, null, array_values(array_filter($skip['withdraw'] ?? [])));
if ($wIds !== []) {
$id = (int) $wIds[0];
return [
'type' => 'withdraw',
'id' => $id,
'label' => 'شارژ/برداشت #'.$id,
];
}
$oIds = $this->missingOrderIds(1, null, array_values(array_filter($skip['order'] ?? [])));
if ($oIds !== []) {
$id = (int) $oIds[0];
return [
'type' => 'order',
'id' => $id,
'label' => 'سفارش #'.$id,
];
}
return null;
}
/**
* لیست یکپارچهٔ خطا خورده + جاافتاده برای تب ادمین.
*
@@ -466,17 +532,33 @@ class TahesabSyncService
}
/** @return list<int> */
private function missingWithdrawIds(int $limit, ?int $customerId = null): array
private function missingWithdrawIds(int $limit, ?int $customerId = null, array $exclude = []): array
{
if ($limit <= 0 || ! Schema::hasTable('withdraws')) {
$query = $this->missingWithdrawQuery($customerId, $exclude);
if ($query === null || $limit <= 0) {
return [];
}
$linked = Tahesab::query()
->whereNotNull('withdraw_id')
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->pluck('withdraw_id')
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;
}
return \App\Models\Withdraw::query()
->where('sts', 4)
@@ -488,7 +570,21 @@ class TahesabSyncService
->orWhere('note', 'not like', 'fix:intval-gold-loss%');
})
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->when($linked !== [], fn ($q) => $q->whereNotIn('id', $linked))
->when($exclude !== [], fn ($q) => $q->whereNotIn('id', $exclude))
->whereNotExists(function ($sub) {
$sub->from('tahesab')->selectRaw('1')->whereColumn('tahesab.withdraw_id', 'withdraws.id');
});
}
/** @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')
@@ -496,30 +592,28 @@ class TahesabSyncService
->all();
}
/** @return list<int> */
private function missingOrderIds(int $limit, ?int $customerId = null): array
public function countMissingOrders(?int $customerId = null): int
{
if ($limit <= 0 || ! Schema::hasTable('orders')) {
return [];
}
$query = $this->missingOrderQuery($customerId);
$linked = Tahesab::query()
->whereNotNull('order_id')
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
->pluck('order_id')
->all();
return $query ? (int) $query->count() : 0;
}
private function missingOrderQuery(?int $customerId = null, array $exclude = [])
{
if (! Schema::hasTable('orders')) {
return null;
}
return \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($linked !== [], fn ($q) => $q->whereNotIn('id', $linked))
->orderBy('id')
->limit($limit)
->pluck('id')
->map(fn ($id) => (int) $id)
->all();
->when($exclude !== [], fn ($q) => $q->whereNotIn('id', $exclude))
->whereNotExists(function ($sub) {
$sub->from('tahesab')->selectRaw('1')->whereColumn('tahesab.order_id', 'orders.id');
});
}
private function backfillWithdraw(TahesabController $controller, int $withdrawId): bool