|
|
@@ -0,0 +1,97 @@
|
|
|
+<?php
|
|
|
+require_once __DIR__ . '/../lib/identity.php';
|
|
|
+require_once __DIR__ . '/../lib/jobs.php';
|
|
|
+
|
|
|
+[$actor, $vendor_id] = resolve_request_actor();
|
|
|
+
|
|
|
+if ($actor === 'ICG') {
|
|
|
+ http_response_code(403);
|
|
|
+ echo 'Partial ship is a vendor action';
|
|
|
+ return;
|
|
|
+}
|
|
|
+
|
|
|
+$pdo = db();
|
|
|
+$job_id = (int) ($_POST['job_id'] ?? 0);
|
|
|
+$partial = (int) ($_POST['partial_qty'] ?? 0);
|
|
|
+
|
|
|
+if ($job_id <= 0 || $partial <= 0) {
|
|
|
+ http_response_code(400);
|
|
|
+ echo 'Bad parameters';
|
|
|
+ return;
|
|
|
+}
|
|
|
+
|
|
|
+$stmt = $pdo->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 ((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: new row's status, and the qty deduction on the original.
|
|
|
+ $hist = $pdo->prepare(
|
|
|
+ 'INSERT INTO job_history (job_id, field, old_value, new_value, actor)
|
|
|
+ VALUES (?, ?, ?, ?, ?)'
|
|
|
+ );
|
|
|
+ $hist->execute([$new_id, 'status', '', 'Shipped', $actor]);
|
|
|
+ $hist->execute([$job_id, 'qty', (string) $current_qty, (string) $remaining, $actor]);
|
|
|
+
|
|
|
+ $pdo->exec('COMMIT');
|
|
|
+ echo 'Success';
|
|
|
+} catch (Throwable $e) {
|
|
|
+ $pdo->exec('ROLLBACK');
|
|
|
+ throw $e;
|
|
|
+}
|