diff --git a/api/bulk_operation.php b/api/bulk_operation.php index 57fa6ec..bc3a14f 100644 --- a/api/bulk_operation.php +++ b/api/bulk_operation.php @@ -131,12 +131,19 @@ if (isset($result['error'])) { if ($inaccessibleCount > 0) { $message .= " ($inaccessibleCount skipped - no access)"; } - echo json_encode([ + $response = [ 'success' => true, 'operation_id' => $operationId, 'processed' => $result['processed'], 'failed' => $result['failed'], 'skipped' => $inaccessibleCount, 'message' => $message - ]); + ]; + // Best-effort batches (the default; see processBulkOperation()'s docblock) + // can partially fail — surface the per-ticket reasons so the admin isn't + // just told a count. The dashboard's bulkResultMessage() already expects this. + if (!empty($result['errors'])) { + $response['errors'] = $result['errors']; + } + echo json_encode($response); } diff --git a/models/BulkOperationsModel.php b/models/BulkOperationsModel.php index 47dee2c..afd64be 100644 --- a/models/BulkOperationsModel.php +++ b/models/BulkOperationsModel.php @@ -106,12 +106,18 @@ class BulkOperationsModel /** * Process a bulk operation * - * Uses database transaction to ensure atomicity - either all tickets - * are updated or none are (on failure, changes are rolled back). + * Runs the whole batch inside one database transaction, but by default + * ($atomic = false, which is what api/bulk_operation.php uses) that + * transaction is always committed: a per-ticket failure (e.g. a + * disallowed workflow transition) is recorded in $failed/$errors and + * skipped, while every other ticket in the batch still succeeds. This + * is a best-effort batch, not an all-or-nothing one — set $atomic to + * true to roll back the entire batch when any ticket fails. * * @param int $operationId Operation ID * @param bool $atomic If true, rollback all changes on any failure - * @return array Result with processed and failed counts + * @return array Result with processed/failed counts and an errors[] list of + * per-ticket failure reasons (surfaced to the admin by the caller) */ public function processBulkOperation($operationId, bool $atomic = false) {