jobs_partial_ship.php 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. <?php
  2. require_once __DIR__ . '/../lib/identity.php';
  3. require_once __DIR__ . '/../lib/jobs.php';
  4. [$actor, $vendor_id] = resolve_request_actor();
  5. if ($actor === 'ICG') {
  6. http_response_code(403);
  7. echo 'Partial ship is a vendor action';
  8. return;
  9. }
  10. $pdo = db();
  11. $job_id = (int) ($_POST['job_id'] ?? 0);
  12. $partial = (int) ($_POST['partial_qty'] ?? 0);
  13. if ($job_id <= 0 || $partial <= 0) {
  14. http_response_code(400);
  15. echo 'Bad parameters';
  16. return;
  17. }
  18. $stmt = $pdo->prepare('SELECT * FROM jobs WHERE id = ?');
  19. $stmt->execute([$job_id]);
  20. $job = $stmt->fetch();
  21. if (!$job) {
  22. http_response_code(404);
  23. echo 'Job not found';
  24. return;
  25. }
  26. if (!empty($job['deleted_at'])) {
  27. http_response_code(410);
  28. echo 'Job has been deleted';
  29. return;
  30. }
  31. if ((int) $job['vendor_id'] !== $vendor_id) {
  32. http_response_code(403);
  33. echo 'Wrong vendor';
  34. return;
  35. }
  36. if (!in_array($job['status'], ['', 'Finished'], true)) {
  37. http_response_code(400);
  38. echo 'Can only partial-ship from open or finished status';
  39. return;
  40. }
  41. $current_qty = (int) $job['qty'];
  42. if ($partial >= $current_qty) {
  43. http_response_code(400);
  44. echo 'Partial quantity must be less than current quantity (' . $current_qty . ')';
  45. return;
  46. }
  47. // BEGIN IMMEDIATE + retry mirrors the apply_job_change pattern.
  48. $attempts = 0;
  49. while (true) {
  50. $attempts++;
  51. try {
  52. $pdo->exec('BEGIN IMMEDIATE');
  53. break;
  54. } catch (PDOException $e) {
  55. if ($attempts >= 50 || !is_busy_error($e)) throw $e;
  56. usleep(100000);
  57. }
  58. }
  59. try {
  60. $remaining = $current_qty - $partial;
  61. // The shipped portion as its own row.
  62. $ins = $pdo->prepare(
  63. "INSERT INTO jobs (vendor_id, job, material, description, qty, due_date, ack, status)
  64. VALUES (?, ?, ?, ?, ?, ?, '', 'Shipped')"
  65. );
  66. $ins->execute([
  67. $job['vendor_id'], $job['job'], $job['material'], $job['description'],
  68. $partial, $job['due_date']
  69. ]);
  70. $new_id = (int) $pdo->lastInsertId();
  71. // Deduct from the original; keep its current status.
  72. $upd = $pdo->prepare(
  73. "UPDATE jobs SET qty = ?, updated_at = datetime('now') WHERE id = ?"
  74. );
  75. $upd->execute([$remaining, $job_id]);
  76. // Audit trail covering both rows so the log page can reconstruct the split.
  77. $hist = $pdo->prepare(
  78. 'INSERT INTO job_history (job_id, field, old_value, new_value, actor)
  79. VALUES (?, ?, ?, ?, ?)'
  80. );
  81. // The new row was created (this is its first appearance). new_value is the
  82. // vendor slug, matching the convention from bin/jobs_add.php. The actor is
  83. // the vendor performing the partial ship, so it doubles as the vendor slug.
  84. $hist->execute([$new_id, 'created', null, $actor, $actor]);
  85. // The new row starts Shipped (transition from default '').
  86. $hist->execute([$new_id, 'status', '', 'Shipped', $actor]);
  87. // The original row lost qty.
  88. $hist->execute([$job_id, 'qty', (string) $current_qty, (string) $remaining, $actor]);
  89. // A linking marker so the log can show "split off N units into job #X".
  90. $hist->execute([$job_id, 'partial_ship', (string) $current_qty, (string) $partial . ' -> #' . $new_id, $actor]);
  91. $pdo->exec('COMMIT');
  92. echo 'Success';
  93. } catch (Throwable $e) {
  94. $pdo->exec('ROLLBACK');
  95. throw $e;
  96. }