prepare('SELECT * FROM jobs WHERE id = ?'); $stmt->execute([$job_id]); $job = $stmt->fetch(); if (!$job) { http_response_code(404); echo 'Job not found'; return; } if (!empty($job['deleted_at'])) { http_response_code(410); echo 'Job has been deleted'; return; } if ((int) $job['vendor_id'] !== $vendor_id) { http_response_code(403); echo 'Wrong vendor'; return; } if (!in_array($job['status'], ['', 'Finished'], true)) { http_response_code(400); echo 'Can only partial-ship from open or finished status'; return; } $current_qty = (int) $job['qty']; if ($partial >= $current_qty) { http_response_code(400); echo 'Partial quantity must be less than current quantity (' . $current_qty . ')'; return; } // BEGIN IMMEDIATE + retry mirrors the apply_job_change pattern. $attempts = 0; while (true) { $attempts++; try { $pdo->exec('BEGIN IMMEDIATE'); break; } catch (PDOException $e) { if ($attempts >= 50 || !is_busy_error($e)) throw $e; usleep(100000); } } try { $remaining = $current_qty - $partial; // The shipped portion as its own row. $ins = $pdo->prepare( "INSERT INTO jobs (vendor_id, job, material, description, qty, due_date, ack, status) VALUES (?, ?, ?, ?, ?, ?, '', 'Shipped')" ); $ins->execute([ $job['vendor_id'], $job['job'], $job['material'], $job['description'], $partial, $job['due_date'] ]); $new_id = (int) $pdo->lastInsertId(); // Deduct from the original; keep its current status. $upd = $pdo->prepare( "UPDATE jobs SET qty = ?, updated_at = datetime('now') WHERE id = ?" ); $upd->execute([$remaining, $job_id]); // Audit trail covering both rows so the log page can reconstruct the split. $hist = $pdo->prepare( 'INSERT INTO job_history (job_id, field, old_value, new_value, actor) VALUES (?, ?, ?, ?, ?)' ); // The new row was created (this is its first appearance). new_value is the // vendor slug, matching the convention from bin/jobs_add.php. The actor is // the vendor performing the partial ship, so it doubles as the vendor slug. $hist->execute([$new_id, 'created', null, $actor, $actor]); // The new row starts Shipped (transition from default ''). $hist->execute([$new_id, 'status', '', 'Shipped', $actor]); // The original row lost qty. $hist->execute([$job_id, 'qty', (string) $current_qty, (string) $remaining, $actor]); // A linking marker so the log can show "split off N units into job #X". $hist->execute([$job_id, 'partial_ship', (string) $current_qty, (string) $partial . ' -> #' . $new_id, $actor]); $pdo->exec('COMMIT'); echo 'Success'; } catch (Throwable $e) { $pdo->exec('ROLLBACK'); throw $e; }