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:
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\TahesabDocSyncService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PumpTahesabDocSyncCommand extends Command
|
||||
{
|
||||
protected $signature = 'tahesab:pump-doc-sync';
|
||||
|
||||
protected $description = 'Pump pending Tahesab documents in the background (started from admin, not scheduled)';
|
||||
|
||||
public function handle(TahesabDocSyncService $service): int
|
||||
{
|
||||
$count = $service->runLoop();
|
||||
$this->info((string) $count);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -214,6 +215,7 @@ class TahesabController extends Controller
|
||||
'phone_conflicts' => $phoneConflicts,
|
||||
'panel_only_list' => $panelOnlyList,
|
||||
'tahesab_only_list' => $tahesabOnlyResult,
|
||||
'doc_sync' => app(TahesabDocSyncService::class)->snapshot(),
|
||||
];
|
||||
|
||||
// برای تشخیص نام دقیق فیلدها: ?include_tahesab=1&raw=1
|
||||
@@ -672,11 +674,27 @@ class TahesabController extends Controller
|
||||
|
||||
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 pendingDocs(Request $request)
|
||||
{
|
||||
$customerId = $request->filled('customer_id') ? (int) $request->input('customer_id') : null;
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* ارسال آرام همهٔ سندهای جاافتاده به تهحساب — بدون schedule.
|
||||
* استارت از API؛ حلقه با artisan در پسزمینه یا یک تیک روی pending-summary.
|
||||
*/
|
||||
class TahesabDocSyncService
|
||||
{
|
||||
public const CACHE_KEY = 'tahesab_doc_sync_job';
|
||||
|
||||
/** فاصله بین دو سند تا تهحساب بن نکند (ثانیه) */
|
||||
public const GAP_SECONDS = 2.5;
|
||||
|
||||
private const STALE_SECONDS = 8;
|
||||
|
||||
private const TTL_RUNNING = 21600;
|
||||
|
||||
private const TTL_DONE = 7200;
|
||||
|
||||
/** @var resource|null */
|
||||
private $lockHandle;
|
||||
|
||||
public function snapshot(): array
|
||||
{
|
||||
$job = $this->read();
|
||||
|
||||
return $this->publicSnapshot($job);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,message:string,doc_sync:array}
|
||||
*/
|
||||
public function start(): array
|
||||
{
|
||||
$sync = app(TahesabSyncService::class);
|
||||
if (! $sync->isSyncEnabled()) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'همگامسازی تهحساب فعال نیست یا تنظیمات ناقص است',
|
||||
'doc_sync' => $this->snapshot(),
|
||||
];
|
||||
}
|
||||
|
||||
$current = $this->read();
|
||||
if (($current['running'] ?? false) === true) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'ارسال سندها از قبل در جریان است',
|
||||
'doc_sync' => $this->publicSnapshot($current),
|
||||
];
|
||||
}
|
||||
|
||||
$totals = $sync->pendingTotals();
|
||||
$total = (int) ($totals['total'] ?? 0);
|
||||
if ($total <= 0) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'سند جاافتادهای برای ارسال نیست',
|
||||
'doc_sync' => $this->emptySnapshot(),
|
||||
];
|
||||
}
|
||||
|
||||
$job = [
|
||||
'running' => true,
|
||||
'stop' => false,
|
||||
'stopped' => false,
|
||||
'finished' => false,
|
||||
'total' => $total,
|
||||
'processed' => 0,
|
||||
'success' => 0,
|
||||
'failed' => 0,
|
||||
'current' => '',
|
||||
'errors' => [],
|
||||
'skip' => ['row' => [], 'withdraw' => [], 'order' => []],
|
||||
'started_at' => now()->toDateTimeString(),
|
||||
'finished_at' => null,
|
||||
'beat_at' => now()->toDateTimeString(),
|
||||
];
|
||||
$this->write($job, self::TTL_RUNNING);
|
||||
$this->spawnPump();
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'ارسال سندها شروع شد',
|
||||
'doc_sync' => $this->publicSnapshot($job),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,message:string,doc_sync:array}
|
||||
*/
|
||||
public function stop(): array
|
||||
{
|
||||
$job = $this->read();
|
||||
if (($job['running'] ?? false) !== true) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'ارسال در جریان نیست',
|
||||
'doc_sync' => $this->publicSnapshot($job),
|
||||
];
|
||||
}
|
||||
|
||||
$job['stop'] = true;
|
||||
$this->write($job, self::TTL_RUNNING);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'درخواست توقف ثبت شد',
|
||||
'doc_sync' => $this->publicSnapshot($job),
|
||||
];
|
||||
}
|
||||
|
||||
public function tickIfStale(): void
|
||||
{
|
||||
$job = $this->read();
|
||||
if (($job['running'] ?? false) !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$beat = $job['beat_at'] ?? null;
|
||||
if ($beat && now()->diffInSeconds(\Illuminate\Support\Carbon::parse($beat)) < self::STALE_SECONDS) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pumpOnce();
|
||||
}
|
||||
|
||||
public function runLoop(): int
|
||||
{
|
||||
$done = 0;
|
||||
while (true) {
|
||||
if (! $this->tryLock()) {
|
||||
return $done;
|
||||
}
|
||||
try {
|
||||
$cont = $this->pumpOnceUnlocked();
|
||||
} finally {
|
||||
$this->releaseLock();
|
||||
}
|
||||
if (! $cont) {
|
||||
break;
|
||||
}
|
||||
$done++;
|
||||
usleep((int) (self::GAP_SECONDS * 1_000_000));
|
||||
}
|
||||
|
||||
return $done;
|
||||
}
|
||||
|
||||
public function pumpOnce(): bool
|
||||
{
|
||||
if (! $this->tryLock()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return $this->pumpOnceUnlocked();
|
||||
} finally {
|
||||
$this->releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
private function pumpOnceUnlocked(): bool
|
||||
{
|
||||
$job = $this->read();
|
||||
if (($job['running'] ?? false) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($job['stop'] ?? false) === true) {
|
||||
$this->finish($job, true);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$sync = app(TahesabSyncService::class);
|
||||
$next = $sync->peekNextPending($job['skip'] ?? []);
|
||||
if ($next === null) {
|
||||
$this->finish($job, false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$job['current'] = (string) ($next['label'] ?? '');
|
||||
$job['beat_at'] = now()->toDateTimeString();
|
||||
$this->write($job, self::TTL_RUNNING);
|
||||
|
||||
$type = (string) $next['type'];
|
||||
$id = (int) $next['id'];
|
||||
$ok = false;
|
||||
$message = '';
|
||||
try {
|
||||
$result = $sync->syncOne($type, $id);
|
||||
$ok = (bool) ($result['ok'] ?? false);
|
||||
$message = (string) ($result['message'] ?? '');
|
||||
} catch (\Throwable $e) {
|
||||
$ok = false;
|
||||
$message = $e->getMessage();
|
||||
Log::warning('Tahesab doc sync item failed', [
|
||||
'type' => $type,
|
||||
'id' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $ok && $this->looksRateLimited($message)) {
|
||||
$job = $this->read();
|
||||
if (($job['running'] ?? false) !== true) {
|
||||
return false;
|
||||
}
|
||||
$job['current'] = 'محدودیت نرخ تهحساب — کمی صبر…';
|
||||
$job['beat_at'] = now()->toDateTimeString();
|
||||
$this->write($job, self::TTL_RUNNING);
|
||||
usleep(5_000_000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$job = $this->read();
|
||||
if (($job['running'] ?? false) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$job['processed'] = (int) ($job['processed'] ?? 0) + 1;
|
||||
if ($ok) {
|
||||
$job['success'] = (int) ($job['success'] ?? 0) + 1;
|
||||
} else {
|
||||
$job['failed'] = (int) ($job['failed'] ?? 0) + 1;
|
||||
$errors = is_array($job['errors'] ?? null) ? $job['errors'] : [];
|
||||
$errors[] = [
|
||||
'label' => $job['current'] ?: ($type.' #'.$id),
|
||||
'error' => $message !== '' ? $message : 'ناموفق',
|
||||
];
|
||||
$job['errors'] = array_slice($errors, -30);
|
||||
$skip = is_array($job['skip'] ?? null) ? $job['skip'] : ['row' => [], 'withdraw' => [], 'order' => []];
|
||||
$skip[$type] = array_values(array_unique(array_merge($skip[$type] ?? [], [$id])));
|
||||
$job['skip'] = $skip;
|
||||
}
|
||||
|
||||
$job['beat_at'] = now()->toDateTimeString();
|
||||
$this->write($job, self::TTL_RUNNING);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function finish(array $job, bool $stopped): void
|
||||
{
|
||||
$job['running'] = false;
|
||||
$job['finished'] = true;
|
||||
$job['stopped'] = $stopped;
|
||||
$job['stop'] = false;
|
||||
$job['current'] = '';
|
||||
$job['finished_at'] = now()->toDateTimeString();
|
||||
$job['beat_at'] = now()->toDateTimeString();
|
||||
$this->write($job, self::TTL_DONE);
|
||||
}
|
||||
|
||||
private function spawnPump(): void
|
||||
{
|
||||
if (\PHP_OS_FAMILY === 'Windows') {
|
||||
return;
|
||||
}
|
||||
|
||||
$php = PHP_BINARY;
|
||||
if (stripos((string) $php, 'fpm') !== false || ! is_executable((string) $php)) {
|
||||
$php = 'php';
|
||||
}
|
||||
$artisan = base_path('artisan');
|
||||
$cmd = 'nohup '.escapeshellarg($php).' '.escapeshellarg($artisan)
|
||||
.' tahesab:pump-doc-sync >/dev/null 2>&1 &';
|
||||
if (! function_exists('exec')) {
|
||||
Log::warning('Tahesab doc sync spawn skipped: exec disabled');
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
exec($cmd);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Tahesab doc sync spawn failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function tryLock(): bool
|
||||
{
|
||||
$path = storage_path('framework/cache/tahesab-doc-sync.lock');
|
||||
$dir = dirname($path);
|
||||
if (! is_dir($dir)) {
|
||||
@mkdir($dir, 0775, true);
|
||||
}
|
||||
$handle = fopen($path, 'c');
|
||||
if ($handle === false) {
|
||||
return false;
|
||||
}
|
||||
if (! flock($handle, LOCK_EX | LOCK_NB)) {
|
||||
fclose($handle);
|
||||
|
||||
return false;
|
||||
}
|
||||
$this->lockHandle = $handle;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function releaseLock(): void
|
||||
{
|
||||
if (! is_resource($this->lockHandle)) {
|
||||
$this->lockHandle = null;
|
||||
|
||||
return;
|
||||
}
|
||||
flock($this->lockHandle, LOCK_UN);
|
||||
fclose($this->lockHandle);
|
||||
$this->lockHandle = null;
|
||||
}
|
||||
|
||||
private function looksRateLimited(string $message): bool
|
||||
{
|
||||
$m = mb_strtolower($message);
|
||||
|
||||
return str_contains($m, '429')
|
||||
|| str_contains($m, 'too many')
|
||||
|| str_contains($m, 'rate limit')
|
||||
|| str_contains($m, 'تعداد درخواست');
|
||||
}
|
||||
|
||||
private function read(): array
|
||||
{
|
||||
$job = Cache::get(self::CACHE_KEY);
|
||||
|
||||
return is_array($job) ? $job : [];
|
||||
}
|
||||
|
||||
private function write(array $job, int $ttl): void
|
||||
{
|
||||
Cache::put(self::CACHE_KEY, $job, $ttl);
|
||||
}
|
||||
|
||||
private function emptySnapshot(): array
|
||||
{
|
||||
return $this->publicSnapshot([]);
|
||||
}
|
||||
|
||||
private function publicSnapshot(array $job): array
|
||||
{
|
||||
$running = (bool) ($job['running'] ?? false);
|
||||
$finished = (bool) ($job['finished'] ?? false);
|
||||
$total = (int) ($job['total'] ?? 0);
|
||||
$processed = (int) ($job['processed'] ?? 0);
|
||||
$percent = $total > 0 ? min(100, (int) round(($processed / $total) * 100)) : 0;
|
||||
if ($finished && ! ($job['stopped'] ?? false)) {
|
||||
$percent = 100;
|
||||
}
|
||||
|
||||
return [
|
||||
'running' => $running,
|
||||
'finished' => $finished,
|
||||
'stopped' => (bool) ($job['stopped'] ?? false),
|
||||
'percent' => $percent,
|
||||
'total' => $total,
|
||||
'processed' => $processed,
|
||||
'success' => (int) ($job['success'] ?? 0),
|
||||
'failed' => (int) ($job['failed'] ?? 0),
|
||||
'current' => (string) ($job['current'] ?? ''),
|
||||
'errors' => is_array($job['errors'] ?? null) ? $job['errors'] : [],
|
||||
'started_at' => $job['started_at'] ?? null,
|
||||
'finished_at' => $job['finished_at'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -247,6 +247,8 @@ Route::middleware('auth:user')->group(function (){
|
||||
Route::get('tahesab/pending-docs', [TahesabController::class, 'pendingDocs']);
|
||||
Route::post('tahesab/sync-one', [TahesabController::class, 'syncOne']);
|
||||
Route::post('tahesab/sync-all-pending', [TahesabController::class, 'syncAllPending']);
|
||||
Route::post('tahesab/doc-sync/start', [TahesabController::class, 'startDocSync']);
|
||||
Route::post('tahesab/doc-sync/stop', [TahesabController::class, 'stopDocSync']);
|
||||
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']);
|
||||
|
||||
Reference in New Issue
Block a user