added accounting failing handles
This commit is contained in:
@@ -258,6 +258,384 @@ class ZarinSyncService
|
||||
&& trim($config->getString('zarin_token', '')) !== '';
|
||||
}
|
||||
|
||||
public function unsyncedRowsQuery()
|
||||
{
|
||||
return Zarin::query()->where(function ($q) {
|
||||
$q->where(function ($inner) {
|
||||
$inner->whereNull('zarin_id')->orWhere('zarin_id', '');
|
||||
})->orWhere('sync_status', self::STATUS_FAILED);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{failed_rows:int,missing_withdraws:int,missing_orders:int,total:int}
|
||||
*/
|
||||
public function pendingSummary(?int $customerId = null): array
|
||||
{
|
||||
$failedRows = (int) $this->unsyncedRowsQuery()
|
||||
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
||||
->count();
|
||||
$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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function pendingDocs(?int $customerId = null, int $limit = 200): array
|
||||
{
|
||||
$limit = max(1, min(500, $limit));
|
||||
$rows = [];
|
||||
|
||||
$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' => 'سند در زرین ساخته نشده (جاافتاده)',
|
||||
'zarin_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' => 'سند در زرین ساخته نشده (جاافتاده)',
|
||||
'zarin_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(ZarinController::class);
|
||||
|
||||
try {
|
||||
if ($type === 'row') {
|
||||
$row = Zarin::with('customer')->findOrFail($id);
|
||||
|
||||
return $this->retry($row);
|
||||
}
|
||||
if ($type === 'withdraw') {
|
||||
$ok = $this->backfillWithdraw($controller, $id);
|
||||
|
||||
return ['ok' => $ok, 'message' => $ok ? 'سند شارژ/برداشت ثبت شد' : 'ارسال شارژ/برداشت ناموفق بود'];
|
||||
}
|
||||
if ($type === 'order') {
|
||||
$ok = $this->backfillOrder($controller, $id);
|
||||
|
||||
return ['ok' => $ok, 'message' => $ok ? 'سند سفارش ثبت شد' : 'ارسال سفارش ناموفق بود'];
|
||||
}
|
||||
|
||||
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(ZarinController::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 {
|
||||
$ok = $this->backfillWithdraw($controller, (int) $withdrawId);
|
||||
if ($ok) {
|
||||
$backfilledWithdraws++;
|
||||
} else {
|
||||
$failed++;
|
||||
$errors[] = 'شارژ/برداشت #'.$withdrawId.': ارسال به زرین ناموفق';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
$errors[] = 'شارژ/برداشت #'.$withdrawId.': '.$e->getMessage();
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->missingOrderIds(max(0, $limit - $used), $customerId) as $orderId) {
|
||||
$used++;
|
||||
try {
|
||||
$ok = $this->backfillOrder($controller, (int) $orderId);
|
||||
if ($ok) {
|
||||
$backfilledOrders++;
|
||||
} else {
|
||||
$failed++;
|
||||
$errors[] = 'سفارش #'.$orderId.': ارسال به زرین ناموفق';
|
||||
}
|
||||
} 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
|
||||
{
|
||||
if ($limit <= 0 || ! Schema::hasTable('withdraws')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$linked = Zarin::query()
|
||||
->whereNotNull('withdraw_id')
|
||||
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
||||
->pluck('withdraw_id')
|
||||
->all();
|
||||
|
||||
return \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($linked !== [], fn ($q) => $q->whereNotIn('id', $linked))
|
||||
->orderBy('id')
|
||||
->limit($limit)
|
||||
->pluck('id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->all();
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function missingOrderIds(int $limit, ?int $customerId = null): array
|
||||
{
|
||||
if ($limit <= 0 || ! Schema::hasTable('orders')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$linked = Zarin::query()
|
||||
->whereNotNull('order_id')
|
||||
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
||||
->pluck('order_id')
|
||||
->all();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private function backfillWithdraw(ZarinController $controller, int $withdrawId): bool
|
||||
{
|
||||
$withdraw = \App\Models\Withdraw::query()->find($withdrawId);
|
||||
if (! $withdraw || ! $withdraw->customer_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cash = (float) ($withdraw->cash ?? 0);
|
||||
$gold = (float) ($withdraw->gold ?? 0);
|
||||
|
||||
if (abs($cash) > 1e-9) {
|
||||
$result = $controller->wallet([
|
||||
'customer_id' => (int) $withdraw->customer_id,
|
||||
'value' => abs($cash),
|
||||
'direction' => $cash >= 0 ? 0 : 1,
|
||||
'withdraw_id' => $withdrawId,
|
||||
'note' => $withdraw->note,
|
||||
]);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
if (abs($gold) > 1e-9) {
|
||||
$result = $controller->walletGold([
|
||||
'customer_id' => (int) $withdraw->customer_id,
|
||||
'value' => abs($gold),
|
||||
'direction' => $gold >= 0 ? 1 : 0,
|
||||
'withdraw_id' => $withdrawId,
|
||||
'note' => $withdraw->note,
|
||||
]);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function backfillOrder(ZarinController $controller, int $orderId): bool
|
||||
{
|
||||
$order = \App\Models\Order::query()->with('product')->find($orderId);
|
||||
if (! $order || ! $order->customer_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$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],
|
||||
];
|
||||
|
||||
$result = $controller->storeTrade($payload);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
private function flattenResponse(array $response): array
|
||||
{
|
||||
$data = $response['data'] ?? null;
|
||||
|
||||
Reference in New Issue
Block a user