Files
opensourcepos/app/Models/ImportBatch.php
objecttothis c365e6e6dd feat(jobs): add batch-level import context and queue pause support
- Add import_batch_contexts table plus ImportBatchContext model so
  per-batch data (e.g. Items CSV attribute definitions) is stored
  once and referenced by context_id, instead of duplicated into
  every queued row's job payload.
- Extend queue_jobs, queue_jobs_failed, and new queue_paused_jobs
  tables with batch_id/context_id columns and FKs (ON DELETE
  RESTRICT) back to import_batch_contexts, so context rows cannot
  be purged while any job still references them.
- Introduce ImportBatchService.pushRow() to centralize queuing a
  batch row with batch/context stamping and priority; update
  Customers and Items controllers to use it instead of calling
  importBatch->create() and queue->push() separately.
- Add queue_paused_jobs table and Jobs::postPauseJobs()/pauseQueues()
  to move pending jobs out of queue_jobs into a paused table per
  queue (all core queues or a selected subset), leaving in-flight
  jobs untouched (issue #3833 pause/resume design).
- Change Jobs::getEditJob()/postSaveJob() routes to accept separate
  source/id path segments instead of a combined uid string.
- Add test coverage for ImportBatchContext, ImportBatchService, and
  ImportBatch (including purge-retains-while-referenced guards).

Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-10-07 17:55:22 +04:00

114 lines
3.6 KiB
PHP

<?php
namespace App\Models;
use CodeIgniter\Model;
/**
* Tracks progress of a queued CSV import (issue #3833 Phase 3). Each row of
* an import is pushed as an independent job, so no single job knows it is
* the last one — increment() resolves this with an atomic counter, and only
* the job that pushes the batch to totals returns true from isComplete() so
* 'import_completed' fires exactly once.
*/
class ImportBatch extends Model
{
protected $table = 'import_batches';
protected $primaryKey = 'id';
protected $useAutoIncrement = false;
protected $useTimestamps = true;
protected $allowedFields = [
'id',
'type',
'total',
'completed',
'failed',
'status',
];
public function create(string $batchId, string $type, int $total): void
{
$this->insert([
'id' => $batchId,
'type' => $type,
'total' => $total,
'status' => $total > 0 ? 'processing' : 'completed',
]);
}
/**
* Atomically increments completed or failed for the batch and reports
* whether this call finished it. Locks the row for the duration of the
* transaction so concurrent workers incrementing the same batch can't
* both observe completion.
*
* @return bool True if this call completed the batch (completed + failed = total).
*/
public function increment(string $batchId, bool $rowFailed): bool
{
$this->db->transStart();
$sql = $this->db->table($this->table)
->where('id', $batchId)
->getCompiledSelect();
$batch = $this->db->query($sql . ' FOR UPDATE')->getRowArray();
$column = $rowFailed ? 'failed' : 'completed';
$this->db->table($this->table)
->where('id', $batchId)
->set($column, $column . ' + 1', false)
->update();
$completed = ($batch['completed'] ?? 0) + ($rowFailed ? 0 : 1);
$failed = ($batch['failed'] ?? 0) + ($rowFailed ? 1 : 0);
$total = $batch['total'] ?? 0;
$isComplete = ($completed + $failed) >= $total;
if ($isComplete) {
$this->db->table($this->table)
->where('id', $batchId)
->update(['status' => $failed > 0 ? 'partial' : 'completed']);
}
$this->db->transComplete();
return $isComplete;
}
/**
* Deletes batches finished more than $retentionDays ago, but only once no
* row referencing their batch_id remains in queue_jobs or
* queue_jobs_failed (e.g. a failed row kept alive by a longer failed-job
* retention window). 0 means keep until manual purge.
*/
public function purgeFinished(int $retentionDays): void
{
if ($retentionDays <= 0) {
return;
}
$cutoff = date('Y-m-d H:i:s', strtotime("-{$retentionDays} days"));
$candidates = $this->db->table($this->table)
->select('id')
->whereIn('status', ['completed', 'partial'])
->where('updated_at <=', $cutoff)
->get()
->getResultArray();
foreach ($candidates as $batch) {
$batchId = $batch['id'];
$stillReferenced = $this->db->table('queue_jobs')->where('batch_id', $batchId)->countAllResults() > 0
|| $this->db->table('queue_jobs_failed')->where('batch_id', $batchId)->countAllResults() > 0;
if (!$stillReferenced) {
$this->db->table($this->table)->where('id', $batchId)->delete();
model(ImportBatchContext::class)->deleteForBatch($batchId);
}
}
}
}