jobs_partial_ship.php 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  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 ((int) $job['vendor_id'] !== $vendor_id) {
  27. http_response_code(403);
  28. echo 'Wrong vendor';
  29. return;
  30. }
  31. if (!in_array($job['status'], ['', 'Finished'], true)) {
  32. http_response_code(400);
  33. echo 'Can only partial-ship from open or finished status';
  34. return;
  35. }
  36. $current_qty = (int) $job['qty'];
  37. if ($partial >= $current_qty) {
  38. http_response_code(400);
  39. echo 'Partial quantity must be less than current quantity (' . $current_qty . ')';
  40. return;
  41. }
  42. // BEGIN IMMEDIATE + retry mirrors the apply_job_change pattern.
  43. $attempts = 0;
  44. while (true) {
  45. $attempts++;
  46. try {
  47. $pdo->exec('BEGIN IMMEDIATE');
  48. break;
  49. } catch (PDOException $e) {
  50. if ($attempts >= 50 || !is_busy_error($e)) throw $e;
  51. usleep(100000);
  52. }
  53. }
  54. try {
  55. $remaining = $current_qty - $partial;
  56. // The shipped portion as its own row.
  57. $ins = $pdo->prepare(
  58. "INSERT INTO jobs (vendor_id, job, material, description, qty, due_date, ack, status)
  59. VALUES (?, ?, ?, ?, ?, ?, '', 'Shipped')"
  60. );
  61. $ins->execute([
  62. $job['vendor_id'], $job['job'], $job['material'], $job['description'],
  63. $partial, $job['due_date']
  64. ]);
  65. $new_id = (int) $pdo->lastInsertId();
  66. // Deduct from the original; keep its current status.
  67. $upd = $pdo->prepare(
  68. "UPDATE jobs SET qty = ?, updated_at = datetime('now') WHERE id = ?"
  69. );
  70. $upd->execute([$remaining, $job_id]);
  71. // Audit trail covering both rows so the log page can reconstruct the split.
  72. $hist = $pdo->prepare(
  73. 'INSERT INTO job_history (job_id, field, old_value, new_value, actor)
  74. VALUES (?, ?, ?, ?, ?)'
  75. );
  76. // The new row was created (this is its first appearance). new_value is the
  77. // vendor slug, matching the convention from bin/jobs_add.php. The actor is
  78. // the vendor performing the partial ship, so it doubles as the vendor slug.
  79. $hist->execute([$new_id, 'created', null, $actor, $actor]);
  80. // The new row starts Shipped (transition from default '').
  81. $hist->execute([$new_id, 'status', '', 'Shipped', $actor]);
  82. // The original row lost qty.
  83. $hist->execute([$job_id, 'qty', (string) $current_qty, (string) $remaining, $actor]);
  84. // A linking marker so the log can show "split off N units into job #X".
  85. $hist->execute([$job_id, 'partial_ship', (string) $current_qty, (string) $partial . ' -> #' . $new_id, $actor]);
  86. $pdo->exec('COMMIT');
  87. echo 'Success';
  88. } catch (Throwable $e) {
  89. $pdo->exec('ROLLBACK');
  90. throw $e;
  91. }