@naisys/erp 3.0.0-beta.23 → 3.0.0-beta.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,7 @@
33
33
  <meta name="format-detection" content="telephone=no" />
34
34
 
35
35
  <title>NAISYS ERP</title>
36
- <script type="module" crossorigin src="/erp/assets/index-DycEn-R_.js"></script>
36
+ <script type="module" crossorigin src="/erp/assets/index-ey6Ixpe7.js"></script>
37
37
  <link rel="modulepreload" crossorigin href="/erp/assets/rolldown-runtime-CvHMtSRF.js">
38
38
  <link rel="modulepreload" crossorigin href="/erp/assets/vendor-MNFI7PUp.js">
39
39
  <link rel="stylesheet" crossorigin href="/erp/assets/vendor-CLUPjUnv.css">
@@ -34,6 +34,7 @@ export function requirePermission(permission) {
34
34
  statusCode: 403,
35
35
  error: "Forbidden",
36
36
  message: `Permission '${permission}' required`,
37
+ missingPermission: permission,
37
38
  });
38
39
  return;
39
40
  }
@@ -2,6 +2,9 @@ function send(reply, statusCode, error, message) {
2
2
  reply.status(statusCode);
3
3
  return { statusCode, error, message };
4
4
  }
5
+ export function badRequest(reply, message) {
6
+ return send(reply, 400, "Bad Request", message);
7
+ }
5
8
  export function notFound(reply, message) {
6
9
  return send(reply, 404, "Not Found", message);
7
10
  }
@@ -1,6 +1,36 @@
1
1
  import { InventoryListQuerySchema, InventoryListResponseSchema, } from "@naisys/erp-shared";
2
+ import { hasPermission } from "../auth-middleware.js";
2
3
  import erpDb from "../erpDb.js";
3
4
  import { API_PREFIX, paginationLinks } from "../hateoas.js";
5
+ function buildInventoryActionTemplates(user) {
6
+ const templates = [
7
+ {
8
+ rel: "viewInstance",
9
+ hrefTemplate: `${API_PREFIX}/items/{itemKey}/instances/{id}`,
10
+ method: "GET",
11
+ title: "View Instance",
12
+ },
13
+ ];
14
+ if (hasPermission(user, "item_manager")) {
15
+ templates.push({
16
+ rel: "update-field-value",
17
+ hrefTemplate: `${API_PREFIX}/items/{itemKey}/instances/{id}/fields/{fieldSeqNo}`,
18
+ method: "PUT",
19
+ title: "Update Field Value (implicit set 0)",
20
+ schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
21
+ body: { value: "" },
22
+ });
23
+ templates.push({
24
+ rel: "update-set-field-value",
25
+ hrefTemplate: `${API_PREFIX}/items/{itemKey}/instances/{id}/sets/{setIndex}/fields/{fieldSeqNo}`,
26
+ method: "PUT",
27
+ title: "Update Field Value (explicit set index)",
28
+ schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
29
+ body: { value: "" },
30
+ });
31
+ }
32
+ return templates;
33
+ }
4
34
  export default function inventoryRoutes(fastify) {
5
35
  const app = fastify.withTypeProvider();
6
36
  app.get("/", {
@@ -55,14 +85,7 @@ export default function inventoryRoutes(fastify) {
55
85
  _links: paginationLinks("inventory", page, pageSize, total, {
56
86
  search,
57
87
  }),
58
- _actionTemplates: [
59
- {
60
- rel: "viewInstance",
61
- hrefTemplate: `${API_PREFIX}/items/{itemKey}/instances/{id}`,
62
- method: "GET",
63
- title: "View Instance",
64
- },
65
- ],
88
+ _actionTemplates: buildInventoryActionTemplates(request.erpUser),
66
89
  };
67
90
  },
68
91
  });
@@ -4,7 +4,7 @@ import { hasPermission, requirePermission } from "../auth-middleware.js";
4
4
  import erpDb from "../erpDb.js";
5
5
  import { notFound } from "../error-handler.js";
6
6
  import { API_PREFIX, selfLink } from "../hateoas.js";
7
- import { calcNextSeqNo, childItemLinks, formatAuditFields, mutationResult, } from "../route-helpers.js";
7
+ import { calcNextSeqNo, childItemLinks, formatAuditFields, mutationResult, permGate, } from "../route-helpers.js";
8
8
  import { createField, deleteField, ensureFieldSet, findExistingField, getField, listFields, updateField, } from "../services/field-service.js";
9
9
  import { findExisting as findExistingItem } from "../services/item-service.js";
10
10
  const ParamsSchema = z.object({ key: z.string() });
@@ -27,23 +27,26 @@ function formatField(key, user, field) {
27
27
  required: field.required,
28
28
  ...formatAuditFields(field),
29
29
  _links: childItemLinks(base, field.seqNo, "Fields", `/items/${key}`, "Item", "Field"),
30
- _actions: hasPermission(user, "item_manager")
31
- ? [
30
+ _actions: (() => {
31
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
32
+ return [
32
33
  {
33
34
  rel: "update",
34
35
  href: `${API_PREFIX}${base}/${field.seqNo}`,
35
36
  method: "PUT",
36
37
  title: "Update",
37
38
  schema: `${API_PREFIX}/schemas/UpdateField`,
39
+ ...gate,
38
40
  },
39
41
  {
40
42
  rel: "delete",
41
43
  href: `${API_PREFIX}${base}/${field.seqNo}`,
42
44
  method: "DELETE",
43
45
  title: "Delete",
46
+ ...gate,
44
47
  },
45
- ]
46
- : [],
48
+ ];
49
+ })(),
47
50
  };
48
51
  }
49
52
  export default function itemFieldRoutes(fastify) {
@@ -81,17 +84,16 @@ export default function itemFieldRoutes(fastify) {
81
84
  hrefTemplate: `${API_PREFIX}${base}/{seqNo}`,
82
85
  },
83
86
  ],
84
- _actions: hasPermission(request.erpUser, "item_manager")
85
- ? [
86
- {
87
- rel: "create",
88
- href: `${API_PREFIX}${base}`,
89
- method: "POST",
90
- title: "Add Field",
91
- schema: `${API_PREFIX}/schemas/CreateField`,
92
- },
93
- ]
94
- : [],
87
+ _actions: [
88
+ {
89
+ rel: "create",
90
+ href: `${API_PREFIX}${base}`,
91
+ method: "POST",
92
+ title: "Add Field",
93
+ schema: `${API_PREFIX}/schemas/CreateField`,
94
+ ...permGate(hasPermission(request.erpUser, "item_manager"), "item_manager"),
95
+ },
96
+ ],
95
97
  };
96
98
  },
97
99
  });
@@ -3,7 +3,7 @@ import { z } from "zod/v4";
3
3
  import { hasPermission, requirePermission } from "../auth-middleware.js";
4
4
  import { notFound, unprocessable } from "../error-handler.js";
5
5
  import { API_PREFIX, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
- import { formatAuditFields, mutationResult, useFullSerializer, wantsFullResponse, } from "../route-helpers.js";
6
+ import { formatAuditFields, mutationResult, permGate, useFullSerializer, wantsFullResponse, } from "../route-helpers.js";
7
7
  import { checkFieldValueShape, deleteFieldValueSet, deserializeFieldValue, serializeFieldValue, upsertFieldValue, validateFieldValue, } from "../services/field-value-service.js";
8
8
  import { createItemInstance, deleteItemInstance, ensureItemInstanceFieldRecord, findItemInstance, findItemInstanceWithField, listItemInstances, updateItemInstance, } from "../services/item-instance-service.js";
9
9
  import { findExisting as findItem } from "../services/item-service.js";
@@ -59,9 +59,8 @@ function instanceLinks(itemKey, instanceId, inst) {
59
59
  return links;
60
60
  }
61
61
  function instanceActions(itemKey, instanceId, user) {
62
- if (!hasPermission(user, "item_manager"))
63
- return [];
64
62
  const href = `${API_PREFIX}/${instanceBasePath(itemKey)}/${instanceId}`;
63
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
65
64
  return [
66
65
  {
67
66
  rel: "update",
@@ -70,12 +69,14 @@ function instanceActions(itemKey, instanceId, user) {
70
69
  title: "Update",
71
70
  schema: `${API_PREFIX}/schemas/UpdateItemInstance`,
72
71
  body: { key: "" },
72
+ ...gate,
73
73
  },
74
74
  {
75
75
  rel: "delete",
76
76
  href,
77
77
  method: "DELETE",
78
78
  title: "Delete",
79
+ ...gate,
79
80
  },
80
81
  ];
81
82
  }
@@ -122,17 +123,35 @@ function buildFieldValues(inst) {
122
123
  return fieldValues;
123
124
  }
124
125
  function buildActionTemplates(itemKey, instanceId, user, hasFields) {
125
- if (!hasPermission(user, "item_manager") || !hasFields)
126
+ if (!hasFields)
126
127
  return [];
127
128
  const instanceHref = `${API_PREFIX}/${instanceBasePath(itemKey)}/${instanceId}`;
129
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
128
130
  return [
129
131
  {
130
- rel: "updateField",
132
+ rel: "update-field-value",
131
133
  hrefTemplate: `${instanceHref}/fields/{fieldSeqNo}`,
132
134
  method: "PUT",
133
- title: "Update Field Value",
135
+ title: "Update Field Value (implicit set 0)",
136
+ schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
137
+ body: { value: "" },
138
+ ...gate,
139
+ },
140
+ {
141
+ rel: "update-set-field-value",
142
+ hrefTemplate: `${instanceHref}/sets/{setIndex}/fields/{fieldSeqNo}`,
143
+ method: "PUT",
144
+ title: "Update Field Value (explicit set index)",
134
145
  schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
135
146
  body: { value: "" },
147
+ ...gate,
148
+ },
149
+ {
150
+ rel: "delete-set",
151
+ hrefTemplate: `${instanceHref}/sets/{setIndex}`,
152
+ method: "DELETE",
153
+ title: "Delete Field Value Set",
154
+ ...gate,
136
155
  },
137
156
  ];
138
157
  }
@@ -202,18 +221,17 @@ export default function itemInstanceRoutes(fastify) {
202
221
  hrefTemplate: `${API_PREFIX}/items/${key}/instances/{id}`,
203
222
  },
204
223
  ],
205
- _actions: hasPermission(request.erpUser, "item_manager")
206
- ? [
207
- {
208
- rel: "create",
209
- href: `${API_PREFIX}/${base}`,
210
- method: "POST",
211
- title: "Create Instance",
212
- schema: `${API_PREFIX}/schemas/CreateItemInstance`,
213
- body: { key: "" },
214
- },
215
- ]
216
- : [],
224
+ _actions: [
225
+ {
226
+ rel: "create",
227
+ href: `${API_PREFIX}/${base}`,
228
+ method: "POST",
229
+ title: "Create Instance",
230
+ schema: `${API_PREFIX}/schemas/CreateItemInstance`,
231
+ body: { key: "" },
232
+ ...permGate(hasPermission(request.erpUser, "item_manager"), "item_manager"),
233
+ },
234
+ ],
217
235
  };
218
236
  },
219
237
  });
@@ -3,7 +3,7 @@ import { z } from "zod/v4";
3
3
  import { hasPermission, requirePermission } from "../auth-middleware.js";
4
4
  import { notFound } from "../error-handler.js";
5
5
  import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
- import { calcNextSeqNo, childItemLinks, formatAuditFields, mutationResult, } from "../route-helpers.js";
6
+ import { calcNextSeqNo, childItemLinks, formatAuditFields, mutationResult, permGate, } from "../route-helpers.js";
7
7
  import { createItem, deleteItem, findExisting, listItems, updateItem, } from "../services/item-service.js";
8
8
  const RESOURCE = "items";
9
9
  const KeyParamsSchema = z.object({
@@ -17,9 +17,8 @@ function itemLinks(key) {
17
17
  ];
18
18
  }
19
19
  function itemActions(key, user) {
20
- if (!hasPermission(user, "item_manager"))
21
- return [];
22
20
  const href = `${API_PREFIX}/${RESOURCE}/${key}`;
21
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
23
22
  return [
24
23
  {
25
24
  rel: "update",
@@ -27,12 +26,14 @@ function itemActions(key, user) {
27
26
  method: "PUT",
28
27
  title: "Update",
29
28
  schema: `${API_PREFIX}/schemas/UpdateItem`,
29
+ ...gate,
30
30
  },
31
31
  {
32
32
  rel: "delete",
33
33
  href,
34
34
  method: "DELETE",
35
35
  title: "Delete",
36
+ ...gate,
36
37
  },
37
38
  ];
38
39
  }
@@ -44,17 +45,16 @@ function formatItemFieldListResponse(itemKey, user, fields) {
44
45
  total: fields.length,
45
46
  nextSeqNo: calcNextSeqNo(maxSeq),
46
47
  _links: [selfLink(base)],
47
- _actions: hasPermission(user, "item_manager")
48
- ? [
49
- {
50
- rel: "create",
51
- href: `${API_PREFIX}${base}`,
52
- method: "POST",
53
- title: "Add Field",
54
- schema: `${API_PREFIX}/schemas/CreateField`,
55
- },
56
- ]
57
- : [],
48
+ _actions: [
49
+ {
50
+ rel: "create",
51
+ href: `${API_PREFIX}${base}`,
52
+ method: "POST",
53
+ title: "Add Field",
54
+ schema: `${API_PREFIX}/schemas/CreateField`,
55
+ ...permGate(hasPermission(user, "item_manager"), "item_manager"),
56
+ },
57
+ ],
58
58
  };
59
59
  }
60
60
  function formatItemField(itemKey, user, field) {
@@ -69,23 +69,26 @@ function formatItemField(itemKey, user, field) {
69
69
  required: field.required,
70
70
  ...formatAuditFields(field),
71
71
  _links: childItemLinks(base, field.seqNo, "Fields", `/items/${itemKey}`, "Item", "Field"),
72
- _actions: hasPermission(user, "item_manager")
73
- ? [
72
+ _actions: (() => {
73
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
74
+ return [
74
75
  {
75
76
  rel: "update",
76
77
  href: `${API_PREFIX}${base}/${field.seqNo}`,
77
78
  method: "PUT",
78
79
  title: "Update",
79
80
  schema: `${API_PREFIX}/schemas/UpdateField`,
81
+ ...gate,
80
82
  },
81
83
  {
82
84
  rel: "delete",
83
85
  href: `${API_PREFIX}${base}/${field.seqNo}`,
84
86
  method: "DELETE",
85
87
  title: "Delete",
88
+ ...gate,
86
89
  },
87
- ]
88
- : [],
90
+ ];
91
+ })(),
89
92
  };
90
93
  }
91
94
  function formatItem(item, user) {
@@ -139,17 +142,16 @@ export default function itemRoutes(fastify) {
139
142
  _linkTemplates: [
140
143
  { rel: "item", hrefTemplate: `${API_PREFIX}/items/{key}` },
141
144
  ],
142
- _actions: hasPermission(request.erpUser, "item_manager")
143
- ? [
144
- {
145
- rel: "create",
146
- href: `${API_PREFIX}/${RESOURCE}`,
147
- method: "POST",
148
- title: "Create Item",
149
- schema: `${API_PREFIX}/schemas/CreateItem`,
150
- },
151
- ]
152
- : [],
145
+ _actions: [
146
+ {
147
+ rel: "create",
148
+ href: `${API_PREFIX}/${RESOURCE}`,
149
+ method: "POST",
150
+ title: "Create Item",
151
+ schema: `${API_PREFIX}/schemas/CreateItem`,
152
+ ...permGate(hasPermission(request.erpUser, "item_manager"), "item_manager"),
153
+ },
154
+ ],
153
155
  };
154
156
  },
155
157
  });
@@ -72,14 +72,13 @@ function laborTicketListActions(orderKey, runNo, seqNo, opRunStatus, user, ticke
72
72
  return actions;
73
73
  }
74
74
  function laborTicketActionTemplates(orderKey, runNo, seqNo, user) {
75
- if (!hasPermission(user, "order_manager"))
76
- return [];
77
75
  return [
78
76
  {
79
77
  rel: "deleteTicket",
80
78
  hrefTemplate: `${API_PREFIX}/${laborResource(orderKey, runNo, seqNo)}/{ticketId}`,
81
79
  method: "DELETE",
82
80
  title: "Delete Ticket",
81
+ ...permGate(hasPermission(user, "order_manager"), "order_manager"),
83
82
  },
84
83
  ];
85
84
  }
@@ -113,10 +113,11 @@ export default function operationRunTransitionRoutes(fastify) {
113
113
  });
114
114
  },
115
115
  });
116
- // SKIP (pending → skipped)
116
+ // SKIP (blocked/pending/in_progress → skipped)
117
117
  app.post("/:seqNo/skip", {
118
118
  schema: {
119
- description: "Skip an operation run (pending → skipped)",
119
+ description: "Skip an operation run (blocked/pending/in_progress → skipped). " +
120
+ "When skipping an in_progress op, any open labor tickets are clocked out.",
120
121
  tags: ["Operation Runs"],
121
122
  params: SeqNoParamsSchema,
122
123
  body: TransitionNoteSchema,
@@ -143,9 +144,15 @@ export default function operationRunTransitionRoutes(fastify) {
143
144
  const statusErr = validateStatusFor("skip", resolved.opRun.status, [
144
145
  OperationRunStatus.blocked,
145
146
  OperationRunStatus.pending,
147
+ OperationRunStatus.in_progress,
146
148
  ]);
147
149
  if (statusErr)
148
150
  return conflict(reply, statusErr);
151
+ // If the op was in progress, close out any active labor tickets so the
152
+ // recorded cost is accurate before we mark the op skipped.
153
+ if (resolved.opRun.status === OperationRunStatus.in_progress) {
154
+ await clockOutAllForOpRun(resolved.opRun.id, userId);
155
+ }
149
156
  const cost = await sumLaborTicketCosts(resolved.opRun.id);
150
157
  const opRun = await transitionStatus(resolved.opRun.id, "skip", resolved.opRun.status, OperationRunStatus.skipped, userId, { ...(cost > 0 ? { cost } : undefined), statusNote: note ?? null });
151
158
  await unblockSuccessors(resolved.run.id, resolved.opRun.operationId, userId);
@@ -76,7 +76,11 @@ async function opRunItemActions(orderKey, runNo, seqNo, opRunId, operationId, st
76
76
  method: "POST",
77
77
  title: "Skip",
78
78
  permission: "order_manager",
79
- statuses: [OperationRunStatus.blocked, OperationRunStatus.pending],
79
+ statuses: [
80
+ OperationRunStatus.blocked,
81
+ OperationRunStatus.pending,
82
+ OperationRunStatus.in_progress,
83
+ ],
80
84
  disabledWhen: () => wcErr,
81
85
  },
82
86
  {
@@ -1,6 +1,6 @@
1
1
  import { CompleteOrderRunSchema, ErrorResponseSchema, OrderRunStatus, OrderRunTransitionSchema, } from "@naisys/erp-shared";
2
2
  import { requirePermission } from "../auth-middleware.js";
3
- import { conflict, notFound, unprocessable } from "../error-handler.js";
3
+ import { badRequest, conflict, notFound, unprocessable, } from "../error-handler.js";
4
4
  import { resolveOrderRun, useFullSerializer, wantsFullResponse, } from "../route-helpers.js";
5
5
  import { checkOpsComplete, completeOrderRun, getReopenTarget, sumOpRunCosts, transitionStatus, validateStatusFor, } from "../services/order-run-service.js";
6
6
  import { formatRun, orderRunItemActions, RunNoParamsSchema, } from "./order-runs.js";
@@ -89,12 +89,15 @@ export default function orderRunTransitionRoutes(fastify) {
89
89
  // COMPLETE (started -> closed, creates item instance)
90
90
  app.post("/:runNo/complete", {
91
91
  schema: {
92
- description: "Complete an order run — creates an item instance and closes the run",
92
+ description: "Complete an order run — creates an item instance and closes the run. " +
93
+ "Returns 400 if any required item field is missing, or if any supplied " +
94
+ "fieldSeqNo doesn't exist on the item.",
93
95
  tags: ["Order Runs"],
94
96
  params: RunNoParamsSchema,
95
97
  body: CompleteOrderRunSchema,
96
98
  response: {
97
99
  200: OrderRunTransitionSchema,
100
+ 400: ErrorResponseSchema,
98
101
  404: ErrorResponseSchema,
99
102
  409: ErrorResponseSchema,
100
103
  422: ErrorResponseSchema,
@@ -119,6 +122,9 @@ export default function orderRunTransitionRoutes(fastify) {
119
122
  const userId = request.erpUser.id;
120
123
  const result = await completeOrderRun(resolved.run.id, resolved.order.id, request.body, userId);
121
124
  if (result.error) {
125
+ if (result.status === 400) {
126
+ return badRequest(reply, result.error);
127
+ }
122
128
  return unprocessable(reply, result.error);
123
129
  }
124
130
  const run = result.run;
@@ -4,7 +4,7 @@ import { hasPermission, requirePermission } from "../auth-middleware.js";
4
4
  import { conflict, notFound } from "../error-handler.js";
5
5
  import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
6
  import { formatAuditFields, mutationResult, permGate, resolveActions, } from "../route-helpers.js";
7
- import { checkHasRevisions, createOrder, deleteOrder, findExisting, listOrders, resolveItemKey, updateOrder, } from "../services/order-service.js";
7
+ import { checkHasRevisions, createOrder, deleteOrder, findExisting, getLatestApprovedRevNo, listOrders, resolveItemKey, updateOrder, } from "../services/order-service.js";
8
8
  function orderLinks(resource, key, schemaName) {
9
9
  return [
10
10
  selfLink(`/${resource}/${key}`),
@@ -64,13 +64,14 @@ const RESOURCE = "orders";
64
64
  const KeyParamsSchema = z.object({
65
65
  key: z.string(),
66
66
  });
67
- function formatOrder(order, user) {
67
+ function formatOrder(order, user, latestApprovedRevNo) {
68
68
  return {
69
69
  id: order.id,
70
70
  key: order.key,
71
71
  description: order.description,
72
72
  status: order.status,
73
73
  itemKey: order.item?.key ?? null,
74
+ latestApprovedRevNo,
74
75
  ...formatAuditFields(order),
75
76
  _links: [
76
77
  ...orderLinks(RESOURCE, order.key, "Order"),
@@ -81,8 +82,8 @@ function formatOrder(order, user) {
81
82
  };
82
83
  }
83
84
  function formatListOrder(order, user) {
84
- const { _actions, ...rest } = formatOrder(order, user);
85
- const { _links: _, ...withoutLinks } = rest;
85
+ const { _actions, ...rest } = formatOrder(order, user, null);
86
+ const { _links: _, latestApprovedRevNo: __, ...withoutLinks } = rest;
86
87
  return withoutLinks;
87
88
  }
88
89
  export default function orderRoutes(fastify) {
@@ -159,7 +160,8 @@ export default function orderRoutes(fastify) {
159
160
  }
160
161
  }
161
162
  const order = await createOrder(key, description, itemId, userId);
162
- const full = formatOrder(order, request.erpUser);
163
+ // Newly-created order has no revisions yet, so latestApprovedRevNo is null
164
+ const full = formatOrder(order, request.erpUser, null);
163
165
  reply.status(201);
164
166
  return mutationResult(request, reply, full, {
165
167
  id: full.id,
@@ -186,7 +188,8 @@ export default function orderRoutes(fastify) {
186
188
  if (!order) {
187
189
  return notFound(reply, `Order '${key}' not found`);
188
190
  }
189
- return formatOrder(order, request.erpUser);
191
+ const latestApprovedRevNo = await getLatestApprovedRevNo(order.id);
192
+ return formatOrder(order, request.erpUser, latestApprovedRevNo);
190
193
  },
191
194
  });
192
195
  // UPDATE
@@ -225,7 +228,8 @@ export default function orderRoutes(fastify) {
225
228
  }
226
229
  }
227
230
  const order = await updateOrder(key, dbData, userId);
228
- const full = formatOrder(order, request.erpUser);
231
+ const latestApprovedRevNo = await getLatestApprovedRevNo(order.id);
232
+ const full = formatOrder(order, request.erpUser, latestApprovedRevNo);
229
233
  return mutationResult(request, reply, full, {
230
234
  _actions: full._actions,
231
235
  });
@@ -127,6 +127,7 @@ export default function userRoutes(fastify) {
127
127
  statusCode: 403,
128
128
  error: "Forbidden",
129
129
  message: "Permission 'erp_admin' required",
130
+ missingPermission: "erp_admin",
130
131
  });
131
132
  return;
132
133
  }
@@ -3,7 +3,7 @@ import { z } from "zod/v4";
3
3
  import { hasPermission, requirePermission } from "../auth-middleware.js";
4
4
  import { notFound } from "../error-handler.js";
5
5
  import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
- import { formatAuditFields, mutationResult } from "../route-helpers.js";
6
+ import { formatAuditFields, mutationResult, permGate } from "../route-helpers.js";
7
7
  import { assignUser, createWorkCenter, deleteWorkCenter, findExisting, listWorkCenters, removeUser, updateWorkCenter, } from "../services/work-center-service.js";
8
8
  const RESOURCE = "work-centers";
9
9
  const KeyParamsSchema = z.object({
@@ -21,9 +21,8 @@ function wcLinks(key) {
21
21
  ];
22
22
  }
23
23
  function wcActions(key, user) {
24
- if (!hasPermission(user, "erp_admin"))
25
- return [];
26
24
  const href = `${API_PREFIX}/${RESOURCE}/${key}`;
25
+ const gate = permGate(hasPermission(user, "erp_admin"), "erp_admin");
27
26
  return [
28
27
  {
29
28
  rel: "update",
@@ -31,12 +30,14 @@ function wcActions(key, user) {
31
30
  method: "PUT",
32
31
  title: "Update",
33
32
  schema: `${API_PREFIX}/schemas/UpdateWorkCenter`,
33
+ ...gate,
34
34
  },
35
35
  {
36
36
  rel: "delete",
37
37
  href,
38
38
  method: "DELETE",
39
39
  title: "Delete",
40
+ ...gate,
40
41
  },
41
42
  {
42
43
  rel: "assignUser",
@@ -44,11 +45,12 @@ function wcActions(key, user) {
44
45
  method: "POST",
45
46
  title: "Assign User",
46
47
  schema: `${API_PREFIX}/schemas/AssignWorkCenterUser`,
48
+ ...gate,
47
49
  },
48
50
  ];
49
51
  }
50
52
  function formatWorkCenter(wc, user) {
51
- const isAdmin = hasPermission(user, "erp_admin");
53
+ const adminGate = permGate(hasPermission(user, "erp_admin"), "erp_admin");
52
54
  return {
53
55
  id: wc.id,
54
56
  key: wc.key,
@@ -58,16 +60,15 @@ function formatWorkCenter(wc, user) {
58
60
  username: a.user.username,
59
61
  createdAt: a.createdAt.toISOString(),
60
62
  createdBy: a.createdBy?.username ?? null,
61
- _actions: isAdmin
62
- ? [
63
- {
64
- rel: "remove",
65
- href: `${API_PREFIX}/${RESOURCE}/${wc.key}/users/${a.user.username}`,
66
- method: "DELETE",
67
- title: "Remove",
68
- },
69
- ]
70
- : [],
63
+ _actions: [
64
+ {
65
+ rel: "remove",
66
+ href: `${API_PREFIX}/${RESOURCE}/${wc.key}/users/${a.user.username}`,
67
+ method: "DELETE",
68
+ title: "Remove",
69
+ ...adminGate,
70
+ },
71
+ ],
71
72
  })),
72
73
  ...formatAuditFields(wc),
73
74
  _links: wcLinks(wc.key),
@@ -117,17 +118,16 @@ export default function workCenterRoutes(fastify) {
117
118
  hrefTemplate: `${API_PREFIX}/work-centers/{key}`,
118
119
  },
119
120
  ],
120
- _actions: hasPermission(request.erpUser, "erp_admin")
121
- ? [
122
- {
123
- rel: "create",
124
- href: `${API_PREFIX}/${RESOURCE}`,
125
- method: "POST",
126
- title: "Create Work Center",
127
- schema: `${API_PREFIX}/schemas/CreateWorkCenter`,
128
- },
129
- ]
130
- : [],
121
+ _actions: [
122
+ {
123
+ rel: "create",
124
+ href: `${API_PREFIX}/${RESOURCE}`,
125
+ method: "POST",
126
+ title: "Create Work Center",
127
+ schema: `${API_PREFIX}/schemas/CreateWorkCenter`,
128
+ ...permGate(hasPermission(request.erpUser, "erp_admin"), "erp_admin"),
129
+ },
130
+ ],
131
131
  };
132
132
  },
133
133
  });