@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.
@@ -113,6 +113,30 @@ export function checkFieldValueShape(label, type, isArray, value) {
113
113
  }
114
114
  return null;
115
115
  }
116
+ /**
117
+ * Validate a list of field definitions across one or more set indexes, given a
118
+ * way to look up the current value for each (fieldId, setIndex) pair. Returns
119
+ * one entry per invalid cell; callers format as appropriate. Iteration order
120
+ * is ascending setIndex, then the order of `fieldDefs` as given.
121
+ */
122
+ export function validateFieldSet(fieldDefs, setIndexes, getValue) {
123
+ const failures = [];
124
+ for (const si of [...setIndexes].sort((a, b) => a - b)) {
125
+ for (const def of fieldDefs) {
126
+ const value = getValue(def.id, si);
127
+ const result = validateFieldValue(def.type, def.isArray, def.required, value);
128
+ if (!result.valid) {
129
+ failures.push({
130
+ fieldId: def.id,
131
+ label: def.label,
132
+ setIndex: si,
133
+ error: result.error,
134
+ });
135
+ }
136
+ }
137
+ }
138
+ return failures;
139
+ }
116
140
  export function validateFieldValue(type, isArray, required, value) {
117
141
  const shapeErr = checkFieldValueShape("field", type, isArray, value);
118
142
  if (shapeErr)
@@ -223,9 +247,9 @@ export async function clearAttachmentFieldValue(fieldRecordId, fieldId, setIndex
223
247
  await upsertFieldValue(fieldRecordId, fieldId, setIndex, empty, userId);
224
248
  }
225
249
  // --- Mutations ---
226
- export async function upsertFieldValue(fieldRecordId, fieldId, setIndex, value, userId) {
250
+ export async function upsertFieldValue(fieldRecordId, fieldId, setIndex, value, userId, tx = erpDb) {
227
251
  const dbValue = serializeFieldValue(value);
228
- await erpDb.fieldValue.upsert({
252
+ await tx.fieldValue.upsert({
229
253
  where: {
230
254
  fieldRecordId_fieldId_setIndex: { fieldRecordId, fieldId, setIndex },
231
255
  },
@@ -1,6 +1,7 @@
1
1
  import { OperationRunStatus as OperationRunStatusValues, OrderRunStatus as OrderRunStatusValues, } from "@naisys/erp-shared";
2
2
  import { writeAuditEntry } from "../audit.js";
3
3
  import erpDb from "../erpDb.js";
4
+ import { deserializeFieldValue, upsertFieldValue, validateFieldSet, } from "./field-value-service.js";
4
5
  // --- Prisma include & result type ---
5
6
  export const includeRev = {
6
7
  orderRev: { select: { revNo: true } },
@@ -259,7 +260,7 @@ async function autoGenerateInstanceKey(erpTx, itemId) {
259
260
  }
260
261
  export async function completeOrderRun(orderRunId, orderId, data, userId) {
261
262
  return erpDb.$transaction(async (erpTx) => {
262
- // Load the order with its item
263
+ // Load the order with its item and full item field definitions.
263
264
  const order = await erpTx.order.findUniqueOrThrow({
264
265
  where: { id: orderId },
265
266
  select: {
@@ -270,7 +271,15 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
270
271
  fieldSet: {
271
272
  select: {
272
273
  fields: {
273
- select: { id: true },
274
+ select: {
275
+ id: true,
276
+ seqNo: true,
277
+ label: true,
278
+ type: true,
279
+ isArray: true,
280
+ required: true,
281
+ },
282
+ orderBy: { seqNo: "asc" },
274
283
  },
275
284
  },
276
285
  },
@@ -279,14 +288,60 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
279
288
  },
280
289
  });
281
290
  if (!order.item) {
282
- return { error: "Order has no item assigned — cannot complete" };
291
+ return {
292
+ error: "Order has no item assigned — cannot complete",
293
+ status: 422,
294
+ };
295
+ }
296
+ const itemFields = order.item.fieldSet?.fields ?? [];
297
+ const fieldsBySeqNo = new Map(itemFields.map((f) => [f.seqNo, f]));
298
+ const fieldsById = new Map(itemFields.map((f) => [f.id, f]));
299
+ // Validate caller-supplied fieldSeqNos exist on the item.
300
+ const callerValues = data.fieldValues ?? [];
301
+ for (const fv of callerValues) {
302
+ if (!fieldsBySeqNo.has(fv.fieldSeqNo)) {
303
+ return {
304
+ error: `Unknown item field seqNo ${fv.fieldSeqNo} — item has no field with that sequence number`,
305
+ status: 400,
306
+ };
307
+ }
308
+ }
309
+ const resolved = new Map();
310
+ const keyOf = (fieldId, setIndex) => `${fieldId}:${setIndex}`;
311
+ for (const fv of callerValues) {
312
+ const def = fieldsBySeqNo.get(fv.fieldSeqNo);
313
+ const setIndex = fv.setIndex ?? 0;
314
+ resolved.set(keyOf(def.id, setIndex), {
315
+ fieldId: def.id,
316
+ value: fv.value,
317
+ setIndex,
318
+ });
319
+ }
320
+ // Validate all item fields against caller-supplied values at setIndex 0.
321
+ // `fieldValues[]` on CompleteOrderRun is flat (no multi-set support), so
322
+ // we only validate set 0. Flags both missing-required and type-invalid.
323
+ const failures = validateFieldSet(itemFields, [0], (fieldId, setIndex) => {
324
+ const def = fieldsById.get(fieldId);
325
+ const r = resolved.get(keyOf(fieldId, setIndex));
326
+ if (r)
327
+ return deserializeFieldValue(r.value, def.isArray);
328
+ return def.isArray ? [] : "";
329
+ });
330
+ if (failures.length > 0) {
331
+ return {
332
+ error: `Cannot complete order run: ${failures
333
+ .map((f) => `${f.label} — ${f.error}`)
334
+ .join("; ")}. Provide values via fieldValues[] using fieldSeqNo.`,
335
+ status: 400,
336
+ missingFields: failures.map((f) => f.label),
337
+ };
283
338
  }
284
339
  // Determine instance key
285
340
  let instanceKey = data.instanceKey;
286
341
  if (!instanceKey) {
287
342
  const result = await autoGenerateInstanceKey(erpTx, order.item.id);
288
343
  if ("error" in result)
289
- return result;
344
+ return { error: result.error, status: 422 };
290
345
  instanceKey = result.key;
291
346
  }
292
347
  // Check for duplicate key
@@ -296,6 +351,7 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
296
351
  if (existing) {
297
352
  return {
298
353
  error: `Instance key "${instanceKey}" already exists for this item`,
354
+ status: 422,
299
355
  };
300
356
  }
301
357
  // Create the item instance
@@ -309,8 +365,8 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
309
365
  updatedById: userId,
310
366
  },
311
367
  });
312
- // Create field record and field values if item has a field set
313
- if (order.item.fieldSetId && (data.fieldValues?.length ?? 0) > 0) {
368
+ // Create field record and field values if we have any to write.
369
+ if (order.item.fieldSetId && resolved.size > 0) {
314
370
  const fieldRecord = await erpTx.fieldRecord.create({
315
371
  data: {
316
372
  fieldSetId: order.item.fieldSetId,
@@ -321,17 +377,8 @@ export async function completeOrderRun(orderRunId, orderId, data, userId) {
321
377
  where: { id: instance.id },
322
378
  data: { fieldRecordId: fieldRecord.id },
323
379
  });
324
- for (const fv of data.fieldValues ?? []) {
325
- await erpTx.fieldValue.create({
326
- data: {
327
- fieldRecordId: fieldRecord.id,
328
- fieldId: fv.fieldId,
329
- setIndex: fv.setIndex ?? 0,
330
- value: fv.value,
331
- createdById: userId,
332
- updatedById: userId,
333
- },
334
- });
380
+ for (const fv of resolved.values()) {
381
+ await upsertFieldValue(fieldRecord.id, fv.fieldId, fv.setIndex, fv.value, userId, erpTx);
335
382
  }
336
383
  }
337
384
  // Sum operation run costs and transition run to closed
@@ -1,3 +1,4 @@
1
+ import { RevisionStatus } from "@naisys/erp-shared";
1
2
  import erpDb from "../erpDb.js";
2
3
  import { includeUsers } from "../route-helpers.js";
3
4
  // --- Prisma include & result type ---
@@ -31,6 +32,14 @@ export async function checkHasRevisions(orderId) {
31
32
  });
32
33
  return revisionCount > 0;
33
34
  }
35
+ // --- Derived fields ---
36
+ export async function getLatestApprovedRevNo(orderId) {
37
+ const result = await erpDb.orderRevision.aggregate({
38
+ where: { orderId, status: RevisionStatus.approved },
39
+ _max: { revNo: true },
40
+ });
41
+ return result._max.revNo ?? null;
42
+ }
34
43
  // --- Mutations ---
35
44
  export async function resolveItemKey(itemKey) {
36
45
  if (!itemKey)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@naisys/erp",
3
- "version": "3.0.0-beta.23",
3
+ "version": "3.0.0-beta.25",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@naisys/erp",
9
- "version": "3.0.0-beta.23",
9
+ "version": "3.0.0-beta.25",
10
10
  "dependencies": {
11
11
  "@fastify/cookie": "^11.0.2",
12
12
  "@fastify/cors": "^11.2.0",
@@ -14,11 +14,11 @@
14
14
  "@fastify/rate-limit": "^10.3.0",
15
15
  "@fastify/static": "^9.0.0",
16
16
  "@fastify/swagger": "^9.7.0",
17
- "@naisys/common": "3.0.0-beta.23",
18
- "@naisys/common-node": "3.0.0-beta.23",
19
- "@naisys/erp-shared": "3.0.0-beta.23",
20
- "@naisys/hub-database": "3.0.0-beta.23",
21
- "@naisys/supervisor-database": "3.0.0-beta.23",
17
+ "@naisys/common": "3.0.0-beta.25",
18
+ "@naisys/common-node": "3.0.0-beta.25",
19
+ "@naisys/erp-shared": "3.0.0-beta.25",
20
+ "@naisys/hub-database": "3.0.0-beta.25",
21
+ "@naisys/supervisor-database": "3.0.0-beta.25",
22
22
  "@prisma/adapter-better-sqlite3": "^7.5.0",
23
23
  "@prisma/client": "^7.5.0",
24
24
  "@scalar/fastify-api-reference": "^1.48.7",
@@ -444,41 +444,41 @@
444
444
  }
445
445
  },
446
446
  "node_modules/@naisys/common": {
447
- "version": "3.0.0-beta.23",
448
- "resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.23.tgz",
449
- "integrity": "sha512-zOHBEb2UPUsgQID7SC7qY+LopW1RiA1mULB/62zzmpR+IkG9SQgPbb96Qdw2XJjTCcNBIs1HMOvhfHoWcoECDQ==",
447
+ "version": "3.0.0-beta.25",
448
+ "resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.25.tgz",
449
+ "integrity": "sha512-v21DvH/KlxKMxoTwwlInMyLgqVcSAYNYsMF2z7FXYiRL9fcqX15+S7oFZ2PsB+SR6MXlhN7AachF7BsYi7ikCw==",
450
450
  "dependencies": {
451
451
  "semver": "^7.7.4",
452
452
  "zod": "^4.3.6"
453
453
  }
454
454
  },
455
455
  "node_modules/@naisys/common-node": {
456
- "version": "3.0.0-beta.23",
457
- "resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.23.tgz",
458
- "integrity": "sha512-DdBy419PCgVS42Jkv/fKcm92Kd3lnf2rlxu356fpG8Kp5FysocanGIPEBw/oOlRz1+ONpMFWUsohvs9mxlbpzw==",
456
+ "version": "3.0.0-beta.25",
457
+ "resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.25.tgz",
458
+ "integrity": "sha512-WMc8gRNBEB3oh14zRjDNezqldLyPz2aXe7KHVZVEkIpShiYY8pMBva1up/1q40NmlWw/m/rbHRa6yrDUuH3qwg==",
459
459
  "dependencies": {
460
- "@naisys/common": "3.0.0-beta.23",
460
+ "@naisys/common": "3.0.0-beta.25",
461
461
  "better-sqlite3": "^12.6.2",
462
462
  "js-yaml": "^4.1.1",
463
463
  "pino": "^10.3.1"
464
464
  }
465
465
  },
466
466
  "node_modules/@naisys/erp-shared": {
467
- "version": "3.0.0-beta.23",
468
- "resolved": "https://registry.npmjs.org/@naisys/erp-shared/-/erp-shared-3.0.0-beta.23.tgz",
469
- "integrity": "sha512-wSTo6g1x6lsDLqLg4sOjBtZJaFq1+P2R5JNUSUl3jnJkzKdEVze0kgWnUU9bNlbUYqhq3iPkNLWwvRzLJq1u/A==",
467
+ "version": "3.0.0-beta.25",
468
+ "resolved": "https://registry.npmjs.org/@naisys/erp-shared/-/erp-shared-3.0.0-beta.25.tgz",
469
+ "integrity": "sha512-x4RgdksPu4wRKoYKyPeMnSO5nBhk+2G4MY5uy7iYU+OzH6Ozg5MofXTFX62YzKRrYcpPie2B8c37Ihkq59dtfA==",
470
470
  "dependencies": {
471
- "@naisys/common": "3.0.0-beta.23",
471
+ "@naisys/common": "3.0.0-beta.25",
472
472
  "zod": "^4.3.6"
473
473
  }
474
474
  },
475
475
  "node_modules/@naisys/hub-database": {
476
- "version": "3.0.0-beta.23",
477
- "resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.23.tgz",
478
- "integrity": "sha512-lxVJAFZnPld9llruXG+aXkfihEG0XENpZr4+NUEXSgrAmWXGxJMdLqLjZuAT3yJBv6Hn2U5+uOLHDZvotETuYg==",
476
+ "version": "3.0.0-beta.25",
477
+ "resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.25.tgz",
478
+ "integrity": "sha512-BtlvEaCwULaZlsgAZN6+1kKOxc0yHOyLV7CQmobrdBTuW8TTL4r71WoIL1wIVZFfu4Dz1SgCO5aSPh4LqRJkxg==",
479
479
  "dependencies": {
480
- "@naisys/common": "3.0.0-beta.23",
481
- "@naisys/common-node": "3.0.0-beta.23",
480
+ "@naisys/common": "3.0.0-beta.25",
481
+ "@naisys/common-node": "3.0.0-beta.25",
482
482
  "@prisma/adapter-better-sqlite3": "^7.5.0",
483
483
  "@prisma/client": "^7.5.0",
484
484
  "better-sqlite3": "^12.6.2",
@@ -486,12 +486,12 @@
486
486
  }
487
487
  },
488
488
  "node_modules/@naisys/supervisor-database": {
489
- "version": "3.0.0-beta.23",
490
- "resolved": "https://registry.npmjs.org/@naisys/supervisor-database/-/supervisor-database-3.0.0-beta.23.tgz",
491
- "integrity": "sha512-xfu18pBFrc8GHl5cFsyS5fWHNsR9S/12/acaAcjOKyo+ud71sMTPLn4mcxIaI4nVSXENDAXDjDWKA6Muy8iRCg==",
489
+ "version": "3.0.0-beta.25",
490
+ "resolved": "https://registry.npmjs.org/@naisys/supervisor-database/-/supervisor-database-3.0.0-beta.25.tgz",
491
+ "integrity": "sha512-v1rB1fcXgpbaffp4KzAlD1gNAZX9MDIbv4N96J/Q0Zv/Oia1Jr55/yfXscCRtqKhp0NhBxED00irYiKKC7psoQ==",
492
492
  "dependencies": {
493
- "@naisys/common": "3.0.0-beta.23",
494
- "@naisys/common-node": "3.0.0-beta.23",
493
+ "@naisys/common": "3.0.0-beta.25",
494
+ "@naisys/common-node": "3.0.0-beta.25",
495
495
  "@prisma/adapter-better-sqlite3": "^7.5.0",
496
496
  "@prisma/client": "^7.5.0",
497
497
  "bcryptjs": "^3.0.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naisys/erp",
3
- "version": "3.0.0-beta.23",
3
+ "version": "3.0.0-beta.25",
4
4
  "description": "NAISYS ERP - Web UI for AI-driven order and work management",
5
5
  "type": "module",
6
6
  "main": "dist/erpServer.js",
@@ -46,11 +46,11 @@
46
46
  "@fastify/rate-limit": "^10.3.0",
47
47
  "@fastify/static": "^9.0.0",
48
48
  "@fastify/swagger": "^9.7.0",
49
- "@naisys/erp-shared": "3.0.0-beta.23",
50
- "@naisys/common": "3.0.0-beta.23",
51
- "@naisys/common-node": "3.0.0-beta.23",
52
- "@naisys/hub-database": "3.0.0-beta.23",
53
- "@naisys/supervisor-database": "3.0.0-beta.23",
49
+ "@naisys/erp-shared": "3.0.0-beta.25",
50
+ "@naisys/common": "3.0.0-beta.25",
51
+ "@naisys/common-node": "3.0.0-beta.25",
52
+ "@naisys/hub-database": "3.0.0-beta.25",
53
+ "@naisys/supervisor-database": "3.0.0-beta.25",
54
54
  "@prisma/adapter-better-sqlite3": "^7.5.0",
55
55
  "@prisma/client": "^7.5.0",
56
56
  "@scalar/fastify-api-reference": "^1.48.7",