@happyvertical/smrt-manufacturing 0.53.4 → 0.53.6

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.
Files changed (106) hide show
  1. package/AGENTS.md +58 -7
  2. package/README.md +228 -4
  3. package/agents/multilevel-and-runs.md +28 -0
  4. package/dist/collections/AssemblyCollection.d.ts +15 -0
  5. package/dist/collections/AssemblyCollection.d.ts.map +1 -0
  6. package/dist/collections/OperationCollection.d.ts +20 -0
  7. package/dist/collections/OperationCollection.d.ts.map +1 -0
  8. package/dist/collections/ProductionRunCollection.d.ts +15 -0
  9. package/dist/collections/ProductionRunCollection.d.ts.map +1 -0
  10. package/dist/collections/ProductionRunCompletionCollection.d.ts +14 -0
  11. package/dist/collections/ProductionRunCompletionCollection.d.ts.map +1 -0
  12. package/dist/collections/RoutingStepCollection.d.ts +15 -0
  13. package/dist/collections/RoutingStepCollection.d.ts.map +1 -0
  14. package/dist/collections/index.d.ts +5 -0
  15. package/dist/collections/index.d.ts.map +1 -1
  16. package/dist/index.d.ts +23 -5
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1660 -42
  19. package/dist/index.js.map +1 -1
  20. package/dist/manifest.json +1910 -161
  21. package/dist/models/Assembly.d.ts +62 -0
  22. package/dist/models/Assembly.d.ts.map +1 -0
  23. package/dist/models/BillOfMaterials.d.ts +12 -4
  24. package/dist/models/BillOfMaterials.d.ts.map +1 -1
  25. package/dist/models/BomLine.d.ts +11 -5
  26. package/dist/models/BomLine.d.ts.map +1 -1
  27. package/dist/models/Operation.d.ts +59 -0
  28. package/dist/models/Operation.d.ts.map +1 -0
  29. package/dist/models/ProductionRun.d.ts +67 -0
  30. package/dist/models/ProductionRun.d.ts.map +1 -0
  31. package/dist/models/ProductionRunCompletion.d.ts +36 -0
  32. package/dist/models/ProductionRunCompletion.d.ts.map +1 -0
  33. package/dist/models/RoutingStep.d.ts +46 -0
  34. package/dist/models/RoutingStep.d.ts.map +1 -0
  35. package/dist/models/index.d.ts +5 -0
  36. package/dist/models/index.d.ts.map +1 -1
  37. package/dist/operation-types.d.ts +108 -0
  38. package/dist/operation-types.d.ts.map +1 -0
  39. package/dist/production-run-types.d.ts +100 -0
  40. package/dist/production-run-types.d.ts.map +1 -0
  41. package/dist/quantity.d.ts +36 -0
  42. package/dist/quantity.d.ts.map +1 -0
  43. package/dist/services/AssemblyService.d.ts +222 -0
  44. package/dist/services/AssemblyService.d.ts.map +1 -0
  45. package/dist/services/BomService.d.ts +108 -5
  46. package/dist/services/BomService.d.ts.map +1 -1
  47. package/dist/services/OperationService.d.ts +48 -0
  48. package/dist/services/OperationService.d.ts.map +1 -0
  49. package/dist/services/ProductionRunService.d.ts +123 -0
  50. package/dist/services/ProductionRunService.d.ts.map +1 -0
  51. package/dist/services/ProductionService.d.ts +11 -0
  52. package/dist/services/ProductionService.d.ts.map +1 -1
  53. package/dist/services/RoutingService.d.ts +50 -0
  54. package/dist/services/RoutingService.d.ts.map +1 -0
  55. package/dist/services/index.d.ts +5 -0
  56. package/dist/services/index.d.ts.map +1 -1
  57. package/dist/services/structure-walk.d.ts +77 -0
  58. package/dist/services/structure-walk.d.ts.map +1 -0
  59. package/dist/smrt-knowledge.json +1771 -207
  60. package/dist/svelte/components/AssemblyForm.svelte +69 -0
  61. package/dist/svelte/components/AssemblyForm.svelte.d.ts +38 -0
  62. package/dist/svelte/components/AssemblyForm.svelte.d.ts.map +1 -0
  63. package/dist/svelte/components/AssemblyList.svelte +229 -0
  64. package/dist/svelte/components/AssemblyList.svelte.d.ts +28 -0
  65. package/dist/svelte/components/AssemblyList.svelte.d.ts.map +1 -0
  66. package/dist/svelte/components/BomEditor.svelte +575 -0
  67. package/dist/svelte/components/BomEditor.svelte.d.ts +29 -0
  68. package/dist/svelte/components/BomEditor.svelte.d.ts.map +1 -0
  69. package/dist/svelte/components/BomStructureTree.svelte +164 -0
  70. package/dist/svelte/components/BomStructureTree.svelte.d.ts +16 -0
  71. package/dist/svelte/components/BomStructureTree.svelte.d.ts.map +1 -0
  72. package/dist/svelte/components/OperationForm.svelte +258 -0
  73. package/dist/svelte/components/OperationForm.svelte.d.ts +17 -0
  74. package/dist/svelte/components/OperationForm.svelte.d.ts.map +1 -0
  75. package/dist/svelte/components/OperationList.svelte +170 -0
  76. package/dist/svelte/components/OperationList.svelte.d.ts +17 -0
  77. package/dist/svelte/components/OperationList.svelte.d.ts.map +1 -0
  78. package/dist/svelte/components/ProductionRunList.svelte +213 -0
  79. package/dist/svelte/components/ProductionRunList.svelte.d.ts +18 -0
  80. package/dist/svelte/components/ProductionRunList.svelte.d.ts.map +1 -0
  81. package/dist/svelte/components/RequirementsTree.svelte +125 -0
  82. package/dist/svelte/components/RequirementsTree.svelte.d.ts +13 -0
  83. package/dist/svelte/components/RequirementsTree.svelte.d.ts.map +1 -0
  84. package/dist/svelte/components/RequirementsTreeLevel.svelte +120 -0
  85. package/dist/svelte/components/RequirementsTreeLevel.svelte.d.ts +12 -0
  86. package/dist/svelte/components/RequirementsTreeLevel.svelte.d.ts.map +1 -0
  87. package/dist/svelte/i18n.d.ts +119 -0
  88. package/dist/svelte/i18n.d.ts.map +1 -0
  89. package/dist/svelte/i18n.js +119 -0
  90. package/dist/svelte/index.d.ts +33 -0
  91. package/dist/svelte/index.d.ts.map +1 -0
  92. package/dist/svelte/index.js +34 -0
  93. package/dist/svelte/types.d.ts +257 -0
  94. package/dist/svelte/types.d.ts.map +1 -0
  95. package/dist/svelte/types.js +249 -0
  96. package/dist/tenant-scope.d.ts +25 -0
  97. package/dist/tenant-scope.d.ts.map +1 -0
  98. package/dist/types.d.ts +207 -3
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +13 -1
  101. package/dist/types.js.map +1 -1
  102. package/dist/ui.d.ts +17 -0
  103. package/dist/ui.d.ts.map +1 -0
  104. package/dist/ui.js +94 -0
  105. package/dist/ui.js.map +1 -0
  106. package/package.json +41 -11
package/dist/index.js CHANGED
@@ -1,18 +1,134 @@
1
- import { BomNotFoundError, NoActiveBomForProductError } from "./types.js";
2
- import { ObjectRegistry, SmrtCollection, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
3
- import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
1
+ import { BomExplosionLimitError, BomNotFoundError, MAX_EXPLOSION_DEPTH, MAX_EXPLOSION_LINES, NoActiveBomForProductError } from "./types.js";
2
+ import { ObjectRegistry, SmrtCollection, SmrtObject, ValidationError, crossPackageRef, field, foreignKey, resolveDatabase, smrt } from "@happyvertical/smrt-core";
3
+ import { ProductCollection, SkuCollection } from "@happyvertical/smrt-products/collections";
4
+ import { Material, Product, ProductType } from "@happyvertical/smrt-products/models";
5
+ import { TenantScoped, getCurrentTenant, tenantId, withTenant, withTenantGlobalRead } from "@happyvertical/smrt-tenancy";
4
6
  import { createStockService } from "@happyvertical/smrt-inventory";
5
7
  import { createLogger } from "@happyvertical/logger";
8
+ //#region \0rolldown/runtime.js
9
+ var __defProp$7 = Object.defineProperty;
10
+ var __exportAll = (all, no_symbols) => {
11
+ let target = {};
12
+ for (var name in all) __defProp$7(target, name, {
13
+ get: all[name],
14
+ enumerable: true
15
+ });
16
+ if (!no_symbols) __defProp$7(target, Symbol.toStringTag, { value: "Module" });
17
+ return target;
18
+ };
19
+ //#endregion
6
20
  //#region src/__smrt-register__.ts
7
- ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-manufacturing\",\"packageVersion\":\"0.53.4\",\"objects\":{\"@happyvertical/smrt-manufacturing:BillOfMaterialsCollection\":{\"name\":\"billofmaterialscollection\",\"className\":\"BillOfMaterialsCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BillOfMaterialsCollection\",\"collection\":\"billofmaterialses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/BillOfMaterialsCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByProduct\":{\"name\":\"findByProduct\",\"async\":true,\"parameters\":[{\"name\":\"productId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials[]>\",\"isStatic\":false,\"isPublic\":true},\"findActiveForProduct\":{\"name\":\"findActiveForProduct\",\"async\":true,\"parameters\":[{\"name\":\"productId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials | null>\",\"isStatic\":false,\"isPublic\":true},\"findByStatus\":{\"name\":\"findByStatus\",\"async\":true,\"parameters\":[{\"name\":\"status\",\"type\":\"BomStatus\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_boms\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"BillOfMaterials\",\"exportName\":\"BillOfMaterialsCollection\",\"collectionExportName\":\"BillOfMaterialsCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_boms\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_boms\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"version\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"effective_date\\\" TIMESTAMP,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'draft',\\n \\\"notes\\\" TEXT DEFAULT '',\\n \\\"currency\\\" TEXT DEFAULT 'USD'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"product_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"version\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"effective_date\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"draft\"},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"}},\"indexes\":[{\"name\":\"manufacturing_boms_product_id_version_idx\",\"columns\":[\"product_id\",\"version\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_boms_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_boms_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"b2691adc\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BomLineCollection\":{\"name\":\"bomlinecollection\",\"className\":\"BomLineCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BomLineCollection\",\"collection\":\"bomlines\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/BomLineCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByBom\":{\"name\":\"findByBom\",\"async\":true,\"parameters\":[{\"name\":\"bomId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BomLine[]>\",\"isStatic\":false,\"isPublic\":true},\"findByComponent\":{\"name\":\"findByComponent\",\"async\":true,\"parameters\":[{\"name\":\"componentSkuId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BomLine[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_bom_lines\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"BomLine\",\"exportName\":\"BomLineCollection\",\"collectionExportName\":\"BomLineCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_bom_lines\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_bom_lines\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"component_sku_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"qty_per_unit\\\" REAL,\\n \\\"uom\\\" TEXT DEFAULT 'each',\\n \\\"waste_percent\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"component_sku_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"qty_per_unit\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"uom\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"each\"},\"waste_percent\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_bom_lines_bom_id_component_sku_id_idx\",\"columns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_bom_lines_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_bom_lines_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cfbae7e8\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BillOfMaterials\":{\"name\":\"billofmaterials\",\"className\":\"BillOfMaterials\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BillOfMaterials\",\"collection\":\"billofmaterialses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/BillOfMaterials.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"productId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"version\":{\"type\":\"integer\",\"required\":true,\"default\":1,\"_meta\":{\"required\":true}},\"effectiveDate\":{\"type\":\"datetime\",\"required\":false},\"status\":{\"type\":\"text\",\"required\":true,\"default\":\"draft\",\"_meta\":{\"required\":true}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"currency\":{\"type\":\"text\",\"required\":false,\"default\":\"USD\"}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"manufacturing_boms\",\"conflictColumns\":[\"product_id\",\"version\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":true,\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"BillOfMaterials\",\"collectionExportName\":\"BillOfMaterialsCollection\",\"validationRules\":[{\"field\":\"productId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"version\",\"rule\":\"required\",\"fieldType\":\"integer\"},{\"field\":\"status\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_boms\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_boms\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"version\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"effective_date\\\" TIMESTAMP,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'draft',\\n \\\"notes\\\" TEXT DEFAULT '',\\n \\\"currency\\\" TEXT DEFAULT 'USD'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"product_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"version\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"effective_date\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"draft\"},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"}},\"indexes\":[{\"name\":\"manufacturing_boms_product_id_version_idx\",\"columns\":[\"product_id\",\"version\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_boms_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_boms_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"b2691adc\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BomLine\":{\"name\":\"bomline\",\"className\":\"BomLine\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BomLine\",\"collection\":\"bomlines\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/BomLine.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"bomId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"componentSkuId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"qtyPerUnit\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"uom\":{\"type\":\"text\",\"required\":false,\"default\":\"each\"},\"wastePercent\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"}},\"methods\":{\"effectiveQtyPerUnit\":{\"name\":\"effectiveQtyPerUnit\",\"async\":false,\"parameters\":[],\"returnType\":\"number\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_bom_lines\",\"conflictColumns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"skipApiCheck\":true},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"BomLine\",\"collectionExportName\":\"BomLineCollection\",\"validationRules\":[{\"field\":\"bomId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"componentSkuId\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_bom_lines\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_bom_lines\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"component_sku_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"qty_per_unit\\\" REAL,\\n \\\"uom\\\" TEXT DEFAULT 'each',\\n \\\"waste_percent\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"component_sku_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"qty_per_unit\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"uom\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"each\"},\"waste_percent\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_bom_lines_bom_id_component_sku_id_idx\",\"columns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_bom_lines_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_bom_lines_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cfbae7e8\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-inventory\",\"@happyvertical/smrt-tenancy\"]}"));
21
+ ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-manufacturing\",\"packageVersion\":\"0.53.6\",\"objects\":{\"@happyvertical/smrt-manufacturing:BillOfMaterialsCollection\":{\"name\":\"billofmaterialscollection\",\"className\":\"BillOfMaterialsCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BillOfMaterialsCollection\",\"collection\":\"billofmaterialses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/BillOfMaterialsCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByProduct\":{\"name\":\"findByProduct\",\"async\":true,\"parameters\":[{\"name\":\"productId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials[]>\",\"isStatic\":false,\"isPublic\":true},\"findActiveForProduct\":{\"name\":\"findActiveForProduct\",\"async\":true,\"parameters\":[{\"name\":\"productId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials | null>\",\"isStatic\":false,\"isPublic\":true},\"findByStatus\":{\"name\":\"findByStatus\",\"async\":true,\"parameters\":[{\"name\":\"status\",\"type\":\"BomStatus\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_boms\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"BillOfMaterials\",\"exportName\":\"BillOfMaterialsCollection\",\"collectionExportName\":\"BillOfMaterialsCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_boms\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_boms\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"version\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"effective_date\\\" TIMESTAMP,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'draft',\\n \\\"notes\\\" TEXT DEFAULT '',\\n \\\"currency\\\" TEXT DEFAULT 'USD'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"product_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"version\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"effective_date\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"draft\"},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"}},\"indexes\":[{\"name\":\"manufacturing_boms_product_id_version_idx\",\"columns\":[\"product_id\",\"version\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_boms_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_boms_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"b2691adc\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BomLineCollection\":{\"name\":\"bomlinecollection\",\"className\":\"BomLineCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BomLineCollection\",\"collection\":\"bomlines\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/BomLineCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByBom\":{\"name\":\"findByBom\",\"async\":true,\"parameters\":[{\"name\":\"bomId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BomLine[]>\",\"isStatic\":false,\"isPublic\":true},\"findByComponent\":{\"name\":\"findByComponent\",\"async\":true,\"parameters\":[{\"name\":\"componentSkuId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BomLine[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_bom_lines\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"BomLine\",\"exportName\":\"BomLineCollection\",\"collectionExportName\":\"BomLineCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_bom_lines\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_bom_lines\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"component_sku_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"qty_per_unit\\\" REAL,\\n \\\"uom\\\" TEXT DEFAULT 'each',\\n \\\"waste_percent\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"component_sku_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"qty_per_unit\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"uom\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"each\"},\"waste_percent\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_bom_lines_bom_id_component_sku_id_idx\",\"columns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_bom_lines_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_bom_lines_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cfbae7e8\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:OperationCollection\":{\"name\":\"operationcollection\",\"className\":\"OperationCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:OperationCollection\",\"collection\":\"operations\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/OperationCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByCode\":{\"name\":\"findByCode\",\"async\":true,\"parameters\":[{\"name\":\"code\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Operation | null>\",\"isStatic\":false,\"isPublic\":true},\"listOperations\":{\"name\":\"listOperations\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true,\"memberTypes\":[\"boolean\"]}],\"returnType\":\"Promise<Operation[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_operations\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"Operation\",\"exportName\":\"OperationCollection\",\"collectionExportName\":\"OperationCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_operations\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_operations\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"code\\\" TEXT NOT NULL DEFAULT '',\\n \\\"name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"category\\\" TEXT DEFAULT '',\\n \\\"is_active\\\" BOOLEAN DEFAULT TRUE,\\n \\\"required_qualification_id\\\" UUID\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"code\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"category\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"is_active\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false,\"default\":true},\"required_qualification_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"manufacturing_operations_code_tenant_id_idx\",\"columns\":[\"code\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_operations_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_operations_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_operations_required_qualification_id_idx\",\"columns\":[\"required_qualification_id\"]}],\"version\":\"0b3134d6\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:ProductionRunCollection\":{\"name\":\"productionruncollection\",\"className\":\"ProductionRunCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:ProductionRunCollection\",\"collection\":\"productionruns\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/ProductionRunCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByBom\":{\"name\":\"findByBom\",\"async\":true,\"parameters\":[{\"name\":\"bomId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<ProductionRun[]>\",\"isStatic\":false,\"isPublic\":true},\"findByStatus\":{\"name\":\"findByStatus\",\"async\":true,\"parameters\":[{\"name\":\"status\",\"type\":\"ProductionRunStatus\",\"optional\":false}],\"returnType\":\"Promise<ProductionRun[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_production_runs\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"ProductionRun\",\"exportName\":\"ProductionRunCollection\",\"collectionExportName\":\"ProductionRunCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_production_runs\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_production_runs\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" UUID NOT NULL,\\n \\\"target_qty\\\" REAL,\\n \\\"completed_qty\\\" REAL,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'planned'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_boms\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"target_qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"completed_qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"planned\"}},\"indexes\":[{\"name\":\"manufacturing_production_runs_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_production_runs_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_production_runs_bom_id_idx\",\"columns\":[\"bom_id\"]}],\"version\":\"2ca8147d\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:ProductionRunCompletionCollection\":{\"name\":\"productionruncompletioncollection\",\"className\":\"ProductionRunCompletionCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:ProductionRunCompletionCollection\",\"collection\":\"productionruncompletions\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/ProductionRunCompletionCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByRun\":{\"name\":\"findByRun\",\"async\":true,\"parameters\":[{\"name\":\"runId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<ProductionRunCompletion[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_production_run_completions\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"ProductionRunCompletion\",\"exportName\":\"ProductionRunCompletionCollection\",\"collectionExportName\":\"ProductionRunCompletionCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_production_run_completions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_production_run_completions\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"run_id\\\" UUID NOT NULL,\\n \\\"qty\\\" REAL,\\n \\\"completed_at\\\" TIMESTAMP\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"run_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_production_runs\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"completed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"manufacturing_production_run_completions_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_production_run_completions_tenant_ea7a0d0125_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_production_run_completions_run_id_idx\",\"columns\":[\"run_id\"]}],\"version\":\"c940de6c\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:RoutingStepCollection\":{\"name\":\"routingstepcollection\",\"className\":\"RoutingStepCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:RoutingStepCollection\",\"collection\":\"routingsteps\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/RoutingStepCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByBom\":{\"name\":\"findByBom\",\"async\":true,\"parameters\":[{\"name\":\"bomId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<RoutingStep[]>\",\"isStatic\":false,\"isPublic\":true},\"findByOperation\":{\"name\":\"findByOperation\",\"async\":true,\"parameters\":[{\"name\":\"operationId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<RoutingStep[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_routing_steps\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"RoutingStep\",\"exportName\":\"RoutingStepCollection\",\"collectionExportName\":\"RoutingStepCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_routing_steps\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_routing_steps\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" UUID NOT NULL,\\n \\\"operation_id\\\" UUID NOT NULL,\\n \\\"sequence\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"estimated_minutes\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_boms\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"operation_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_operations\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"sequence\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"estimated_minutes\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_routing_steps_bom_id_sequence_idx\",\"columns\":[\"bom_id\",\"sequence\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_routing_steps_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_routing_steps_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_routing_steps_operation_id_idx\",\"columns\":[\"operation_id\"]}],\"version\":\"41ac3c66\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:Assembly\":{\"name\":\"assembly\",\"className\":\"Assembly\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:Assembly\",\"collection\":\"products\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/Assembly.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"created_at\":{\"type\":\"datetime\",\"required\":false},\"updated_at\":{\"type\":\"datetime\",\"required\":false},\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"generated\":true,\"source\":\"tenantScoped_decorator\",\"sqlType\":\"UUID\",\"__tenancy\":{\"isTenantIdField\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"autoFilter\":true,\"autoPopulate\":true,\"allowSuperAdminBypass\":false}}},\"productType\":{\"type\":\"json\",\"required\":false},\"name\":{\"type\":\"text\",\"required\":false},\"description\":{\"type\":\"text\",\"required\":false},\"category\":{\"type\":\"text\",\"required\":false},\"manufacturer\":{\"type\":\"text\",\"required\":false},\"model\":{\"type\":\"text\",\"required\":false},\"price\":{\"type\":\"integer\",\"required\":true,\"default\":0},\"inStock\":{\"type\":\"boolean\",\"required\":true,\"default\":true},\"specifications\":{\"type\":\"json\",\"required\":false,\"default\":{}},\"tags\":{\"type\":\"json\",\"required\":false,\"default\":[]},\"estimatedLabourMinutes\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"defaultOperationId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"Operation\",\"_meta\":{\"nullable\":true,\"onDelete\":\"RESTRICT\"}},\"partReference\":{\"type\":\"text\",\"required\":false,\"default\":\"\"}},\"methods\":{\"getSpecification\":{\"name\":\"getSpecification\",\"async\":true,\"parameters\":[{\"name\":\"key\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<unknown>\",\"isStatic\":false,\"isPublic\":true},\"updateSpecification\":{\"name\":\"updateSpecification\",\"async\":true,\"parameters\":[{\"name\":\"key\",\"type\":\"string\",\"optional\":false},{\"name\":\"value\",\"type\":\"unknown\",\"optional\":false}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true},\"getAssets\":{\"name\":\"getAssets\",\"async\":true,\"parameters\":[{\"name\":\"relationship\",\"type\":\"string\",\"optional\":true}],\"returnType\":\"Promise<Asset[]>\",\"isStatic\":false,\"isPublic\":true},\"addAsset\":{\"name\":\"addAsset\",\"async\":true,\"parameters\":[{\"name\":\"asset\",\"type\":\"Asset\",\"optional\":false},{\"name\":\"relationship\",\"type\":\"any\",\"optional\":true,\"default\":\"attachment\"},{\"name\":\"sortOrder\",\"type\":\"any\",\"optional\":true}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true},\"removeAsset\":{\"name\":\"removeAsset\",\"async\":true,\"parameters\":[{\"name\":\"assetId\",\"type\":\"string\",\"optional\":false},{\"name\":\"relationship\",\"type\":\"string\",\"optional\":true}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"skipApiCheck\":true},\"tenantScoped\":{\"mode\":\"optional\"},\"tableName\":\"products\",\"tableStrategy\":\"sti\"},\"extends\":\"Product\",\"exportName\":\"Assembly\",\"collectionExportName\":\"AssemblyCollection\",\"validationRules\":[{\"field\":\"price\",\"rule\":\"required\",\"fieldType\":\"integer\"},{\"field\":\"inStock\",\"rule\":\"required\",\"fieldType\":\"boolean\"}],\"schema\":{\"tableName\":\"products\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"products\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"_meta_type\\\" TEXT NOT NULL,\\n \\\"_meta_data\\\" JSON,\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_type\\\" TEXT,\\n \\\"name\\\" TEXT,\\n \\\"description\\\" TEXT,\\n \\\"category\\\" TEXT,\\n \\\"manufacturer\\\" TEXT,\\n \\\"model\\\" TEXT,\\n \\\"price\\\" INTEGER DEFAULT 0,\\n \\\"in_stock\\\" BOOLEAN DEFAULT TRUE,\\n \\\"specifications\\\" JSON DEFAULT '{}',\\n \\\"tags\\\" JSON DEFAULT '[]',\\n \\\"estimated_labour_minutes\\\" INTEGER DEFAULT 0,\\n \\\"default_operation_id\\\" UUID,\\n \\\"part_reference\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"_meta_type\":{\"type\":\"TEXT\",\"notNull\":true},\"_meta_data\":{\"type\":\"JSON\",\"notNull\":false},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false},\"product_type\":{\"type\":\"TEXT\",\"notNull\":false},\"name\":{\"type\":\"TEXT\",\"notNull\":false},\"description\":{\"type\":\"TEXT\",\"notNull\":false},\"category\":{\"type\":\"TEXT\",\"notNull\":false},\"manufacturer\":{\"type\":\"TEXT\",\"notNull\":false},\"model\":{\"type\":\"TEXT\",\"notNull\":false},\"price\":{\"type\":\"INTEGER\",\"notNull\":false,\"default\":0},\"in_stock\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"default\":true},\"specifications\":{\"type\":\"JSON\",\"notNull\":false,\"default\":{}},\"tags\":{\"type\":\"JSON\",\"notNull\":false,\"default\":[]},\"estimated_labour_minutes\":{\"type\":\"INTEGER\",\"notNull\":false,\"default\":0},\"default_operation_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"foreignKey\":{\"table\":\"manufacturing_operations\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"part_reference\":{\"type\":\"TEXT\",\"notNull\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"products_slug_context_meta_type_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\",\"_meta_type\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"products_meta_type_idx\",\"columns\":[\"_meta_type\"]},{\"name\":\"products_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"products_default_operation_id_idx\",\"columns\":[\"default_operation_id\"]}],\"version\":\"f2f2bf95\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BillOfMaterials\":{\"name\":\"billofmaterials\",\"className\":\"BillOfMaterials\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BillOfMaterials\",\"collection\":\"billofmaterialses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/BillOfMaterials.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"productId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"version\":{\"type\":\"integer\",\"required\":true,\"default\":1,\"_meta\":{\"required\":true}},\"effectiveDate\":{\"type\":\"datetime\",\"required\":false},\"status\":{\"type\":\"text\",\"required\":true,\"default\":\"draft\",\"_meta\":{\"required\":true}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"currency\":{\"type\":\"text\",\"required\":false,\"default\":\"USD\"}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"manufacturing_boms\",\"conflictColumns\":[\"product_id\",\"version\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":true,\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"BillOfMaterials\",\"collectionExportName\":\"BillOfMaterialsCollection\",\"validationRules\":[{\"field\":\"productId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"version\",\"rule\":\"required\",\"fieldType\":\"integer\"},{\"field\":\"status\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_boms\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_boms\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"version\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"effective_date\\\" TIMESTAMP,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'draft',\\n \\\"notes\\\" TEXT DEFAULT '',\\n \\\"currency\\\" TEXT DEFAULT 'USD'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"product_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"version\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"effective_date\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"draft\"},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"}},\"indexes\":[{\"name\":\"manufacturing_boms_product_id_version_idx\",\"columns\":[\"product_id\",\"version\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_boms_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_boms_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"b2691adc\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BomLine\":{\"name\":\"bomline\",\"className\":\"BomLine\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BomLine\",\"collection\":\"bomlines\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/BomLine.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"bomId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"componentSkuId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"qtyPerUnit\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"uom\":{\"type\":\"text\",\"required\":false,\"default\":\"each\"},\"wastePercent\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"}},\"methods\":{\"effectiveQtyPerUnit\":{\"name\":\"effectiveQtyPerUnit\",\"async\":false,\"parameters\":[],\"returnType\":\"number\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_bom_lines\",\"conflictColumns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"skipApiCheck\":true},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"BomLine\",\"collectionExportName\":\"BomLineCollection\",\"validationRules\":[{\"field\":\"bomId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"componentSkuId\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_bom_lines\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_bom_lines\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"component_sku_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"qty_per_unit\\\" REAL,\\n \\\"uom\\\" TEXT DEFAULT 'each',\\n \\\"waste_percent\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"component_sku_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"qty_per_unit\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"uom\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"each\"},\"waste_percent\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_bom_lines_bom_id_component_sku_id_idx\",\"columns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_bom_lines_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_bom_lines_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cfbae7e8\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:Operation\":{\"name\":\"operation\",\"className\":\"Operation\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:Operation\",\"collection\":\"operations\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/Operation.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"code\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"name\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"category\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"isActive\":{\"type\":\"boolean\",\"required\":false,\"default\":true},\"requiredQualificationId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-human-resources:Qualification\",\"_meta\":{\"nullable\":true}}},\"methods\":{\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_operations\",\"conflictColumns\":[\"code\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"Operation\",\"collectionExportName\":\"OperationCollection\",\"validationRules\":[{\"field\":\"code\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"name\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_operations\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_operations\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"code\\\" TEXT NOT NULL DEFAULT '',\\n \\\"name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"category\\\" TEXT DEFAULT '',\\n \\\"is_active\\\" BOOLEAN DEFAULT TRUE,\\n \\\"required_qualification_id\\\" UUID\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"code\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"category\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"is_active\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false,\"default\":true},\"required_qualification_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"manufacturing_operations_code_tenant_id_idx\",\"columns\":[\"code\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_operations_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_operations_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_operations_required_qualification_id_idx\",\"columns\":[\"required_qualification_id\"]}],\"version\":\"0b3134d6\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:ProductionRun\":{\"name\":\"productionrun\",\"className\":\"ProductionRun\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:ProductionRun\",\"collection\":\"productionruns\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/ProductionRun.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"bomId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"BillOfMaterials\",\"_meta\":{\"required\":true,\"onDelete\":\"RESTRICT\"}},\"targetQty\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"completedQty\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":true,\"default\":\"planned\",\"_meta\":{\"required\":true}}},\"methods\":{\"remainingQty\":{\"name\":\"remainingQty\",\"async\":false,\"parameters\":[],\"returnType\":\"number\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_production_runs\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"},\"conflictColumns\":[\"tenant_id\",\"slug\",\"context\"]},\"extends\":\"SmrtObject\",\"exportName\":\"ProductionRun\",\"collectionExportName\":\"ProductionRunCollection\",\"validationRules\":[{\"field\":\"bomId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"status\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_production_runs\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_production_runs\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" UUID NOT NULL,\\n \\\"target_qty\\\" REAL,\\n \\\"completed_qty\\\" REAL,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'planned'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_boms\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"target_qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"completed_qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"planned\"}},\"indexes\":[{\"name\":\"manufacturing_production_runs_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_production_runs_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_production_runs_bom_id_idx\",\"columns\":[\"bom_id\"]}],\"version\":\"2ca8147d\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:ProductionRunCompletion\":{\"name\":\"productionruncompletion\",\"className\":\"ProductionRunCompletion\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:ProductionRunCompletion\",\"collection\":\"productionruncompletions\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/ProductionRunCompletion.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"runId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"ProductionRun\",\"_meta\":{\"required\":true,\"onDelete\":\"CASCADE\"}},\"qty\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"completedAt\":{\"type\":\"datetime\",\"required\":false}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"manufacturing_production_run_completions\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"},\"conflictColumns\":[\"tenant_id\",\"slug\",\"context\"]},\"extends\":\"SmrtObject\",\"exportName\":\"ProductionRunCompletion\",\"collectionExportName\":\"ProductionRunCompletionCollection\",\"validationRules\":[{\"field\":\"runId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"manufacturing_production_run_completions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_production_run_completions\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"run_id\\\" UUID NOT NULL,\\n \\\"qty\\\" REAL,\\n \\\"completed_at\\\" TIMESTAMP\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"run_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_production_runs\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"completed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"manufacturing_production_run_completions_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_production_run_completions_tenant_ea7a0d0125_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_production_run_completions_run_id_idx\",\"columns\":[\"run_id\"]}],\"version\":\"c940de6c\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:RoutingStep\":{\"name\":\"routingstep\",\"className\":\"RoutingStep\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:RoutingStep\",\"collection\":\"routingsteps\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/RoutingStep.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"bomId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"BillOfMaterials\",\"_meta\":{\"required\":true,\"onDelete\":\"CASCADE\"}},\"operationId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"Operation\",\"_meta\":{\"required\":true,\"onDelete\":\"RESTRICT\"}},\"sequence\":{\"type\":\"integer\",\"required\":true,\"default\":1,\"_meta\":{\"required\":true}},\"estimatedMinutes\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"manufacturing_routing_steps\",\"conflictColumns\":[\"bom_id\",\"sequence\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"skipApiCheck\":true},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"RoutingStep\",\"collectionExportName\":\"RoutingStepCollection\",\"validationRules\":[{\"field\":\"bomId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"operationId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"sequence\",\"rule\":\"required\",\"fieldType\":\"integer\"}],\"schema\":{\"tableName\":\"manufacturing_routing_steps\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_routing_steps\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" UUID NOT NULL,\\n \\\"operation_id\\\" UUID NOT NULL,\\n \\\"sequence\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"estimated_minutes\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_boms\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"operation_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_operations\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"sequence\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"estimated_minutes\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_routing_steps_bom_id_sequence_idx\",\"columns\":[\"bom_id\",\"sequence\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_routing_steps_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_routing_steps_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_routing_steps_operation_id_idx\",\"columns\":[\"operation_id\"]}],\"version\":\"41ac3c66\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-fields\",\"@happyvertical/smrt-inventory\",\"@happyvertical/smrt-products\",\"@happyvertical/smrt-tenancy\"]}"));
22
+ //#endregion
23
+ //#region src/models/Operation.ts
24
+ var __defProp$6 = Object.defineProperty;
25
+ var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
26
+ var __decorateClass$6 = (decorators, target, key, kind) => {
27
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
28
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
29
+ if (kind && result) __defProp$6(target, key, result);
30
+ return result;
31
+ };
32
+ var Operation = class extends SmrtObject {
33
+ tenantId = null;
34
+ code = "";
35
+ name = "";
36
+ /**
37
+ * Free-text grouping for lists (`fabrication`, `finishing`, `quality`).
38
+ * Empty when the operation is not grouped.
39
+ */
40
+ category = "";
41
+ /**
42
+ * `false` once the operation is retired. A retired operation stays on the
43
+ * routings and history that name it and is left out of pickers.
44
+ */
45
+ isActive = true;
46
+ requiredQualificationId = null;
47
+ constructor(options = {}) {
48
+ super(options);
49
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
50
+ if (options.code !== void 0) this.code = options.code;
51
+ if (options.name !== void 0) this.name = options.name;
52
+ if (options.category !== void 0) this.category = options.category;
53
+ if (options.isActive !== void 0) this.isActive = options.isActive;
54
+ if (options.requiredQualificationId !== void 0) this.requiredQualificationId = options.requiredQualificationId;
55
+ }
56
+ /** Operations are retired, never deleted, so history keeps resolving. */
57
+ async delete() {
58
+ throw new Error(`Operation ${this.code || this.id} cannot be deleted; retire it instead.`);
59
+ }
60
+ };
61
+ __decorateClass$6([tenantId({ nullable: true })], Operation.prototype, "tenantId", 2);
62
+ __decorateClass$6([field({ required: true })], Operation.prototype, "code", 2);
63
+ __decorateClass$6([field({ required: true })], Operation.prototype, "name", 2);
64
+ __decorateClass$6([crossPackageRef("@happyvertical/smrt-human-resources:Qualification", { nullable: true })], Operation.prototype, "requiredQualificationId", 2);
65
+ Operation = __decorateClass$6([TenantScoped({ mode: "optional" }), smrt({
66
+ tableName: "manufacturing_operations",
67
+ conflictColumns: ["code", "tenant_id"],
68
+ api: { include: ["list", "get"] },
69
+ mcp: { include: ["list", "get"] },
70
+ cli: { include: ["list", "get"] }
71
+ })], Operation);
72
+ //#endregion
73
+ //#region src/models/Assembly.ts
74
+ var __defProp$5 = Object.defineProperty;
75
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
76
+ var __decorateClass$5 = (decorators, target, key, kind) => {
77
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
78
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
79
+ if (kind && result) __defProp$5(target, key, result);
80
+ return result;
81
+ };
82
+ var Assembly = class extends Product {
83
+ productType = ProductType.ASSEMBLY;
84
+ estimatedLabourMinutes = 0;
85
+ defaultOperationId = null;
86
+ partReference = "";
87
+ constructor(options = {}) {
88
+ super(options);
89
+ if (options.estimatedLabourMinutes !== void 0) this.estimatedLabourMinutes = options.estimatedLabourMinutes;
90
+ if (options.defaultOperationId !== void 0) this.defaultOperationId = options.defaultOperationId;
91
+ if (options.partReference !== void 0) this.partReference = options.partReference;
92
+ }
93
+ /**
94
+ * Refuses a labour estimate that is not a whole number of minutes of zero
95
+ * or more, on every save (generated REST and collection writes included).
96
+ */
97
+ async validateBeforeSave() {
98
+ await super.validateBeforeSave();
99
+ const minutes = this.estimatedLabourMinutes;
100
+ if (!Number.isSafeInteger(minutes) || minutes < 0) throw ValidationError.invalidValue("estimatedLabourMinutes", minutes, "a whole number of minutes, zero or more");
101
+ }
102
+ };
103
+ __decorateClass$5([field()], Assembly.prototype, "estimatedLabourMinutes", 2);
104
+ __decorateClass$5([foreignKey(Operation, {
105
+ nullable: true,
106
+ onDelete: "RESTRICT"
107
+ })], Assembly.prototype, "defaultOperationId", 2);
108
+ __decorateClass$5([field()], Assembly.prototype, "partReference", 2);
109
+ Assembly = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
110
+ api: { include: [
111
+ "list",
112
+ "get",
113
+ "create",
114
+ "update"
115
+ ] },
116
+ mcp: { include: ["list", "get"] },
117
+ cli: { skipApiCheck: true }
118
+ })], Assembly);
119
+ //#endregion
120
+ //#region src/collections/AssemblyCollection.ts
121
+ var AssemblyCollection = class extends ProductCollection {
122
+ static _itemClass = Assembly;
123
+ };
8
124
  //#endregion
9
125
  //#region src/models/BillOfMaterials.ts
10
- var __defProp$1 = Object.defineProperty;
11
- var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
12
- var __decorateClass$1 = (decorators, target, key, kind) => {
13
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
126
+ var __defProp$4 = Object.defineProperty;
127
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
128
+ var __decorateClass$4 = (decorators, target, key, kind) => {
129
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
14
130
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
15
- if (kind && result) __defProp$1(target, key, result);
131
+ if (kind && result) __defProp$4(target, key, result);
16
132
  return result;
17
133
  };
18
134
  var BillOfMaterials = class extends SmrtObject {
@@ -47,12 +163,25 @@ var BillOfMaterials = class extends SmrtObject {
47
163
  if (options.notes !== void 0) this.notes = options.notes;
48
164
  if (options.currency !== void 0) this.currency = options.currency;
49
165
  }
166
+ /**
167
+ * Refuses saving the bill as `active` when one of its lines would make
168
+ * its product contain itself, directly or through sub-assemblies' active
169
+ * bills, with a `BomCycleError` naming the path. Draft and superseded
170
+ * bills are not part of the structure and are not checked here (their
171
+ * lines are checked as they are saved).
172
+ */
173
+ async validateBeforeSave() {
174
+ await super.validateBeforeSave();
175
+ if (this.status !== "active") return;
176
+ const { AssemblyService } = await Promise.resolve().then(() => AssemblyService_exports);
177
+ await (await AssemblyService.create({ db: this.db })).assertBillAcyclic(this);
178
+ }
50
179
  };
51
- __decorateClass$1([tenantId({ nullable: true })], BillOfMaterials.prototype, "tenantId", 2);
52
- __decorateClass$1([field({ required: true })], BillOfMaterials.prototype, "productId", 2);
53
- __decorateClass$1([field({ required: true })], BillOfMaterials.prototype, "version", 2);
54
- __decorateClass$1([field({ required: true })], BillOfMaterials.prototype, "status", 2);
55
- BillOfMaterials = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
180
+ __decorateClass$4([tenantId({ nullable: true })], BillOfMaterials.prototype, "tenantId", 2);
181
+ __decorateClass$4([field({ required: true })], BillOfMaterials.prototype, "productId", 2);
182
+ __decorateClass$4([field({ required: true })], BillOfMaterials.prototype, "version", 2);
183
+ __decorateClass$4([field({ required: true })], BillOfMaterials.prototype, "status", 2);
184
+ BillOfMaterials = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
56
185
  tableName: "manufacturing_boms",
57
186
  conflictColumns: [
58
187
  "product_id",
@@ -112,12 +241,12 @@ var BillOfMaterialsCollection = class extends SmrtCollection {
112
241
  };
113
242
  //#endregion
114
243
  //#region src/models/BomLine.ts
115
- var __defProp = Object.defineProperty;
116
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
117
- var __decorateClass = (decorators, target, key, kind) => {
118
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
244
+ var __defProp$3 = Object.defineProperty;
245
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
246
+ var __decorateClass$3 = (decorators, target, key, kind) => {
247
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
119
248
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
120
- if (kind && result) __defProp(target, key, result);
249
+ if (kind && result) __defProp$3(target, key, result);
121
250
  return result;
122
251
  };
123
252
  var BomLine = class extends SmrtObject {
@@ -158,13 +287,24 @@ var BomLine = class extends SmrtObject {
158
287
  effectiveQtyPerUnit() {
159
288
  return Number(this.qtyPerUnit ?? 0) * (1 + Number(this.wastePercent ?? 0) / 100);
160
289
  }
290
+ /**
291
+ * Refuses a line that would make its bill's product contain itself,
292
+ * directly or through sub-assemblies' active bills, with a
293
+ * `BomCycleError` naming the path. Runs on every save, so the generated
294
+ * REST `create`/`update` routes refuse it too.
295
+ */
296
+ async validateBeforeSave() {
297
+ await super.validateBeforeSave();
298
+ const { AssemblyService } = await Promise.resolve().then(() => AssemblyService_exports);
299
+ await (await AssemblyService.create({ db: this.db })).assertLineAcyclic(this);
300
+ }
161
301
  };
162
- __decorateClass([tenantId({ nullable: true })], BomLine.prototype, "tenantId", 2);
163
- __decorateClass([field({ required: true })], BomLine.prototype, "bomId", 2);
164
- __decorateClass([field({ required: true })], BomLine.prototype, "componentSkuId", 2);
165
- __decorateClass([field({ type: "decimal" })], BomLine.prototype, "qtyPerUnit", 2);
166
- __decorateClass([field({ type: "decimal" })], BomLine.prototype, "wastePercent", 2);
167
- BomLine = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
302
+ __decorateClass$3([tenantId({ nullable: true })], BomLine.prototype, "tenantId", 2);
303
+ __decorateClass$3([field({ required: true })], BomLine.prototype, "bomId", 2);
304
+ __decorateClass$3([field({ required: true })], BomLine.prototype, "componentSkuId", 2);
305
+ __decorateClass$3([field({ type: "decimal" })], BomLine.prototype, "qtyPerUnit", 2);
306
+ __decorateClass$3([field({ type: "decimal" })], BomLine.prototype, "wastePercent", 2);
307
+ BomLine = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
168
308
  tableName: "manufacturing_bom_lines",
169
309
  conflictColumns: [
170
310
  "bom_id",
@@ -208,24 +348,710 @@ var BomLineCollection = class extends SmrtCollection {
208
348
  }
209
349
  };
210
350
  //#endregion
351
+ //#region src/collections/OperationCollection.ts
352
+ var OperationCollection = class extends SmrtCollection {
353
+ static _itemClass = Operation;
354
+ /** Return the operation with the given code, or `null`. */
355
+ async findByCode(code) {
356
+ return (await this.list({
357
+ where: { code },
358
+ limit: 1
359
+ }))[0] ?? null;
360
+ }
361
+ /**
362
+ * Return operations ordered by category then code. Retired operations are
363
+ * left out unless `includeRetired` is set, so the default is a picker list.
364
+ */
365
+ async listOperations(options = {}) {
366
+ return this.list({
367
+ where: options.includeRetired ? {} : { isActive: true },
368
+ orderBy: ["category ASC", "code ASC"]
369
+ });
370
+ }
371
+ };
372
+ //#endregion
373
+ //#region src/quantity.ts
374
+ var QUANTITY_DECIMALS = 6;
375
+ var SCALE = 10 ** 6;
376
+ var MAX_QUANTITY = 999999999;
377
+ function roundQuantity(value) {
378
+ return Number(value.toFixed(6));
379
+ }
380
+ function toMillionths(value) {
381
+ return Number(value.toFixed(6).replace(".", ""));
382
+ }
383
+ function fromMillionths(millionths) {
384
+ return millionths / SCALE;
385
+ }
386
+ //#endregion
387
+ //#region src/models/ProductionRun.ts
388
+ var __defProp$2 = Object.defineProperty;
389
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
390
+ var __decorateClass$2 = (decorators, target, key, kind) => {
391
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
392
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
393
+ if (kind && result) __defProp$2(target, key, result);
394
+ return result;
395
+ };
396
+ var PRODUCTION_RUN_STATUSES = [
397
+ "planned",
398
+ "in_progress",
399
+ "done",
400
+ "cancelled"
401
+ ];
402
+ var ProductionRun = class extends SmrtObject {
403
+ tenantId = null;
404
+ bomId = "";
405
+ targetQty = 0;
406
+ completedQty = 0;
407
+ status = "planned";
408
+ constructor(options = {}) {
409
+ super(options);
410
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
411
+ if (options.bomId !== void 0) this.bomId = options.bomId;
412
+ if (options.targetQty !== void 0) this.targetQty = options.targetQty;
413
+ if (options.completedQty !== void 0) this.completedQty = options.completedQty;
414
+ if (options.status !== void 0) this.status = options.status;
415
+ }
416
+ /**
417
+ * Units still to build, at the six-decimal quantity precision; `0` once
418
+ * the target is reached.
419
+ */
420
+ remainingQty() {
421
+ return Math.max(0, roundQuantity(Number(this.targetQty) - Number(this.completedQty)));
422
+ }
423
+ /** Refuses a status outside the lifecycle and impossible quantities. */
424
+ async validateBeforeSave() {
425
+ await super.validateBeforeSave();
426
+ if (!PRODUCTION_RUN_STATUSES.includes(this.status)) throw new ValidationError(`Unknown production run status: ${String(this.status)}`, "MANUFACTURING_PRODUCTION_RUN_INVALID", { field: "status" });
427
+ const target = Number(this.targetQty);
428
+ const completed = Number(this.completedQty);
429
+ if (!Number.isFinite(target) || target <= 0) throw new ValidationError("A production run needs a target quantity greater than zero.", "MANUFACTURING_PRODUCTION_RUN_INVALID", { field: "targetQty" });
430
+ if (!Number.isFinite(completed) || completed < 0) throw new ValidationError("A production run cannot have a negative completed quantity.", "MANUFACTURING_PRODUCTION_RUN_INVALID", { field: "completedQty" });
431
+ }
432
+ };
433
+ __decorateClass$2([tenantId({ nullable: true })], ProductionRun.prototype, "tenantId", 2);
434
+ __decorateClass$2([foreignKey(BillOfMaterials, {
435
+ required: true,
436
+ onDelete: "RESTRICT"
437
+ })], ProductionRun.prototype, "bomId", 2);
438
+ __decorateClass$2([field({ type: "decimal" })], ProductionRun.prototype, "targetQty", 2);
439
+ __decorateClass$2([field({ type: "decimal" })], ProductionRun.prototype, "completedQty", 2);
440
+ __decorateClass$2([field({ required: true })], ProductionRun.prototype, "status", 2);
441
+ ProductionRun = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
442
+ tableName: "manufacturing_production_runs",
443
+ api: { include: ["list", "get"] },
444
+ mcp: { include: ["list", "get"] },
445
+ cli: { include: ["list", "get"] }
446
+ })], ProductionRun);
447
+ //#endregion
448
+ //#region src/collections/ProductionRunCollection.ts
449
+ var ProductionRunCollection = class extends SmrtCollection {
450
+ static _itemClass = ProductionRun;
451
+ /** Runs against one bill, newest first. */
452
+ async findByBom(bomId) {
453
+ return this.list({
454
+ where: { bomId },
455
+ orderBy: "created_at DESC"
456
+ });
457
+ }
458
+ /** Runs in one status, oldest first (a work queue). */
459
+ async findByStatus(status) {
460
+ return this.list({
461
+ where: { status },
462
+ orderBy: "created_at ASC"
463
+ });
464
+ }
465
+ };
466
+ //#endregion
467
+ //#region src/models/ProductionRunCompletion.ts
468
+ var __defProp$1 = Object.defineProperty;
469
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
470
+ var __decorateClass$1 = (decorators, target, key, kind) => {
471
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
472
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
473
+ if (kind && result) __defProp$1(target, key, result);
474
+ return result;
475
+ };
476
+ var ProductionRunCompletion = class extends SmrtObject {
477
+ tenantId = null;
478
+ runId = "";
479
+ qty = 0;
480
+ /** When the units were finished (defaults to when the report was made). */
481
+ completedAt = /* @__PURE__ */ new Date();
482
+ constructor(options = {}) {
483
+ super(options);
484
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
485
+ if (options.runId !== void 0) this.runId = options.runId;
486
+ if (options.qty !== void 0) this.qty = options.qty;
487
+ if (options.completedAt !== void 0) this.completedAt = options.completedAt instanceof Date ? options.completedAt : new Date(options.completedAt);
488
+ }
489
+ /** Refuses a report of zero, negative or non-finite units. */
490
+ async validateBeforeSave() {
491
+ await super.validateBeforeSave();
492
+ const qty = Number(this.qty);
493
+ if (!Number.isFinite(qty) || qty <= 0) throw new ValidationError("A completion needs a quantity greater than zero.", "MANUFACTURING_PRODUCTION_RUN_INVALID", { field: "qty" });
494
+ }
495
+ };
496
+ __decorateClass$1([tenantId({ nullable: true })], ProductionRunCompletion.prototype, "tenantId", 2);
497
+ __decorateClass$1([foreignKey(ProductionRun, {
498
+ required: true,
499
+ onDelete: "CASCADE"
500
+ })], ProductionRunCompletion.prototype, "runId", 2);
501
+ __decorateClass$1([field({ type: "decimal" })], ProductionRunCompletion.prototype, "qty", 2);
502
+ ProductionRunCompletion = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
503
+ tableName: "manufacturing_production_run_completions",
504
+ api: { include: ["list", "get"] },
505
+ mcp: { include: ["list", "get"] },
506
+ cli: { include: ["list", "get"] }
507
+ })], ProductionRunCompletion);
508
+ //#endregion
509
+ //#region src/collections/ProductionRunCompletionCollection.ts
510
+ var ProductionRunCompletionCollection = class extends SmrtCollection {
511
+ static _itemClass = ProductionRunCompletion;
512
+ /** A run's completions in the order they happened. */
513
+ async findByRun(runId) {
514
+ return this.list({
515
+ where: { runId },
516
+ orderBy: ["completedAt ASC", "created_at ASC"]
517
+ });
518
+ }
519
+ };
520
+ //#endregion
521
+ //#region src/models/RoutingStep.ts
522
+ var __defProp = Object.defineProperty;
523
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
524
+ var __decorateClass = (decorators, target, key, kind) => {
525
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
526
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
527
+ if (kind && result) __defProp(target, key, result);
528
+ return result;
529
+ };
530
+ var RoutingStep = class extends SmrtObject {
531
+ tenantId = null;
532
+ bomId = "";
533
+ operationId = "";
534
+ sequence = 1;
535
+ estimatedMinutes = 0;
536
+ /** Optional free-form note for this step. */
537
+ notes = "";
538
+ constructor(options = {}) {
539
+ super(options);
540
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
541
+ if (options.bomId !== void 0) this.bomId = options.bomId;
542
+ if (options.operationId !== void 0) this.operationId = options.operationId;
543
+ if (options.sequence !== void 0) this.sequence = options.sequence;
544
+ if (options.estimatedMinutes !== void 0) this.estimatedMinutes = options.estimatedMinutes;
545
+ if (options.notes !== void 0) this.notes = options.notes;
546
+ }
547
+ };
548
+ __decorateClass([tenantId({ nullable: true })], RoutingStep.prototype, "tenantId", 2);
549
+ __decorateClass([foreignKey(BillOfMaterials, {
550
+ required: true,
551
+ onDelete: "CASCADE"
552
+ })], RoutingStep.prototype, "bomId", 2);
553
+ __decorateClass([foreignKey(Operation, {
554
+ required: true,
555
+ onDelete: "RESTRICT"
556
+ })], RoutingStep.prototype, "operationId", 2);
557
+ __decorateClass([field({ required: true })], RoutingStep.prototype, "sequence", 2);
558
+ __decorateClass([field({ type: "decimal" })], RoutingStep.prototype, "estimatedMinutes", 2);
559
+ RoutingStep = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
560
+ tableName: "manufacturing_routing_steps",
561
+ conflictColumns: [
562
+ "bom_id",
563
+ "sequence",
564
+ "tenant_id"
565
+ ],
566
+ api: { include: ["list", "get"] },
567
+ mcp: { include: ["list", "get"] },
568
+ cli: { skipApiCheck: true }
569
+ })], RoutingStep);
570
+ //#endregion
571
+ //#region src/collections/RoutingStepCollection.ts
572
+ var RoutingStepCollection = class extends SmrtCollection {
573
+ static _itemClass = RoutingStep;
574
+ /** Return the routing of a bill in step order; empty when it has none. */
575
+ async findByBom(bomId) {
576
+ return this.list({
577
+ where: { bomId },
578
+ orderBy: "sequence ASC"
579
+ });
580
+ }
581
+ /** Return every step that uses the given operation, across bills. */
582
+ async findByOperation(operationId) {
583
+ return this.list({
584
+ where: { operationId },
585
+ orderBy: "bomId ASC"
586
+ });
587
+ }
588
+ };
589
+ //#endregion
590
+ //#region src/operation-types.ts
591
+ var OperationNotFoundError = class extends Error {
592
+ constructor(operationId) {
593
+ super(`Operation not found: ${operationId}`);
594
+ this.operationId = operationId;
595
+ }
596
+ operationId;
597
+ name = "OperationNotFoundError";
598
+ };
599
+ var DuplicateOperationCodeError = class extends Error {
600
+ constructor(code) {
601
+ super(`An operation with code "${code}" already exists.`);
602
+ this.code = code;
603
+ }
604
+ code;
605
+ name = "DuplicateOperationCodeError";
606
+ };
607
+ var OperationRetiredError = class extends Error {
608
+ constructor(operationId) {
609
+ super(`Operation ${operationId} is retired and cannot be added to a routing.`);
610
+ this.operationId = operationId;
611
+ }
612
+ operationId;
613
+ name = "OperationRetiredError";
614
+ };
615
+ var InvalidOperationInputError = class extends Error {
616
+ name = "InvalidOperationInputError";
617
+ };
618
+ //#endregion
619
+ //#region src/production-run-types.ts
620
+ var ProductionRunNotFoundError = class extends Error {
621
+ constructor(runId) {
622
+ super(`Production run not found: ${runId}`);
623
+ this.runId = runId;
624
+ }
625
+ runId;
626
+ name = "ProductionRunNotFoundError";
627
+ };
628
+ var ProductionRunStateError = class extends Error {
629
+ constructor(runId, status, action) {
630
+ super(`Production run ${runId} is ${status}; cannot ${action} it.`);
631
+ this.runId = runId;
632
+ this.status = status;
633
+ this.action = action;
634
+ }
635
+ runId;
636
+ status;
637
+ action;
638
+ name = "ProductionRunStateError";
639
+ };
640
+ var ProductionRunOverCompletionError = class extends Error {
641
+ constructor(runId, qty, remaining) {
642
+ super(`Production run ${runId} has ${remaining} left to build; cannot report ${qty}.`);
643
+ this.runId = runId;
644
+ this.qty = qty;
645
+ this.remaining = remaining;
646
+ }
647
+ runId;
648
+ qty;
649
+ remaining;
650
+ name = "ProductionRunOverCompletionError";
651
+ };
652
+ var InvalidProductionRunInputError = class extends Error {
653
+ name = "InvalidProductionRunInputError";
654
+ };
655
+ //#endregion
656
+ //#region src/tenant-scope.ts
657
+ function readOwnAndGlobal(owner, read) {
658
+ const scope = owner ?? getCurrentTenant()?.tenantId ?? null;
659
+ return scope ? withTenantGlobalRead(scope, read) : read();
660
+ }
661
+ function isOwnOrGlobal(row, owner) {
662
+ const rowTenant = row.tenantId ?? null;
663
+ return rowTenant === null || rowTenant === owner;
664
+ }
665
+ //#endregion
666
+ //#region src/services/AssemblyService.ts
667
+ var AssemblyService_exports = /* @__PURE__ */ __exportAll({
668
+ AssemblyService: () => AssemblyService,
669
+ BomCycleError: () => BomCycleError,
670
+ createAssemblyService: () => createAssemblyService
671
+ });
672
+ var BomCycleError = class extends ValidationError {
673
+ constructor(bomId, path) {
674
+ const first = path[0]?.name ?? "";
675
+ super(`Refused: "${first}" would contain itself: ${path.map((entry) => entry.name).join(" → ")}`, "MANUFACTURING_BOM_CYCLE", {
676
+ bomId,
677
+ path
678
+ });
679
+ this.bomId = bomId;
680
+ this.path = path;
681
+ }
682
+ bomId;
683
+ path;
684
+ name = "BomCycleError";
685
+ };
686
+ var UUID_PATTERN$2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
687
+ function visibleTo(row, options) {
688
+ if (options.tenantId === void 0) return true;
689
+ const rowTenant = row.tenantId ?? null;
690
+ return rowTenant === null || rowTenant === options.tenantId;
691
+ }
692
+ function isUuid(value) {
693
+ return typeof value === "string" && UUID_PATTERN$2.test(value);
694
+ }
695
+ var AssemblyService = class AssemblyService {
696
+ constructor(products, skus, boms, lines) {
697
+ this.products = products;
698
+ this.skus = skus;
699
+ this.boms = boms;
700
+ this.lines = lines;
701
+ }
702
+ products;
703
+ skus;
704
+ boms;
705
+ lines;
706
+ /** Factory — prefer {@link createAssemblyService}. */
707
+ static async create(options) {
708
+ const db = await resolveDatabase(options.db);
709
+ const [products, skus, boms, lines] = await Promise.all([
710
+ ProductCollection.create({ db }),
711
+ SkuCollection.create({ db }),
712
+ BillOfMaterialsCollection.create({ db }),
713
+ BomLineCollection.create({ db })
714
+ ]);
715
+ return new AssemblyService(products, skus, boms, lines);
716
+ }
717
+ /**
718
+ * Resolve a component SKU to what it is: an assembly (with its active bill,
719
+ * or `null` when it has none), a material, a bought item, or missing. Never
720
+ * throws for an unknown id; it reports `missing` instead.
721
+ *
722
+ * This is the documented link a multi-level explosion walks: recurse into
723
+ * `activeBom` while `kind === 'assembly'`, stop at every other kind. Pass
724
+ * the parent bill's `tenantId` so a shared assembly resolves to that
725
+ * tenant's bill, not another tenant's.
726
+ */
727
+ async resolveComponent(skuId, options = {}) {
728
+ const missing = (sku2) => ({
729
+ skuId,
730
+ kind: "missing",
731
+ sku: sku2,
732
+ product: null,
733
+ assembly: null,
734
+ activeBom: null,
735
+ buildable: false
736
+ });
737
+ if (!isUuid(skuId)) return missing(null);
738
+ const sku = await this.byId(this.skus, skuId, options);
739
+ if (!sku || !visibleTo(sku, options)) return missing(null);
740
+ if (!isUuid(sku.productId)) return missing(sku);
741
+ const product = await this.byId(this.products, sku.productId, options);
742
+ if (!product || !visibleTo(product, options)) return missing(sku);
743
+ if (product instanceof Assembly) {
744
+ const activeBom = await this.findActiveBom(product.id, options);
745
+ return {
746
+ skuId,
747
+ kind: "assembly",
748
+ sku,
749
+ product,
750
+ assembly: product,
751
+ activeBom,
752
+ buildable: activeBom !== null
753
+ };
754
+ }
755
+ return {
756
+ skuId,
757
+ kind: product instanceof Material ? "material" : "bought",
758
+ sku,
759
+ product,
760
+ assembly: null,
761
+ activeBom: null,
762
+ buildable: false
763
+ };
764
+ }
765
+ /**
766
+ * The highest-version active bill for a product. With `options.tenantId`,
767
+ * that tenant's own bill, or else a global one; another tenant's never.
768
+ */
769
+ async findActiveBom(productId, options = {}) {
770
+ const owner = options.tenantId;
771
+ if (owner === void 0) return this.boms.findActiveForProduct(productId);
772
+ const active = await readOwnAndGlobal(owner, () => this.boms.list({
773
+ where: {
774
+ productId,
775
+ status: "active"
776
+ },
777
+ orderBy: "version DESC"
778
+ }));
779
+ return active.find((bom) => (bom.tenantId ?? null) === owner) ?? active.find((bom) => (bom.tenantId ?? null) === null) ?? null;
780
+ }
781
+ /**
782
+ * One row by id: through the collection's `get` without an explicit
783
+ * tenant, else as a list read of that tenant's and global rows.
784
+ */
785
+ async byId(collection, id, options) {
786
+ const owner = options.tenantId;
787
+ if (owner === void 0) return collection.get({ id });
788
+ return (await readOwnAndGlobal(owner, () => collection.list({
789
+ where: { id },
790
+ limit: 1
791
+ })))[0] ?? null;
792
+ }
793
+ /** `true` when the SKU belongs to an {@link Assembly}. */
794
+ async isAssembly(skuId) {
795
+ return (await this.resolveComponent(skuId)).kind === "assembly";
796
+ }
797
+ /**
798
+ * Read one bill and resolve every line's component. Sub-assembly lines say
799
+ * whether they have an active bill of their own (`component.activeBom`);
800
+ * the caller opens the next level by passing that id back in.
801
+ *
802
+ * @throws {BomNotFoundError} when no bill has this id.
803
+ */
804
+ async getBillStructure(bomId) {
805
+ const bom = isUuid(bomId) ? await this.boms.get({ id: bomId }) : null;
806
+ if (!bom) throw new BomNotFoundError(bomId);
807
+ const cache = /* @__PURE__ */ new Map();
808
+ const scope = { tenantId: bom.tenantId ?? null };
809
+ const lines = (await readOwnAndGlobal(scope.tenantId, () => this.lines.findByBom(bomId))).filter((line) => visibleTo(line, scope));
810
+ const resolved = [];
811
+ for (const line of lines) resolved.push({
812
+ line,
813
+ component: await this.resolveCached(line.componentSkuId, cache, scope)
814
+ });
815
+ return {
816
+ bom,
817
+ lines: resolved
818
+ };
819
+ }
820
+ /**
821
+ * Find the path by which adding `componentSkuIds` to a bill for
822
+ * `productId` would make that product contain itself, or `null` when none
823
+ * would. The walk follows each sub-assembly's **active** bill only; a
824
+ * draft or superseded bill is not part of the structure. Pass the bill's
825
+ * `tenantId` so the walk reads that tenant's structure.
826
+ */
827
+ async findCycle(productId, componentSkuIds, options = {}) {
828
+ const cache = /* @__PURE__ */ new Map();
829
+ const visited = /* @__PURE__ */ new Set();
830
+ const walk = async (skuIds, trail2) => {
831
+ for (const skuId of skuIds) {
832
+ const component = await this.resolveCached(skuId, cache, options);
833
+ const product = component.product;
834
+ if (!product?.id) continue;
835
+ if (product.id === productId) return [...trail2, product];
836
+ if (!component.activeBom || visited.has(product.id)) continue;
837
+ visited.add(product.id);
838
+ const billId = component.activeBom.id;
839
+ const lines = (options.tenantId === void 0 ? await this.lines.findByBom(billId) : await readOwnAndGlobal(options.tenantId, () => this.lines.findByBom(billId))).filter((line) => visibleTo(line, options));
840
+ const found = await walk(lines.map((line) => line.componentSkuId), [...trail2, product]);
841
+ if (found) return found;
842
+ }
843
+ return null;
844
+ };
845
+ const trail = await walk(componentSkuIds, []);
846
+ if (!trail) return null;
847
+ return [trail[trail.length - 1], ...trail].map((product) => ({
848
+ productId: product.id,
849
+ name: product.name || product.id
850
+ }));
851
+ }
852
+ /**
853
+ * Refuse a bill line that would make its bill's product contain itself.
854
+ * A line on a draft bill is checked too, so a cycle is refused when it is
855
+ * written rather than when the bill is activated. A line whose bill does
856
+ * not exist is left to the caller.
857
+ *
858
+ * @throws {BomCycleError} naming the path.
859
+ */
860
+ async assertLineAcyclic(line) {
861
+ if (!isUuid(line.bomId)) return;
862
+ const bom = await this.boms.get({ id: line.bomId });
863
+ if (!bom) return;
864
+ const path = await this.findCycle(bom.productId, [line.componentSkuId], { tenantId: bom.tenantId ?? null });
865
+ if (path) throw new BomCycleError(line.bomId, path);
866
+ }
867
+ /**
868
+ * Refuse saving a bill as `active` when any of its lines would make its
869
+ * product contain itself. Bills in any other status are not part of the
870
+ * structure and pass.
871
+ *
872
+ * The lines checked are the bill's own and those of any stored bill with
873
+ * the same natural key (`productId`, `version`, and the bill's tenant, or
874
+ * the active tenant when the bill has none yet): saving a new
875
+ * instance with that key upserts onto the stored bill and activates its
876
+ * lines, so they are part of what the save makes active.
877
+ *
878
+ * @throws {BomCycleError} naming the path.
879
+ */
880
+ async assertBillAcyclic(bom) {
881
+ if (bom.status !== "active") return;
882
+ const billIds = /* @__PURE__ */ new Set();
883
+ if (isUuid(bom.id)) billIds.add(bom.id);
884
+ const tenantId = bom.tenantId ?? getCurrentTenant()?.tenantId ?? null;
885
+ const sameKey = await readOwnAndGlobal(tenantId, () => this.boms.list({ where: {
886
+ productId: bom.productId,
887
+ version: bom.version
888
+ } }));
889
+ for (const stored of sameKey) if (stored.id && (stored.tenantId ?? null) === tenantId) billIds.add(stored.id);
890
+ const componentSkuIds = [];
891
+ for (const billId of billIds) for (const line of await readOwnAndGlobal(tenantId, () => this.lines.findByBom(billId))) if (visibleTo(line, { tenantId })) componentSkuIds.push(line.componentSkuId);
892
+ if (componentSkuIds.length === 0) return;
893
+ const path = await this.findCycle(bom.productId, componentSkuIds, { tenantId });
894
+ if (path) throw new BomCycleError(bom.id || [...billIds][0] || "", path);
895
+ }
896
+ resolveCached(skuId, cache, options) {
897
+ let pending = cache.get(skuId);
898
+ if (!pending) {
899
+ pending = this.resolveComponent(skuId, options);
900
+ cache.set(skuId, pending);
901
+ }
902
+ return pending;
903
+ }
904
+ };
905
+ function createAssemblyService(options) {
906
+ return AssemblyService.create(options);
907
+ }
908
+ //#endregion
909
+ //#region src/services/structure-walk.ts
910
+ var BomStructureCycleError = class extends BomCycleError {
911
+ name = "BomStructureCycleError";
912
+ constructor(bomId, path) {
913
+ super(bomId, path);
914
+ this.message = `The bill structure contains a cycle: ${path.map((entry) => entry.name).join(" → ")}`;
915
+ }
916
+ };
917
+ var UUID_PATTERN$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
918
+ function maxLevelFor(levels) {
919
+ if (levels === void 0) return 1;
920
+ if (levels === "all") return 32;
921
+ if (!Number.isInteger(levels) || levels < 1 || levels > 32) throw new Error(`levels must be a whole number from 1 to 32, or 'all' (got ${String(levels)})`);
922
+ return levels;
923
+ }
924
+ function isMultiLevel(levels) {
925
+ return maxLevelFor(levels) > 1;
926
+ }
927
+ var StructureWalk = class {
928
+ constructor(assemblies, top, levels) {
929
+ this.assemblies = assemblies;
930
+ this.top = top;
931
+ this.levels = levels;
932
+ this.maxLevel = maxLevelFor(levels);
933
+ }
934
+ assemblies;
935
+ top;
936
+ levels;
937
+ components = /* @__PURE__ */ new Map();
938
+ bills = /* @__PURE__ */ new Map();
939
+ lineCount = 0;
940
+ /** The deepest level whose lines the walk reads. */
941
+ maxLevel;
942
+ /**
943
+ * Resolve a component SKU, reading assemblies' active bills in the top
944
+ * bill's tenant so a shared assembly opens that tenant's bill.
945
+ */
946
+ resolve(skuId) {
947
+ let pending = this.components.get(skuId);
948
+ if (!pending) {
949
+ pending = this.assemblies.resolveComponent(skuId, { tenantId: this.top.tenantId ?? null });
950
+ this.components.set(skuId, pending);
951
+ }
952
+ return pending;
953
+ }
954
+ /**
955
+ * A bill's lines in the top bill's tenant (and global lines), counted
956
+ * against {@link MAX_EXPLOSION_LINES} every time the walk visits them (a
957
+ * shared sub-assembly counts once per use).
958
+ */
959
+ async linesOf(bomId) {
960
+ let pending = this.bills.get(bomId);
961
+ if (!pending) {
962
+ const tenantId = this.top.tenantId ?? null;
963
+ pending = readOwnAndGlobal(tenantId, () => this.assemblies.lines.findByBom(bomId)).then((lines2) => lines2.filter((line) => isOwnOrGlobal(line, tenantId)));
964
+ this.bills.set(bomId, pending);
965
+ }
966
+ const lines = await pending;
967
+ this.lineCount += lines.length;
968
+ if (this.lineCount > 1e4) throw new BomExplosionLimitError(this.top.id, "lines");
969
+ return lines;
970
+ }
971
+ /** The path entry for the top bill. */
972
+ async root() {
973
+ const productId = this.top.productId;
974
+ let name = productId;
975
+ if (UUID_PATTERN$1.test(productId)) {
976
+ const owner = this.top.tenantId ?? null;
977
+ const [product] = await readOwnAndGlobal(owner, () => this.assemblies.products.list({
978
+ where: { id: productId },
979
+ limit: 1
980
+ }));
981
+ if (product?.name && isOwnOrGlobal(product, owner)) name = product.name;
982
+ }
983
+ return {
984
+ bomId: this.top.id,
985
+ productId,
986
+ name
987
+ };
988
+ }
989
+ /**
990
+ * The bill to open below a line at `level`, or `null` when the component
991
+ * is not a sub-assembly with an active bill or `levels` stops here.
992
+ *
993
+ * @throws {BomStructureCycleError} when the sub-assembly is already on
994
+ * `path` (it would contain itself).
995
+ * @throws {BomExplosionLimitError} when `levels: 'all'` would go deeper
996
+ * than {@link MAX_EXPLOSION_DEPTH}.
997
+ */
998
+ open(component, level, path) {
999
+ const bom = component.activeBom;
1000
+ const product = component.product;
1001
+ if (!component.buildable || !bom?.id || !product?.id) return null;
1002
+ const name = product.name || product.id;
1003
+ const at = path.findIndex((entry) => entry.productId === product.id);
1004
+ if (at >= 0) throw new BomStructureCycleError(this.top.id, [...path.slice(at).map((entry) => ({
1005
+ productId: entry.productId,
1006
+ name: entry.name
1007
+ })), {
1008
+ productId: product.id,
1009
+ name
1010
+ }]);
1011
+ if (level >= this.maxLevel) {
1012
+ if (this.levels === "all") throw new BomExplosionLimitError(this.top.id, "depth");
1013
+ return null;
1014
+ }
1015
+ return {
1016
+ bomId: bom.id,
1017
+ productId: product.id,
1018
+ name
1019
+ };
1020
+ }
1021
+ };
1022
+ //#endregion
211
1023
  //#region src/services/BomService.ts
212
1024
  var BomService = class BomService {
213
- constructor(boms, lines, stockService, costResolver) {
1025
+ constructor(boms, lines, stockService, costResolver, routingSteps, operations, rateResolver, assemblies) {
214
1026
  this.boms = boms;
215
1027
  this.lines = lines;
216
1028
  this.stockService = stockService;
217
1029
  this.costResolver = costResolver;
1030
+ this.routingSteps = routingSteps;
1031
+ this.operations = operations;
1032
+ this.rateResolver = rateResolver;
1033
+ this.assemblies = assemblies;
218
1034
  }
219
1035
  boms;
220
1036
  lines;
221
1037
  stockService;
222
1038
  costResolver;
1039
+ routingSteps;
1040
+ operations;
1041
+ rateResolver;
1042
+ assemblies;
223
1043
  /** Factory — prefer {@link createBomService}. */
224
1044
  static async create(options) {
225
1045
  const stockService = options.stockService ?? await buildStockService$1(options);
226
- const sharedDb = options.db ?? stockService.db;
227
- const [boms, lines] = await Promise.all([BillOfMaterialsCollection.create({ db: sharedDb }), BomLineCollection.create({ db: sharedDb })]);
228
- return new BomService(boms, lines, stockService, options.costResolver);
1046
+ const sharedDb = await resolveDatabase(options.stockService ? options.db ?? stockService.db : stockService.db);
1047
+ const [boms, lines, routingSteps, operations, assemblies] = await Promise.all([
1048
+ BillOfMaterialsCollection.create({ db: sharedDb }),
1049
+ BomLineCollection.create({ db: sharedDb }),
1050
+ RoutingStepCollection.create({ db: sharedDb }),
1051
+ OperationCollection.create({ db: sharedDb }),
1052
+ AssemblyService.create({ db: sharedDb })
1053
+ ]);
1054
+ return new BomService(boms, lines, stockService, options.costResolver, routingSteps, operations, options.rateResolver, assemblies);
229
1055
  }
230
1056
  /**
231
1057
  * Walk every {@link BomLine} for the given BOM, resolve each line's
@@ -238,10 +1064,19 @@ var BomService = class BomService {
238
1064
  * set `costUnavailable: true` on their breakdown row; the aggregate
239
1065
  * `hasMissingCosts` flag mirrors this so callers can surface a UI
240
1066
  * warning.
1067
+ *
1068
+ * With `{ levels }` above 1, a sub-assembly line whose bill the walk opens
1069
+ * is costed from that bill instead of the resolver: its `unitCost` is the
1070
+ * bill's rolled-up material cost per unit, its `components` hold that
1071
+ * bill's lines, and a missing cost anywhere below marks it and
1072
+ * `hasMissingCosts` (its known part still counts, so the total is a lower
1073
+ * bound). Other lines, including sub-assemblies with no active bill, use
1074
+ * the resolver as before.
241
1075
  */
242
- async computeMaterialCost(bomId) {
1076
+ async computeMaterialCost(bomId, options = {}) {
1077
+ if (isMultiLevel(options.levels)) return this.computeMultiLevelCost(bomId, options);
243
1078
  const bom = await this.requireBom(bomId);
244
- const lines = await this.lines.findByBom(bomId);
1079
+ const lines = await this.linesOf(bom);
245
1080
  const lineBreakdown = [];
246
1081
  let totalCost = 0;
247
1082
  let hasMissingCosts = false;
@@ -273,6 +1108,66 @@ var BomService = class BomService {
273
1108
  };
274
1109
  }
275
1110
  /**
1111
+ * Roll up the estimated labour to build one unit from the bill's routing:
1112
+ * total minutes, plus cost when a `rateResolver` was supplied (hourly rate
1113
+ * times minutes, per step). A bill with no routing returns an empty
1114
+ * estimate with zero totals, so callers can add it to the material cost
1115
+ * unconditionally. Retired operations still count: the routing names them.
1116
+ *
1117
+ * With `{ levels }` above 1, every sub-assembly the walk opens adds its own
1118
+ * bill's routing, multiplied by the units of it one unit needs (waste
1119
+ * included); see {@link LabourEstimate.subAssemblies}.
1120
+ *
1121
+ * Throws {@link BomNotFoundError} when the BOM does not exist.
1122
+ */
1123
+ async computeLabourEstimate(bomId, options = {}) {
1124
+ if (isMultiLevel(options.levels)) return this.computeMultiLevelLabour(bomId, options);
1125
+ return this.labourFor(await this.requireBom(bomId));
1126
+ }
1127
+ /** One bill's own routing estimate, per unit of its product. */
1128
+ async labourFor(bom) {
1129
+ const bomId = bom.id;
1130
+ const owner = bom.tenantId ?? null;
1131
+ const routing = (await readOwnAndGlobal(owner, () => this.routingSteps.findByBom(bomId))).filter((step) => isOwnOrGlobal(step, owner));
1132
+ const steps = [];
1133
+ let totalMinutes = 0;
1134
+ let totalCost = 0;
1135
+ let hasMissingRates = false;
1136
+ for (const step of routing) {
1137
+ const [operation] = (await readOwnAndGlobal(owner, () => this.operations.list({
1138
+ where: { id: step.operationId },
1139
+ limit: 1
1140
+ }))).filter((row) => isOwnOrGlobal(row, owner));
1141
+ const minutes = Number(step.estimatedMinutes ?? 0);
1142
+ const rate = operation ? await this.resolveRate(operation) : null;
1143
+ const rateUnavailable = rate === null;
1144
+ const stepCost = rate === null ? 0 : minutes / 60 * rate;
1145
+ totalMinutes += minutes;
1146
+ if (rateUnavailable) hasMissingRates = true;
1147
+ else totalCost += stepCost;
1148
+ steps.push({
1149
+ stepId: step.id,
1150
+ sequence: step.sequence,
1151
+ operationId: step.operationId,
1152
+ operationCode: operation?.code ?? "",
1153
+ operationName: operation?.name ?? "",
1154
+ estimatedMinutes: minutes,
1155
+ hourlyRate: rate ?? 0,
1156
+ stepCost,
1157
+ rateUnavailable
1158
+ });
1159
+ }
1160
+ return {
1161
+ bomId,
1162
+ hasRouting: steps.length > 0,
1163
+ totalMinutes,
1164
+ totalCost,
1165
+ currency: bom.currency || "USD",
1166
+ steps,
1167
+ hasMissingRates
1168
+ };
1169
+ }
1170
+ /**
276
1171
  * Return a "shopping list" of materials needed to produce `qty` units of
277
1172
  * the parent product against the given BOM. Lines that reference the
278
1173
  * same component SKU are summed so each `componentSkuId` appears once
@@ -282,13 +1177,17 @@ var BomService = class BomService {
282
1177
  * {@link ProductionService.consumeMaterials} when you're ready to write
283
1178
  * stock movements.
284
1179
  *
1180
+ * With `{ levels }` above 1, sub-assemblies with an active bill are
1181
+ * replaced by what their bills need, waste compounding down the levels;
1182
+ * this is {@link explode}'s `totals`.
1183
+ *
285
1184
  * Throws {@link BomNotFoundError} when the BOM does not exist; throws a
286
1185
  * plain `Error` when `qty` is not a positive finite number.
287
1186
  */
288
- async explodeRequirements(bomId, qty) {
1187
+ async explodeRequirements(bomId, qty, options = {}) {
1188
+ if (isMultiLevel(options.levels)) return (await this.explode(bomId, qty, options)).totals;
289
1189
  assertPositiveQty$1(qty, "explodeRequirements");
290
- await this.requireBom(bomId);
291
- const lines = await this.lines.findByBom(bomId);
1190
+ const lines = await this.linesOf(await this.requireBom(bomId));
292
1191
  const aggregate = /* @__PURE__ */ new Map();
293
1192
  for (const line of lines) {
294
1193
  const effectiveQty = line.effectiveQtyPerUnit() * qty;
@@ -314,9 +1213,25 @@ var BomService = class BomService {
314
1213
  * "where do we pull from?" is left to the caller of
315
1214
  * {@link ProductionService.consumeMaterials}).
316
1215
  *
1216
+ * With `{ levels }` above 1 the check is {@link planRequirements}: a
1217
+ * sub-assembly that is short and has an active bill is opened for its
1218
+ * shortfall, and each shortage is reported on the line where it occurs,
1219
+ * with its `level` and `path`. Stock is allocated as the walk goes, so a
1220
+ * component used in two places is not counted twice.
1221
+ *
317
1222
  * Throws {@link BomNotFoundError} when the BOM does not exist.
318
1223
  */
319
- async canProduce(bomId, qty) {
1224
+ async canProduce(bomId, qty, options = {}) {
1225
+ if (isMultiLevel(options.levels)) {
1226
+ const plan = await this.planRequirements(bomId, qty, options);
1227
+ return plan.ok ? {
1228
+ ok: true,
1229
+ shortages: []
1230
+ } : {
1231
+ ok: false,
1232
+ shortages: plan.shortages
1233
+ };
1234
+ }
320
1235
  const requirements = await this.explodeRequirements(bomId, qty);
321
1236
  const availabilities = await Promise.all(requirements.map(async (requirement) => ({
322
1237
  requirement,
@@ -338,16 +1253,262 @@ var BomService = class BomService {
338
1253
  };
339
1254
  }
340
1255
  /**
1256
+ * Walk the bill for `qty` units and return every line, depth first, with
1257
+ * its level, the path of bills above it and its quantity for the run, plus
1258
+ * the rolled-up `totals`. Gross requirements: stock is not read.
1259
+ *
1260
+ * `levels` (default `1`) says how far to open sub-assemblies; see
1261
+ * {@link ExplosionLevels}. Waste compounds: a sub-assembly line's quantity
1262
+ * includes its own waste, and its bill's lines are multiplied by it.
1263
+ *
1264
+ * Throws {@link BomNotFoundError}, `BomStructureCycleError` when stored
1265
+ * data already has a product containing itself, `BomExplosionLimitError`
1266
+ * past the depth or line limits, and a plain `Error` for a bad `qty` or
1267
+ * `levels`.
1268
+ */
1269
+ async explode(bomId, qty, options = {}) {
1270
+ assertPositiveQty$1(qty, "explode");
1271
+ const levels = options.levels ?? 1;
1272
+ const top = await this.requireBom(bomId);
1273
+ const walk = new StructureWalk(this.assemblies, top, levels);
1274
+ const lines = [];
1275
+ const visit = async (billId, level, path, parentQty) => {
1276
+ for (const line of await walk.linesOf(billId)) {
1277
+ const component = await walk.resolve(line.componentSkuId);
1278
+ const sub = walk.open(component, level, path);
1279
+ const exploded = explodedLine(line, component, level, path, parentQty);
1280
+ exploded.expanded = sub !== null;
1281
+ lines.push(exploded);
1282
+ if (sub) await visit(sub.bomId, level + 1, [...path, sub], exploded.totalQty);
1283
+ }
1284
+ };
1285
+ await visit(bomId, 1, [await walk.root()], qty);
1286
+ return {
1287
+ bomId,
1288
+ productId: top.productId,
1289
+ qty,
1290
+ levels,
1291
+ lines,
1292
+ totals: sumRequirements(lines.filter((line) => !line.expanded))
1293
+ };
1294
+ }
1295
+ /**
1296
+ * Net the walk against stock: for each line, the available stock (all
1297
+ * locations) not yet allocated to an earlier line and how much is short.
1298
+ * A sub-assembly that is short and has an active bill is opened for the
1299
+ * shortfall only, so its lines say what building that shortfall takes;
1300
+ * one covered by stock is not opened. Lines that are short and cannot be
1301
+ * opened are the `shortages`, each at its own level.
1302
+ *
1303
+ * These are facts, not a decision: the application decides whether a
1304
+ * short sub-assembly is built or bought. Does not mutate stock.
1305
+ *
1306
+ * Throws as {@link explode} does.
1307
+ */
1308
+ async planRequirements(bomId, qty, options = {}) {
1309
+ assertPositiveQty$1(qty, "planRequirements");
1310
+ const levels = options.levels ?? 1;
1311
+ const top = await this.requireBom(bomId);
1312
+ const walk = new StructureWalk(this.assemblies, top, levels);
1313
+ const lines = [];
1314
+ const shortages = [];
1315
+ const unallocated = /* @__PURE__ */ new Map();
1316
+ const tenantId = top.tenantId ?? null;
1317
+ const availableFor = (skuId) => {
1318
+ let pending = unallocated.get(skuId);
1319
+ if (!pending) {
1320
+ pending = this.availableInTenant(skuId, tenantId);
1321
+ unallocated.set(skuId, pending);
1322
+ }
1323
+ return pending;
1324
+ };
1325
+ const visit = async (billId, level, path, parentQty) => {
1326
+ for (const line of await walk.linesOf(billId)) {
1327
+ const component = await walk.resolve(line.componentSkuId);
1328
+ const base = explodedLine(line, component, level, path, parentQty);
1329
+ const available = await availableFor(line.componentSkuId);
1330
+ const allocated = Math.min(available, base.totalQty);
1331
+ unallocated.set(line.componentSkuId, Promise.resolve(available - allocated));
1332
+ const short = Math.max(0, roundQuantity(base.totalQty - allocated));
1333
+ const sub = short > 0 ? walk.open(component, level, path) : null;
1334
+ lines.push({
1335
+ ...base,
1336
+ expanded: sub !== null,
1337
+ available,
1338
+ short
1339
+ });
1340
+ if (sub) await visit(sub.bomId, level + 1, [...path, sub], short);
1341
+ else if (short > 0) shortages.push({
1342
+ componentSkuId: line.componentSkuId,
1343
+ requested: base.totalQty,
1344
+ available,
1345
+ level,
1346
+ path,
1347
+ uom: base.uom,
1348
+ buildable: component.buildable
1349
+ });
1350
+ }
1351
+ };
1352
+ await visit(bomId, 1, [await walk.root()], qty);
1353
+ return {
1354
+ bomId,
1355
+ productId: top.productId,
1356
+ qty,
1357
+ levels,
1358
+ lines,
1359
+ shortages,
1360
+ ok: shortages.length === 0
1361
+ };
1362
+ }
1363
+ /**
341
1364
  * Fetch a BOM by id or throw {@link BomNotFoundError}. Centralises the
342
1365
  * "missing BOM" error path so callers don't have to repeat the check.
343
1366
  */
1367
+ /**
1368
+ * A bill's lines: its own tenant's and global ones, whatever tenant
1369
+ * context is active (a context alone would hide a global bill's lines).
1370
+ */
1371
+ async linesOf(bom) {
1372
+ const owner = bom.tenantId ?? null;
1373
+ return (await readOwnAndGlobal(owner, () => this.lines.findByBom(bom.id))).filter((line) => isOwnOrGlobal(line, owner));
1374
+ }
344
1375
  async requireBom(bomId) {
345
1376
  if (!bomId) throw new BomNotFoundError(bomId);
346
- const bom = await this.boms.get(bomId);
1377
+ const tenantId = getCurrentTenant()?.tenantId;
1378
+ const bom = tenantId ? (await readOwnAndGlobal(tenantId, () => this.boms.list({
1379
+ where: { id: bomId },
1380
+ limit: 1
1381
+ }))).find((row) => isOwnOrGlobal(row, tenantId)) ?? null : await this.boms.get(bomId);
347
1382
  if (!bom) throw new BomNotFoundError(bomId);
348
1383
  return bom;
349
1384
  }
350
1385
  /**
1386
+ * Available stock of a SKU across locations, counting only the top bill's
1387
+ * tenant's rows and global ones, whatever tenant context is active (the
1388
+ * walk reads its structure the same way).
1389
+ */
1390
+ async availableInTenant(skuId, tenantId) {
1391
+ const rows = await readOwnAndGlobal(tenantId, () => this.stockService.levels.list({ where: {
1392
+ skuId,
1393
+ state: "available"
1394
+ } }));
1395
+ let total = 0;
1396
+ for (const row of rows) if (isOwnOrGlobal(row, tenantId)) total += Number(row.qty ?? 0);
1397
+ return Math.max(0, total);
1398
+ }
1399
+ /** {@link computeMaterialCost} with `levels` above 1. */
1400
+ async computeMultiLevelCost(bomId, options) {
1401
+ const top = await this.requireBom(bomId);
1402
+ const currency = top.currency || "USD";
1403
+ const walk = new StructureWalk(this.assemblies, top, options.levels ?? 1);
1404
+ const rollUp = async (billId, level, path) => {
1405
+ const lineBreakdown = [];
1406
+ let totalCost = 0;
1407
+ let hasMissingCosts = false;
1408
+ for (const line of await walk.linesOf(billId)) {
1409
+ const component = await walk.resolve(line.componentSkuId);
1410
+ const sub = walk.open(component, level, path);
1411
+ const effectiveQty = line.effectiveQtyPerUnit();
1412
+ const entry = {
1413
+ componentSkuId: line.componentSkuId,
1414
+ qtyPerUnit: Number(line.qtyPerUnit ?? 0),
1415
+ wastePercent: Number(line.wastePercent ?? 0),
1416
+ effectiveQty,
1417
+ unitCost: 0,
1418
+ lineCost: 0,
1419
+ uom: line.uom,
1420
+ costUnavailable: false
1421
+ };
1422
+ const sameCurrency = (component.activeBom?.currency || "USD") === currency;
1423
+ if (sub && sameCurrency) {
1424
+ const inner = await rollUp(sub.bomId, level + 1, [...path, sub]);
1425
+ entry.unitCost = inner.totalCost;
1426
+ entry.lineCost = inner.totalCost * effectiveQty;
1427
+ entry.costUnavailable = inner.hasMissingCosts;
1428
+ entry.subBomId = sub.bomId;
1429
+ entry.components = inner.lineBreakdown;
1430
+ totalCost += entry.lineCost;
1431
+ } else {
1432
+ const resolved = await this.resolveCost(line.componentSkuId);
1433
+ entry.unitCost = resolved ?? 0;
1434
+ entry.lineCost = entry.unitCost * effectiveQty;
1435
+ entry.costUnavailable = resolved === null;
1436
+ if (!entry.costUnavailable) totalCost += entry.lineCost;
1437
+ }
1438
+ if (entry.costUnavailable) hasMissingCosts = true;
1439
+ lineBreakdown.push(entry);
1440
+ }
1441
+ return {
1442
+ totalCost,
1443
+ lineBreakdown,
1444
+ hasMissingCosts
1445
+ };
1446
+ };
1447
+ return {
1448
+ bomId,
1449
+ currency,
1450
+ ...await rollUp(bomId, 1, [await walk.root()])
1451
+ };
1452
+ }
1453
+ /** {@link computeLabourEstimate} with `levels` above 1. */
1454
+ async computeMultiLevelLabour(bomId, options) {
1455
+ const top = await this.requireBom(bomId);
1456
+ const walk = new StructureWalk(this.assemblies, top, options.levels ?? 1);
1457
+ const own = await this.labourFor(top);
1458
+ const estimates = /* @__PURE__ */ new Map();
1459
+ const subAssemblies = [];
1460
+ const visit = async (billId, level, path, units) => {
1461
+ for (const line of await walk.linesOf(billId)) {
1462
+ const component = await walk.resolve(line.componentSkuId);
1463
+ const sub = walk.open(component, level, path);
1464
+ if (!sub || !component.activeBom) continue;
1465
+ const subBill = component.activeBom;
1466
+ let estimate = estimates.get(sub.bomId);
1467
+ if (!estimate) {
1468
+ estimate = this.labourFor(subBill);
1469
+ estimates.set(sub.bomId, estimate);
1470
+ }
1471
+ const unitsPerUnit = units * line.effectiveQtyPerUnit();
1472
+ subAssemblies.push({
1473
+ lineId: line.id,
1474
+ componentSkuId: line.componentSkuId,
1475
+ name: sub.name,
1476
+ level,
1477
+ path,
1478
+ unitsPerUnit,
1479
+ estimate: await estimate
1480
+ });
1481
+ await visit(sub.bomId, level + 1, [...path, sub], unitsPerUnit);
1482
+ }
1483
+ };
1484
+ await visit(bomId, 1, [await walk.root()], 1);
1485
+ let totalMinutes = own.totalMinutes;
1486
+ let totalCost = own.totalCost;
1487
+ let hasMissingRates = own.hasMissingRates;
1488
+ for (const sub of subAssemblies) {
1489
+ totalMinutes += sub.unitsPerUnit * sub.estimate.totalMinutes;
1490
+ if (sub.estimate.currency === own.currency) {
1491
+ totalCost += sub.unitsPerUnit * sub.estimate.totalCost;
1492
+ if (sub.estimate.hasMissingRates) hasMissingRates = true;
1493
+ } else if (sub.estimate.hasRouting) hasMissingRates = true;
1494
+ }
1495
+ return {
1496
+ ...own,
1497
+ hasRouting: own.hasRouting || subAssemblies.some((sub) => sub.estimate.hasRouting),
1498
+ totalMinutes,
1499
+ totalCost,
1500
+ hasMissingRates,
1501
+ subAssemblies
1502
+ };
1503
+ }
1504
+ /** Run the {@link OperationRateResolver}; `null` when there is no rate. */
1505
+ async resolveRate(operation) {
1506
+ if (!this.rateResolver) return null;
1507
+ const value = await this.rateResolver(operation);
1508
+ if (value === null || value === void 0 || !Number.isFinite(value)) return null;
1509
+ return Number(value);
1510
+ }
1511
+ /**
351
1512
  * Run the supplied {@link ComponentCostResolver} for the given component
352
1513
  * SKU. Returns `null` when no resolver is registered or when the
353
1514
  * resolver returns `null` / `undefined`.
@@ -360,6 +1521,40 @@ var BomService = class BomService {
360
1521
  return Number(value);
361
1522
  }
362
1523
  };
1524
+ function explodedLine(line, component, level, path, parentQty) {
1525
+ const effectiveQtyPerUnit = line.effectiveQtyPerUnit();
1526
+ return {
1527
+ lineId: line.id,
1528
+ bomId: line.bomId,
1529
+ level,
1530
+ path,
1531
+ componentSkuId: line.componentSkuId,
1532
+ kind: component.kind,
1533
+ name: component.product?.name ?? "",
1534
+ skuCode: component.sku?.code ?? "",
1535
+ qtyPerUnit: Number(line.qtyPerUnit ?? 0),
1536
+ wastePercent: Number(line.wastePercent ?? 0),
1537
+ effectiveQtyPerUnit,
1538
+ uom: line.uom,
1539
+ totalQty: parentQty * effectiveQtyPerUnit,
1540
+ buildable: component.buildable,
1541
+ subBomId: component.activeBom?.id ?? null,
1542
+ expanded: false
1543
+ };
1544
+ }
1545
+ function sumRequirements(lines) {
1546
+ const totals = /* @__PURE__ */ new Map();
1547
+ for (const line of lines) {
1548
+ const existing = totals.get(line.componentSkuId);
1549
+ if (existing) existing.totalQty += line.totalQty;
1550
+ else totals.set(line.componentSkuId, {
1551
+ componentSkuId: line.componentSkuId,
1552
+ totalQty: line.totalQty,
1553
+ uom: line.uom
1554
+ });
1555
+ }
1556
+ return Array.from(totals.values());
1557
+ }
363
1558
  function assertPositiveQty$1(qty, op) {
364
1559
  if (!Number.isFinite(qty) || qty <= 0) throw new Error(`${op}: qty must be a positive finite number (got ${qty})`);
365
1560
  }
@@ -478,13 +1673,14 @@ var ProductionService = class ProductionService {
478
1673
  const orderId = order.id ?? "";
479
1674
  if (!orderId) throw new Error("consumeMaterials: order.id is required for source attribution");
480
1675
  const bom = await this.resolveBom(order);
1676
+ const owner = bom.tenantId ?? null;
481
1677
  return {
482
1678
  orderId,
483
- lines: await this.lines.findByBom(bom.id),
1679
+ lines: (await readOwnAndGlobal(owner, () => this.lines.findByBom(bom.id))).filter((line) => isOwnOrGlobal(line, owner)),
484
1680
  runQty: options.qty,
485
1681
  locationId: options.locationId,
486
1682
  mutationOptions: {
487
- sourceType: "ProductionOrder",
1683
+ sourceType: options.sourceType ?? "ProductionOrder",
488
1684
  sourceId: orderId,
489
1685
  reasonCode: options.reasonCode ?? "production_consume",
490
1686
  note: options.note
@@ -548,8 +1744,13 @@ var ProductionService = class ProductionService {
548
1744
  */
549
1745
  async resolveBom(order) {
550
1746
  if (order.bomId) {
551
- const bom = await this.boms.get(order.bomId);
552
- if (!bom) throw new BomNotFoundError(order.bomId);
1747
+ const bomId = order.bomId;
1748
+ const tenantId = getCurrentTenant()?.tenantId;
1749
+ const bom = tenantId ? (await readOwnAndGlobal(tenantId, () => this.boms.list({
1750
+ where: { id: bomId },
1751
+ limit: 1
1752
+ }))).find((row) => isOwnOrGlobal(row, tenantId)) ?? null : await this.boms.get(bomId);
1753
+ if (!bom) throw new BomNotFoundError(bomId);
553
1754
  return bom;
554
1755
  }
555
1756
  const productId = order.productId ?? "";
@@ -571,7 +1772,7 @@ function prepareProduce(order, options) {
571
1772
  qty: options.qty,
572
1773
  locationId: options.locationId,
573
1774
  mutationOptions: {
574
- sourceType: "ProductionOrder",
1775
+ sourceType: options.sourceType ?? "ProductionOrder",
575
1776
  sourceId: orderId,
576
1777
  reasonCode: options.reasonCode ?? "production_produce",
577
1778
  note: options.note
@@ -693,6 +1894,423 @@ async function buildProductionService(options) {
693
1894
  return createProductionService({ db: options.db });
694
1895
  }
695
1896
  //#endregion
696
- export { BillOfMaterials, BillOfMaterialsCollection, BomLine, BomLineCollection, BomNotFoundError, BomService, NoActiveBomForProductError, ProductionService, createBomService, createProductionService, installManufacturingDispatchHandlers };
1897
+ //#region src/services/OperationService.ts
1898
+ var OperationService = class OperationService {
1899
+ constructor(operations) {
1900
+ this.operations = operations;
1901
+ }
1902
+ operations;
1903
+ /** Factory — prefer {@link createOperationService}. */
1904
+ static async create(options) {
1905
+ return new OperationService(await OperationCollection.create({ db: options.db }));
1906
+ }
1907
+ /**
1908
+ * Add an operation. The code is trimmed and must be unique per tenant;
1909
+ * throws {@link DuplicateOperationCodeError} when it is taken (retired
1910
+ * operations keep their code).
1911
+ */
1912
+ async define(input) {
1913
+ const code = input.code?.trim() ?? "";
1914
+ const name = input.name?.trim() ?? "";
1915
+ if (!code) throw new InvalidOperationInputError("Operation code is required.");
1916
+ if (!name) throw new InvalidOperationInputError("Operation name is required.");
1917
+ if (await this.operations.findByCode(code)) throw new DuplicateOperationCodeError(code);
1918
+ try {
1919
+ return await this.operations.create({
1920
+ code,
1921
+ name,
1922
+ category: input.category?.trim() ?? "",
1923
+ requiredQualificationId: input.requiredQualificationId?.trim() || null,
1924
+ isActive: true,
1925
+ _insertOnly: true
1926
+ });
1927
+ } catch (error) {
1928
+ if (await this.operations.findByCode(code)) throw new DuplicateOperationCodeError(code);
1929
+ throw error;
1930
+ }
1931
+ }
1932
+ /** Fetch an operation or throw {@link OperationNotFoundError}. */
1933
+ async get(id) {
1934
+ const operation = id ? await this.operations.get(id) : null;
1935
+ if (!operation) throw new OperationNotFoundError(id);
1936
+ return operation;
1937
+ }
1938
+ /** List operations; retired ones only with `includeRetired`. */
1939
+ async list(options = {}) {
1940
+ return this.operations.listOperations(options);
1941
+ }
1942
+ /** Change the display name. The code does not change. */
1943
+ async rename(id, name) {
1944
+ const next = name?.trim() ?? "";
1945
+ if (!next) throw new InvalidOperationInputError("Operation name is required.");
1946
+ const operation = await this.get(id);
1947
+ operation.name = next;
1948
+ await operation.save();
1949
+ return operation;
1950
+ }
1951
+ /** Change category and/or required qualification; an empty string clears. */
1952
+ async update(id, changes) {
1953
+ const operation = await this.get(id);
1954
+ if (changes.category !== void 0) operation.category = changes.category.trim();
1955
+ if (changes.requiredQualificationId !== void 0) operation.requiredQualificationId = changes.requiredQualificationId.trim() || null;
1956
+ await operation.save();
1957
+ return operation;
1958
+ }
1959
+ /** Retire: stays on history and routings, leaves pickers. Idempotent. */
1960
+ async retire(id) {
1961
+ return this.setActive(id, false);
1962
+ }
1963
+ /** Reinstate a retired operation. Idempotent. */
1964
+ async reinstate(id) {
1965
+ return this.setActive(id, true);
1966
+ }
1967
+ async setActive(id, isActive) {
1968
+ const operation = await this.get(id);
1969
+ if (operation.isActive !== isActive) {
1970
+ operation.isActive = isActive;
1971
+ await operation.save();
1972
+ }
1973
+ return operation;
1974
+ }
1975
+ };
1976
+ async function createOperationService(options) {
1977
+ return OperationService.create(options);
1978
+ }
1979
+ //#endregion
1980
+ //#region src/services/ProductionRunService.ts
1981
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1982
+ var OPEN_STATUSES = /* @__PURE__ */ new Set(["planned", "in_progress"]);
1983
+ var ProductionRunService = class ProductionRunService {
1984
+ constructor(stockService, runs, completions, boms) {
1985
+ this.stockService = stockService;
1986
+ this.runs = runs;
1987
+ this.completions = completions;
1988
+ this.boms = boms;
1989
+ }
1990
+ stockService;
1991
+ runs;
1992
+ completions;
1993
+ boms;
1994
+ /** Factory — prefer {@link createProductionRunService}. */
1995
+ static async create(options) {
1996
+ let stockService = options.stockService;
1997
+ if (!stockService) {
1998
+ if (!options.db) throw new Error("ProductionRunService.create: either `db` or `stockService` is required");
1999
+ stockService = await createStockService({ db: await resolveDatabase(options.db) });
2000
+ }
2001
+ const db = stockService.db;
2002
+ if (typeof db.transaction !== "function") throw new Error("ProductionRunService requires a database adapter with transaction() support.");
2003
+ const [runs, completions, boms] = await Promise.all([
2004
+ ProductionRunCollection.create({ db }),
2005
+ ProductionRunCompletionCollection.create({ db }),
2006
+ BillOfMaterialsCollection.create({ db })
2007
+ ]);
2008
+ return new ProductionRunService(stockService, runs, completions, boms);
2009
+ }
2010
+ /**
2011
+ * Create a `planned` run of `targetQty` units, pinned to `bomId`, or to the
2012
+ * active bill of `productId`.
2013
+ *
2014
+ * @throws {InvalidProductionRunInputError} for a target that is not a
2015
+ * positive number, or when neither `bomId` nor `productId` is given.
2016
+ * @throws {BomNotFoundError} when `bomId` does not exist.
2017
+ * @throws {NoActiveBomForProductError} when `productId` has no active bill.
2018
+ */
2019
+ async createRun(input) {
2020
+ const targetQty = positive(input.targetQty, "targetQty");
2021
+ const tenantId = getCurrentTenant()?.tenantId ?? null;
2022
+ let bom = null;
2023
+ if (input.bomId) {
2024
+ const bomId = input.bomId;
2025
+ bom = (UUID_PATTERN.test(bomId) ? await readOwnAndGlobal(tenantId, () => this.boms.list({
2026
+ where: { id: bomId },
2027
+ limit: 1
2028
+ })) : []).find((row) => isOwnOrGlobal(row, tenantId)) ?? null;
2029
+ if (!bom?.id) throw new BomNotFoundError(bomId);
2030
+ } else if (input.productId) {
2031
+ const productId = input.productId;
2032
+ const active = (await readOwnAndGlobal(tenantId, () => this.boms.list({
2033
+ where: {
2034
+ productId,
2035
+ status: "active"
2036
+ },
2037
+ orderBy: "version DESC"
2038
+ }))).filter((candidate) => isOwnOrGlobal(candidate, tenantId));
2039
+ bom = active.find((candidate) => (candidate.tenantId ?? null) === tenantId) ?? active[0] ?? null;
2040
+ if (!bom?.id) throw new NoActiveBomForProductError(input.productId);
2041
+ } else throw new InvalidProductionRunInputError("A production run needs a bomId or a productId.");
2042
+ return this.runs.create({
2043
+ tenantId: bom.tenantId ?? tenantId,
2044
+ bomId: bom.id,
2045
+ targetQty,
2046
+ completedQty: 0,
2047
+ status: "planned"
2048
+ });
2049
+ }
2050
+ /** The run, or `null` when no run has this id. */
2051
+ async get(runId) {
2052
+ if (!UUID_PATTERN.test(runId)) return null;
2053
+ return this.runs.get({ id: runId });
2054
+ }
2055
+ /** A run's completions in the order they happened. */
2056
+ async listCompletions(runId) {
2057
+ if (!UUID_PATTERN.test(runId)) return [];
2058
+ return this.completions.findByRun(runId);
2059
+ }
2060
+ /** Mark a `planned` run `in_progress`. */
2061
+ async start(runId) {
2062
+ return this.change(runId, "start", (run) => {
2063
+ if (run.status !== "planned") throw new ProductionRunStateError(runId, run.status, "start");
2064
+ run.status = "in_progress";
2065
+ });
2066
+ }
2067
+ /** Mark an open run `done`, short of its target if work stopped early. */
2068
+ async finish(runId) {
2069
+ return this.change(runId, "finish", (run) => {
2070
+ run.status = "done";
2071
+ });
2072
+ }
2073
+ /** Cancel an open run. Completions already reported stay. */
2074
+ async cancel(runId) {
2075
+ return this.change(runId, "cancel", (run) => {
2076
+ run.status = "cancelled";
2077
+ });
2078
+ }
2079
+ /**
2080
+ * Change an open run's target. It may not go below what is already done;
2081
+ * setting it to exactly that finishes the run.
2082
+ *
2083
+ * @throws {InvalidProductionRunInputError} for a target that is not a
2084
+ * positive number or is below the completed quantity.
2085
+ */
2086
+ async setTarget(runId, targetQty) {
2087
+ const target = positive(targetQty, "targetQty");
2088
+ return this.change(runId, "retarget", (run) => {
2089
+ const completed = toMillionths(Number(run.completedQty));
2090
+ const goal = toMillionths(target);
2091
+ if (goal < completed) throw new InvalidProductionRunInputError(`The target cannot be below the ${fromMillionths(completed)} already done.`);
2092
+ run.targetQty = target;
2093
+ if (completed > 0 && completed >= goal) run.status = "done";
2094
+ });
2095
+ }
2096
+ /**
2097
+ * Report `qty` finished units on an open run: records a dated completion,
2098
+ * adds it to `completedQty`, moves a `planned` run to `in_progress`, and
2099
+ * marks the run `done` when the target is reached. With `consume` and/or
2100
+ * `produce`, also writes the stock movements through
2101
+ * {@link ProductionService}, stamped `sourceType: 'ProductionRunCompletion'`
2102
+ * with the completion id. Everything runs in one transaction.
2103
+ *
2104
+ * Consumption takes the run's bill's own lines for `qty` units: a
2105
+ * sub-assembly on the bill is taken from stock as a component. Building
2106
+ * that sub-assembly is its own run.
2107
+ *
2108
+ * @throws {ProductionRunNotFoundError}
2109
+ * @throws {ProductionRunStateError} when the run is `done` or `cancelled`.
2110
+ * @throws {ProductionRunOverCompletionError} when `qty` is more than the
2111
+ * run has left.
2112
+ * @throws {InvalidProductionRunInputError} for a bad `qty` or date.
2113
+ * @throws `InsufficientStockError` from inventory when consuming would
2114
+ * drive stock below zero; nothing is recorded then.
2115
+ */
2116
+ async recordCompletion(runId, input) {
2117
+ const qty = positive(input.qty, "qty");
2118
+ const completedAt = input.completedAt === void 0 ? /* @__PURE__ */ new Date() : new Date(input.completedAt);
2119
+ if (Number.isNaN(completedAt.getTime())) throw new InvalidProductionRunInputError("completedAt is not a valid date.");
2120
+ return this.inTransaction(async (tx) => {
2121
+ const run = await this.load(tx, runId);
2122
+ if (!OPEN_STATUSES.has(run.status)) throw new ProductionRunStateError(runId, run.status, "complete");
2123
+ const target = toMillionths(Number(run.targetQty));
2124
+ const done = toMillionths(Number(run.completedQty));
2125
+ const reported = toMillionths(qty);
2126
+ const remaining = Math.max(0, target - done);
2127
+ if (reported > remaining) throw new ProductionRunOverCompletionError(runId, qty, fromMillionths(remaining));
2128
+ const total = done + reported;
2129
+ run.completedQty = fromMillionths(total);
2130
+ run.status = total >= target ? "done" : "in_progress";
2131
+ await run.save();
2132
+ const completion = await tx.completions.create({
2133
+ tenantId: run.tenantId ?? null,
2134
+ runId,
2135
+ qty,
2136
+ completedAt
2137
+ });
2138
+ let consumed = [];
2139
+ let produced = null;
2140
+ const inRunTenant = (work) => run.tenantId ? withTenant({ tenantId: run.tenantId }, work) : work();
2141
+ if (input.consume || input.produce) {
2142
+ const production = await ProductionService.create({ stockService: tx.stock });
2143
+ const ref = {
2144
+ id: completion.id,
2145
+ bomId: run.bomId
2146
+ };
2147
+ const sourceType = "ProductionRunCompletion";
2148
+ const consume = input.consume && {
2149
+ ...input.consume,
2150
+ qty,
2151
+ sourceType
2152
+ };
2153
+ const produce = input.produce && {
2154
+ ...input.produce,
2155
+ qty,
2156
+ sourceType
2157
+ };
2158
+ if (consume && produce) ({consumed, produced} = await inRunTenant(() => production.runProduction(ref, {
2159
+ consume,
2160
+ produce
2161
+ })));
2162
+ else if (consume) consumed = await inRunTenant(() => production.consumeMaterials(ref, consume));
2163
+ else if (produce) produced = await inRunTenant(() => production.produceFinishedGoods(ref, produce));
2164
+ }
2165
+ return {
2166
+ run,
2167
+ completion,
2168
+ consumed,
2169
+ produced
2170
+ };
2171
+ });
2172
+ }
2173
+ /** Apply `apply` to an open run and save it, in one transaction. */
2174
+ change(runId, action, apply) {
2175
+ return this.inTransaction(async (tx) => {
2176
+ const run = await this.load(tx, runId);
2177
+ if (!OPEN_STATUSES.has(run.status)) throw new ProductionRunStateError(runId, run.status, action);
2178
+ apply(run);
2179
+ await run.save();
2180
+ return run;
2181
+ });
2182
+ }
2183
+ async load(tx, runId) {
2184
+ const run = UUID_PATTERN.test(runId) ? await tx.runs.get({ id: runId }) : null;
2185
+ if (!run) throw new ProductionRunNotFoundError(runId);
2186
+ return run;
2187
+ }
2188
+ /**
2189
+ * Run `work` in one transaction on collections bound to it, rerunning the
2190
+ * whole transaction when it lost a revision race on the run row.
2191
+ */
2192
+ inTransaction(work) {
2193
+ return withRevisionRetry$1(() => this.stockService.withTransaction(async (stock) => {
2194
+ const db = stock.db;
2195
+ const [runs, completions] = await Promise.all([ProductionRunCollection.create({ db }), ProductionRunCompletionCollection.create({ db })]);
2196
+ return work({
2197
+ stock,
2198
+ runs,
2199
+ completions
2200
+ });
2201
+ }));
2202
+ }
2203
+ };
2204
+ var MAX_REVISION_ATTEMPTS = 10;
2205
+ async function withRevisionRetry$1(run) {
2206
+ for (let attempt = 1;; attempt++) try {
2207
+ return await run();
2208
+ } catch (error) {
2209
+ if (error?.code !== "RUNTIME_REVISION_CONFLICT" || attempt >= MAX_REVISION_ATTEMPTS) throw error;
2210
+ }
2211
+ }
2212
+ function positive(value, field) {
2213
+ const raw = Number(value);
2214
+ const number = Number.isFinite(raw) ? roundQuantity(raw) : raw;
2215
+ if (!Number.isFinite(number) || number <= 0) throw new InvalidProductionRunInputError(`${field} must be a number greater than zero (got ${String(value)}).`);
2216
+ if (number > 999999999) throw new InvalidProductionRunInputError(`${field} must be at most ${MAX_QUANTITY} (got ${String(value)}).`);
2217
+ return number;
2218
+ }
2219
+ function createProductionRunService(options) {
2220
+ return ProductionRunService.create(options);
2221
+ }
2222
+ //#endregion
2223
+ //#region src/services/RoutingService.ts
2224
+ var RoutingService = class RoutingService {
2225
+ constructor(db, boms, operations, steps) {
2226
+ this.db = db;
2227
+ this.boms = boms;
2228
+ this.operations = operations;
2229
+ this.steps = steps;
2230
+ }
2231
+ db;
2232
+ boms;
2233
+ operations;
2234
+ steps;
2235
+ /** Factory — prefer {@link createRoutingService}. */
2236
+ static async create(options) {
2237
+ const db = await resolveDatabase(options.db);
2238
+ const [boms, operations, steps] = await Promise.all([
2239
+ BillOfMaterialsCollection.create({ db }),
2240
+ OperationCollection.create({ db }),
2241
+ RoutingStepCollection.create({ db })
2242
+ ]);
2243
+ return new RoutingService(db, boms, operations, steps);
2244
+ }
2245
+ /** The routing of a bill in step order; empty when it has none. */
2246
+ async list(bomId) {
2247
+ return this.steps.findByBom(bomId);
2248
+ }
2249
+ /**
2250
+ * Check `inputs` against the routing about to be replaced: durations,
2251
+ * operations, and that a retired operation is not added beyond the
2252
+ * occurrences `current` already has.
2253
+ */
2254
+ async validate(current, inputs) {
2255
+ const retained = /* @__PURE__ */ new Map();
2256
+ for (const step of current) retained.set(step.operationId, (retained.get(step.operationId) ?? 0) + 1);
2257
+ for (const input of inputs) {
2258
+ const minutes = Number(input.estimatedMinutes);
2259
+ if (!Number.isFinite(minutes) || minutes < 0) throw new InvalidOperationInputError("Estimated minutes must be zero or more.");
2260
+ const operation = input.operationId ? await this.operations.get(input.operationId) : null;
2261
+ if (!operation) throw new OperationNotFoundError(input.operationId);
2262
+ const left = retained.get(operation.id) ?? 0;
2263
+ if (left > 0) retained.set(operation.id, left - 1);
2264
+ else if (!operation.isActive) throw new OperationRetiredError(operation.id);
2265
+ }
2266
+ }
2267
+ /**
2268
+ * Replace the bill's routing with `inputs`, numbered 1..n in the order given.
2269
+ * An empty list removes the routing.
2270
+ *
2271
+ * Throws {@link BomNotFoundError}, {@link OperationNotFoundError},
2272
+ * {@link OperationRetiredError} (a retired operation may stay on a routing
2273
+ * that already has it but cannot be newly added), or
2274
+ * {@link InvalidOperationInputError} for a negative or non-finite duration.
2275
+ */
2276
+ async replaceRouting(bomId, inputs) {
2277
+ if (!bomId || !await this.boms.get(bomId)) throw new BomNotFoundError(bomId);
2278
+ const write = async (steps, boms) => {
2279
+ const bom = await boms.get(bomId);
2280
+ if (!bom) throw new BomNotFoundError(bomId);
2281
+ await bom.save();
2282
+ const current = await steps.findByBom(bomId);
2283
+ await this.validate(current, inputs);
2284
+ for (const step of current) await step.delete();
2285
+ const saved = [];
2286
+ for (const [index, input] of inputs.entries()) saved.push(await steps.create({
2287
+ bomId,
2288
+ operationId: input.operationId,
2289
+ sequence: index + 1,
2290
+ estimatedMinutes: Number(input.estimatedMinutes),
2291
+ notes: input.notes?.trim() ?? ""
2292
+ }));
2293
+ return saved;
2294
+ };
2295
+ const underlying = this.db;
2296
+ if (typeof underlying.transaction !== "function") throw new Error("RoutingService requires a database adapter with transaction() support.");
2297
+ return withRevisionRetry(() => underlying.transaction?.(async (txDb) => {
2298
+ const db = txDb;
2299
+ return write(await RoutingStepCollection.create({ db }), await BillOfMaterialsCollection.create({ db }));
2300
+ }));
2301
+ }
2302
+ };
2303
+ async function withRevisionRetry(run) {
2304
+ for (let attempt = 1;; attempt++) try {
2305
+ return await run();
2306
+ } catch (error) {
2307
+ if (error?.code !== "RUNTIME_REVISION_CONFLICT" || attempt >= 5) throw error;
2308
+ }
2309
+ }
2310
+ async function createRoutingService(options) {
2311
+ return RoutingService.create(options);
2312
+ }
2313
+ //#endregion
2314
+ export { Assembly, AssemblyCollection, AssemblyService, BillOfMaterials, BillOfMaterialsCollection, BomCycleError, BomExplosionLimitError, BomLine, BomLineCollection, BomNotFoundError, BomService, BomStructureCycleError, DuplicateOperationCodeError, InvalidOperationInputError, InvalidProductionRunInputError, MAX_EXPLOSION_DEPTH, MAX_EXPLOSION_LINES, MAX_QUANTITY, NoActiveBomForProductError, Operation, OperationCollection, OperationNotFoundError, OperationRetiredError, OperationService, PRODUCTION_RUN_STATUSES, ProductionRun, ProductionRunCollection, ProductionRunCompletion, ProductionRunCompletionCollection, ProductionRunNotFoundError, ProductionRunOverCompletionError, ProductionRunService, ProductionRunStateError, ProductionService, QUANTITY_DECIMALS, RoutingService, RoutingStep, RoutingStepCollection, createAssemblyService, createBomService, createOperationService, createProductionRunService, createProductionService, createRoutingService, installManufacturingDispatchHandlers, roundQuantity };
697
2315
 
698
2316
  //# sourceMappingURL=index.js.map