@open-mercato/core 0.6.8-develop.7031.1.005201cd70 → 0.6.8-develop.7037.1.ea0277b01e

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 4325 entry points
1
+ [build:core] found 4326 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 218 entry points
4
4
  [build:core:generated] built successfully
@@ -1,6 +1,6 @@
1
1
  import { testLinearRegex } from "@open-mercato/shared/lib/regex/linear";
2
2
  import { getNestedValue, resolveSpecialValue } from "./value-resolver.js";
3
- import { createLogger } from "@open-mercato/shared/lib/logger";
3
+ import { createLogger, isLevelEnabled } from "@open-mercato/shared/lib/logger";
4
4
  const logger = createLogger("business_rules").child({ component: "expression-evaluator" });
5
5
  function evaluateExpression(expression, data, context) {
6
6
  if (isGroupCondition(expression)) {
@@ -20,14 +20,14 @@ function evaluateSimpleCondition(condition, data, context) {
20
20
  rightValue = resolveStaticOrSpecialValue(condition.value, context);
21
21
  }
22
22
  const result = applyOperator(leftValue, condition.operator, rightValue);
23
- logger.debug("Simple condition evaluated", {
24
- field: condition.field,
25
- operator: condition.operator,
26
- expectedValue: rightValue,
27
- actualValue: leftValue,
28
- actualValueType: typeof leftValue,
29
- passed: result
30
- });
23
+ if (isLevelEnabled("debug")) {
24
+ logger.debug("Simple condition evaluated", {
25
+ field: condition.field,
26
+ operator: condition.operator,
27
+ actualValueType: typeof leftValue,
28
+ passed: result
29
+ });
30
+ }
31
31
  return result;
32
32
  }
33
33
  function evaluateGroupCondition(condition, data, context) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/business_rules/lib/expression-evaluator.ts"],
4
- "sourcesContent": ["import type { ComparisonOperator, LogicalOperator } from '../data/validators'\nimport { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'\nimport { getNestedValue, resolveSpecialValue } from './value-resolver'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('business_rules').child({ component: 'expression-evaluator' })\n\n/**\n * Type definitions for condition expressions\n */\nexport type ConditionExpression = SimpleCondition | GroupCondition\n\nexport interface SimpleCondition {\n field: string\n operator: ComparisonOperator\n value: any\n valueField?: string // For field-to-field comparison\n}\n\nexport interface GroupCondition {\n operator: LogicalOperator // 'AND' | 'OR' | 'NOT'\n rules: ConditionExpression[]\n}\n\nexport interface EvaluationContext {\n user?: {\n id?: string\n email?: string\n role?: string\n [key: string]: any\n }\n tenant?: {\n id?: string\n [key: string]: any\n }\n organization?: {\n id?: string\n [key: string]: any\n }\n now?: Date\n today?: Date\n [key: string]: any\n}\n\n/**\n * Evaluate a condition expression (simple or grouped)\n */\nexport function evaluateExpression(\n expression: ConditionExpression,\n data: any,\n context: EvaluationContext\n): boolean {\n if (isGroupCondition(expression)) {\n return evaluateGroupCondition(expression, data, context)\n }\n\n return evaluateSimpleCondition(expression, data, context)\n}\n\n/**\n * Check if expression is a group condition\n */\nfunction isGroupCondition(expression: ConditionExpression): expression is GroupCondition {\n return 'rules' in expression && Array.isArray(expression.rules)\n}\n\n/**\n * Evaluate a simple condition\n */\nfunction evaluateSimpleCondition(\n condition: SimpleCondition,\n data: any,\n context: EvaluationContext\n): boolean {\n // Left side: always resolve from data (field path)\n const leftValue = resolveFieldPath(condition.field, data)\n\n let rightValue: any\n if (condition.valueField) {\n // Field-to-field comparison: resolve from data\n rightValue = resolveFieldPath(condition.valueField, data)\n } else {\n // Static value or special value\n rightValue = resolveStaticOrSpecialValue(condition.value, context)\n }\n\n const result = applyOperator(leftValue, condition.operator, rightValue)\n\n logger.debug('Simple condition evaluated', {\n field: condition.field,\n operator: condition.operator,\n expectedValue: rightValue,\n actualValue: leftValue,\n actualValueType: typeof leftValue,\n passed: result,\n })\n\n return result\n}\n\n/**\n * Evaluate a group condition (AND, OR, NOT)\n */\nfunction evaluateGroupCondition(\n condition: GroupCondition,\n data: any,\n context: EvaluationContext\n): boolean {\n const { operator, rules } = condition\n\n if (!rules || rules.length === 0) {\n return true\n }\n\n logger.debug('Group condition evaluating', { operator, ruleCount: rules.length })\n\n let result: boolean\n\n switch (operator) {\n case 'AND':\n result = rules.every((rule) => evaluateExpression(rule, data, context))\n break\n\n case 'OR':\n result = rules.some((rule) => evaluateExpression(rule, data, context))\n break\n\n case 'NOT':\n // NOT operator - negate the first rule (or all rules combined with AND)\n if (rules.length === 1) {\n result = !evaluateExpression(rules[0], data, context)\n } else {\n // Multiple rules - combine with AND then negate\n result = !rules.every((rule) => evaluateExpression(rule, data, context))\n }\n break\n\n default:\n throw new Error(`Unknown logical operator: ${operator}`)\n }\n\n logger.debug('Group condition evaluated', { operator, passed: result })\n\n return result\n}\n\n/**\n * Resolve a field path from data\n */\nfunction resolveFieldPath(path: string, data: any): any {\n return getNestedValue(data, path)\n}\n\n/**\n * Resolve a static value or special template value\n */\nfunction resolveStaticOrSpecialValue(value: any, context: EvaluationContext): any {\n // If not a string, return as-is (already a value)\n if (typeof value !== 'string') {\n return value\n }\n\n // Check for special values (template variables)\n if (value.startsWith('{{') && value.endsWith('}}')) {\n return resolveSpecialValue(value, context)\n }\n\n // Return static string value as-is\n return value\n}\n\n/**\n * Apply a comparison operator\n */\nfunction applyOperator(left: any, operator: ComparisonOperator, right: any): boolean {\n switch (operator) {\n case '=':\n case '==':\n return equals(left, right)\n\n case '!=':\n return !equals(left, right)\n\n case '>':\n return compare(left, right) > 0\n\n case '>=':\n return compare(left, right) >= 0\n\n case '<':\n return compare(left, right) < 0\n\n case '<=':\n return compare(left, right) <= 0\n\n case 'IN':\n return isIn(left, right)\n\n case 'NOT_IN':\n return !isIn(left, right)\n\n case 'CONTAINS':\n return contains(left, right)\n\n case 'NOT_CONTAINS':\n return !contains(left, right)\n\n case 'STARTS_WITH':\n return startsWith(left, right)\n\n case 'ENDS_WITH':\n return endsWith(left, right)\n\n case 'MATCHES':\n return matches(left, right)\n\n case 'IS_EMPTY':\n return isEmpty(left)\n\n case 'IS_NOT_EMPTY':\n return !isEmpty(left)\n\n default:\n throw new Error(`Unknown operator: ${operator}`)\n }\n}\n\n/**\n * Equality comparison (handles different types)\n */\nfunction equals(left: any, right: any): boolean {\n // Handle null/undefined\n if (left == null && right == null) return true\n if (left == null || right == null) return false\n\n // Type coercion for numbers\n if (typeof left === 'number' || typeof right === 'number') {\n return Number(left) === Number(right)\n }\n\n // String comparison (case-sensitive)\n return left === right\n}\n\n/**\n * Comparison for ordering (>, <, >=, <=)\n */\nfunction compare(left: any, right: any): number {\n // Handle null/undefined\n if (left == null && right == null) return 0\n if (left == null) return -1\n if (right == null) return 1\n\n // Date comparison\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime()\n }\n\n // String date comparison\n if (isDateString(left) && isDateString(right)) {\n return new Date(left).getTime() - new Date(right).getTime()\n }\n\n // Numeric comparison\n const leftNum = Number(left)\n const rightNum = Number(right)\n if (!isNaN(leftNum) && !isNaN(rightNum)) {\n return leftNum - rightNum\n }\n\n // String comparison\n const leftStr = String(left)\n const rightStr = String(right)\n return leftStr.localeCompare(rightStr)\n}\n\n/**\n * Check if value is in array\n */\nfunction isIn(value: any, array: any): boolean {\n if (!Array.isArray(array)) {\n return false\n }\n\n return array.some((item) => equals(value, item))\n}\n\n/**\n * Check if array/string contains value\n */\nfunction contains(container: any, value: any): boolean {\n if (container == null) return false\n\n // Array contains\n if (Array.isArray(container)) {\n return container.some((item) => equals(item, value))\n }\n\n // String contains\n if (typeof container === 'string') {\n return container.includes(String(value))\n }\n\n return false\n}\n\n/**\n * Check if string starts with value\n */\nfunction startsWith(str: any, prefix: any): boolean {\n if (str == null || prefix == null) return false\n\n return String(str).startsWith(String(prefix))\n}\n\n/**\n * Check if string ends with value\n */\nfunction endsWith(str: any, suffix: any): boolean {\n if (str == null || suffix == null) return false\n\n return String(str).endsWith(String(suffix))\n}\n\nconst MAX_REGEX_LENGTH = 200\nconst MAX_REGEX_INPUT_LENGTH = 10_000\n\n/**\n * Check if string matches regex pattern (with ReDoS protection)\n */\nfunction matches(str: any, pattern: any): boolean {\n if (str == null || pattern == null) return false\n\n try {\n const patternStr = String(pattern)\n\n // Prevent overly long patterns\n if (patternStr.length > MAX_REGEX_LENGTH) {\n return false\n }\n\n // Check for dangerous patterns that can cause exponential backtracking\n // Patterns like (a+)+, (a*)*, (a+)*, etc.\n if (/(\\(.*[+*]\\).*[+*])/.test(patternStr)) {\n return false\n }\n\n const testStr = String(str)\n if (testStr.length > MAX_REGEX_INPUT_LENGTH) {\n return false\n }\n\n const result = testLinearRegex(patternStr, testStr, {\n maxPatternLength: MAX_REGEX_LENGTH,\n maxInputLength: MAX_REGEX_INPUT_LENGTH,\n })\n return result.ok ? result.matched : false\n } catch (error) {\n // Log the error for debugging but don't expose to user\n const message = error instanceof Error ? error.message : 'Unknown error'\n if (process.env.NODE_ENV !== 'test') {\n logger.error('Regex matching failed', { message })\n }\n return false\n }\n}\n\n/**\n * Check if value is empty (null, undefined, empty string, empty array, empty object)\n */\nfunction isEmpty(value: any): boolean {\n if (value == null) return true\n\n if (typeof value === 'string') return value.trim() === ''\n\n if (Array.isArray(value)) return value.length === 0\n\n if (typeof value === 'object') return Object.keys(value).length === 0\n\n return false\n}\n\n/**\n * Check if string looks like a date (ISO format or common date formats)\n */\nfunction isDateString(value: any): boolean {\n if (typeof value !== 'string') return false\n\n // ISO date format: YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss\n const isoDateRegex = /^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2})?/\n if (isoDateRegex.test(value)) {\n const date = new Date(value)\n return !isNaN(date.getTime())\n }\n\n return false\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ComparisonOperator, LogicalOperator } from '../data/validators'\nimport { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'\nimport { getNestedValue, resolveSpecialValue } from './value-resolver'\nimport { createLogger, isLevelEnabled } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('business_rules').child({ component: 'expression-evaluator' })\n\n/**\n * Type definitions for condition expressions\n */\nexport type ConditionExpression = SimpleCondition | GroupCondition\n\nexport interface SimpleCondition {\n field: string\n operator: ComparisonOperator\n value: any\n valueField?: string // For field-to-field comparison\n}\n\nexport interface GroupCondition {\n operator: LogicalOperator // 'AND' | 'OR' | 'NOT'\n rules: ConditionExpression[]\n}\n\nexport interface EvaluationContext {\n user?: {\n id?: string\n email?: string\n role?: string\n [key: string]: any\n }\n tenant?: {\n id?: string\n [key: string]: any\n }\n organization?: {\n id?: string\n [key: string]: any\n }\n now?: Date\n today?: Date\n [key: string]: any\n}\n\n/**\n * Evaluate a condition expression (simple or grouped)\n */\nexport function evaluateExpression(\n expression: ConditionExpression,\n data: any,\n context: EvaluationContext\n): boolean {\n if (isGroupCondition(expression)) {\n return evaluateGroupCondition(expression, data, context)\n }\n\n return evaluateSimpleCondition(expression, data, context)\n}\n\n/**\n * Check if expression is a group condition\n */\nfunction isGroupCondition(expression: ConditionExpression): expression is GroupCondition {\n return 'rules' in expression && Array.isArray(expression.rules)\n}\n\n/**\n * Evaluate a simple condition\n */\nfunction evaluateSimpleCondition(\n condition: SimpleCondition,\n data: any,\n context: EvaluationContext\n): boolean {\n // Left side: always resolve from data (field path)\n const leftValue = resolveFieldPath(condition.field, data)\n\n let rightValue: any\n if (condition.valueField) {\n // Field-to-field comparison: resolve from data\n rightValue = resolveFieldPath(condition.valueField, data)\n } else {\n // Static value or special value\n rightValue = resolveStaticOrSpecialValue(condition.value, context)\n }\n\n const result = applyOperator(leftValue, condition.operator, rightValue)\n\n if (isLevelEnabled('debug')) {\n logger.debug('Simple condition evaluated', {\n field: condition.field,\n operator: condition.operator,\n actualValueType: typeof leftValue,\n passed: result,\n })\n }\n\n return result\n}\n\n/**\n * Evaluate a group condition (AND, OR, NOT)\n */\nfunction evaluateGroupCondition(\n condition: GroupCondition,\n data: any,\n context: EvaluationContext\n): boolean {\n const { operator, rules } = condition\n\n if (!rules || rules.length === 0) {\n return true\n }\n\n logger.debug('Group condition evaluating', { operator, ruleCount: rules.length })\n\n let result: boolean\n\n switch (operator) {\n case 'AND':\n result = rules.every((rule) => evaluateExpression(rule, data, context))\n break\n\n case 'OR':\n result = rules.some((rule) => evaluateExpression(rule, data, context))\n break\n\n case 'NOT':\n // NOT operator - negate the first rule (or all rules combined with AND)\n if (rules.length === 1) {\n result = !evaluateExpression(rules[0], data, context)\n } else {\n // Multiple rules - combine with AND then negate\n result = !rules.every((rule) => evaluateExpression(rule, data, context))\n }\n break\n\n default:\n throw new Error(`Unknown logical operator: ${operator}`)\n }\n\n logger.debug('Group condition evaluated', { operator, passed: result })\n\n return result\n}\n\n/**\n * Resolve a field path from data\n */\nfunction resolveFieldPath(path: string, data: any): any {\n return getNestedValue(data, path)\n}\n\n/**\n * Resolve a static value or special template value\n */\nfunction resolveStaticOrSpecialValue(value: any, context: EvaluationContext): any {\n // If not a string, return as-is (already a value)\n if (typeof value !== 'string') {\n return value\n }\n\n // Check for special values (template variables)\n if (value.startsWith('{{') && value.endsWith('}}')) {\n return resolveSpecialValue(value, context)\n }\n\n // Return static string value as-is\n return value\n}\n\n/**\n * Apply a comparison operator\n */\nfunction applyOperator(left: any, operator: ComparisonOperator, right: any): boolean {\n switch (operator) {\n case '=':\n case '==':\n return equals(left, right)\n\n case '!=':\n return !equals(left, right)\n\n case '>':\n return compare(left, right) > 0\n\n case '>=':\n return compare(left, right) >= 0\n\n case '<':\n return compare(left, right) < 0\n\n case '<=':\n return compare(left, right) <= 0\n\n case 'IN':\n return isIn(left, right)\n\n case 'NOT_IN':\n return !isIn(left, right)\n\n case 'CONTAINS':\n return contains(left, right)\n\n case 'NOT_CONTAINS':\n return !contains(left, right)\n\n case 'STARTS_WITH':\n return startsWith(left, right)\n\n case 'ENDS_WITH':\n return endsWith(left, right)\n\n case 'MATCHES':\n return matches(left, right)\n\n case 'IS_EMPTY':\n return isEmpty(left)\n\n case 'IS_NOT_EMPTY':\n return !isEmpty(left)\n\n default:\n throw new Error(`Unknown operator: ${operator}`)\n }\n}\n\n/**\n * Equality comparison (handles different types)\n */\nfunction equals(left: any, right: any): boolean {\n // Handle null/undefined\n if (left == null && right == null) return true\n if (left == null || right == null) return false\n\n // Type coercion for numbers\n if (typeof left === 'number' || typeof right === 'number') {\n return Number(left) === Number(right)\n }\n\n // String comparison (case-sensitive)\n return left === right\n}\n\n/**\n * Comparison for ordering (>, <, >=, <=)\n */\nfunction compare(left: any, right: any): number {\n // Handle null/undefined\n if (left == null && right == null) return 0\n if (left == null) return -1\n if (right == null) return 1\n\n // Date comparison\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime()\n }\n\n // String date comparison\n if (isDateString(left) && isDateString(right)) {\n return new Date(left).getTime() - new Date(right).getTime()\n }\n\n // Numeric comparison\n const leftNum = Number(left)\n const rightNum = Number(right)\n if (!isNaN(leftNum) && !isNaN(rightNum)) {\n return leftNum - rightNum\n }\n\n // String comparison\n const leftStr = String(left)\n const rightStr = String(right)\n return leftStr.localeCompare(rightStr)\n}\n\n/**\n * Check if value is in array\n */\nfunction isIn(value: any, array: any): boolean {\n if (!Array.isArray(array)) {\n return false\n }\n\n return array.some((item) => equals(value, item))\n}\n\n/**\n * Check if array/string contains value\n */\nfunction contains(container: any, value: any): boolean {\n if (container == null) return false\n\n // Array contains\n if (Array.isArray(container)) {\n return container.some((item) => equals(item, value))\n }\n\n // String contains\n if (typeof container === 'string') {\n return container.includes(String(value))\n }\n\n return false\n}\n\n/**\n * Check if string starts with value\n */\nfunction startsWith(str: any, prefix: any): boolean {\n if (str == null || prefix == null) return false\n\n return String(str).startsWith(String(prefix))\n}\n\n/**\n * Check if string ends with value\n */\nfunction endsWith(str: any, suffix: any): boolean {\n if (str == null || suffix == null) return false\n\n return String(str).endsWith(String(suffix))\n}\n\nconst MAX_REGEX_LENGTH = 200\nconst MAX_REGEX_INPUT_LENGTH = 10_000\n\n/**\n * Check if string matches regex pattern (with ReDoS protection)\n */\nfunction matches(str: any, pattern: any): boolean {\n if (str == null || pattern == null) return false\n\n try {\n const patternStr = String(pattern)\n\n // Prevent overly long patterns\n if (patternStr.length > MAX_REGEX_LENGTH) {\n return false\n }\n\n // Check for dangerous patterns that can cause exponential backtracking\n // Patterns like (a+)+, (a*)*, (a+)*, etc.\n if (/(\\(.*[+*]\\).*[+*])/.test(patternStr)) {\n return false\n }\n\n const testStr = String(str)\n if (testStr.length > MAX_REGEX_INPUT_LENGTH) {\n return false\n }\n\n const result = testLinearRegex(patternStr, testStr, {\n maxPatternLength: MAX_REGEX_LENGTH,\n maxInputLength: MAX_REGEX_INPUT_LENGTH,\n })\n return result.ok ? result.matched : false\n } catch (error) {\n // Log the error for debugging but don't expose to user\n const message = error instanceof Error ? error.message : 'Unknown error'\n if (process.env.NODE_ENV !== 'test') {\n logger.error('Regex matching failed', { message })\n }\n return false\n }\n}\n\n/**\n * Check if value is empty (null, undefined, empty string, empty array, empty object)\n */\nfunction isEmpty(value: any): boolean {\n if (value == null) return true\n\n if (typeof value === 'string') return value.trim() === ''\n\n if (Array.isArray(value)) return value.length === 0\n\n if (typeof value === 'object') return Object.keys(value).length === 0\n\n return false\n}\n\n/**\n * Check if string looks like a date (ISO format or common date formats)\n */\nfunction isDateString(value: any): boolean {\n if (typeof value !== 'string') return false\n\n // ISO date format: YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss\n const isoDateRegex = /^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2})?/\n if (isoDateRegex.test(value)) {\n const date = new Date(value)\n return !isNaN(date.getTime())\n }\n\n return false\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import { evaluateExpression } from "./expression-evaluator.js";
2
- import { createLogger } from "@open-mercato/shared/lib/logger";
2
+ import { createLogger, isLevelEnabled } from "@open-mercato/shared/lib/logger";
3
3
  const logger = createLogger("business_rules").child({ component: "rule-evaluator" });
4
4
  async function evaluate(rules, data, context) {
5
5
  const startTime = Date.now();
@@ -41,14 +41,15 @@ async function evaluate(rules, data, context) {
41
41
  }
42
42
  async function evaluateSingleRule(rule, data, context) {
43
43
  const startTime = Date.now();
44
- logger.debug("Evaluating rule", {
45
- ruleId: rule.ruleId,
46
- ruleName: rule.ruleName,
47
- ruleType: rule.ruleType,
48
- entityType: rule.entityType,
49
- eventType: rule.eventType,
50
- conditions: rule.conditionExpression
51
- });
44
+ if (isLevelEnabled("debug")) {
45
+ logger.debug("Evaluating rule", {
46
+ ruleId: rule.ruleId,
47
+ ruleName: rule.ruleName,
48
+ ruleType: rule.ruleType,
49
+ entityType: rule.entityType,
50
+ eventType: rule.eventType
51
+ });
52
+ }
52
53
  try {
53
54
  if (!rule.enabled) {
54
55
  logger.debug("Rule is disabled", { ruleId: rule.ruleId });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/business_rules/lib/rule-evaluator.ts"],
4
- "sourcesContent": ["import type { BusinessRule } from '../data/entities'\nimport { evaluateExpression, type EvaluationContext, type ConditionExpression } from './expression-evaluator'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('business_rules').child({ component: 'rule-evaluator' })\n\n/**\n * Rule evaluation context\n */\nexport interface RuleEvaluationContext extends EvaluationContext {\n entityType?: string\n entityId?: string\n eventType?: string\n [key: string]: any\n}\n\n/**\n * Rule evaluation result (for evaluating multiple rules)\n */\nexport interface RuleEvaluationResult {\n conditionsPassed: boolean // At least one rule matched\n evaluationCompleted: boolean // All rules evaluated without critical errors\n matchedRules: BusinessRule[]\n failedRules: BusinessRule[]\n evaluationTime: number\n errors?: string[]\n}\n\n/**\n * Single rule evaluation result\n */\nexport interface SingleRuleResult {\n rule: BusinessRule\n conditionsPassed: boolean // Logical result: did conditions evaluate to true?\n evaluationCompleted: boolean // Technical success: did evaluation finish without errors?\n evaluationTime: number\n error?: string\n}\n\n/**\n * Evaluate multiple rules and return aggregated results\n */\nexport async function evaluate(\n rules: BusinessRule[],\n data: any,\n context: RuleEvaluationContext\n): Promise<RuleEvaluationResult> {\n const startTime = Date.now()\n const matchedRules: BusinessRule[] = []\n const failedRules: BusinessRule[] = []\n const errors: string[] = []\n let anyEvaluationCompleted = false\n\n // Sort rules by priority (higher priority first)\n const sortedRules = sortRulesByPriority(rules)\n\n // Evaluate each rule\n for (const rule of sortedRules) {\n try {\n const result = await evaluateSingleRule(rule, data, context)\n\n // Track if any rule completed evaluation\n if (result.evaluationCompleted) {\n anyEvaluationCompleted = true\n }\n\n if (result.evaluationCompleted && result.conditionsPassed) {\n matchedRules.push(rule)\n } else {\n failedRules.push(rule)\n }\n\n if (result.error) {\n errors.push(`Rule ${rule.ruleId}: ${result.error}`)\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n errors.push(`Rule ${rule.ruleId}: ${errorMessage}`)\n failedRules.push(rule)\n }\n }\n\n const evaluationTime = Math.max(0, Date.now() - startTime)\n\n // Determine if conditions were met (at least one rule matched)\n const conditionsPassed = matchedRules.length > 0\n\n return {\n conditionsPassed,\n evaluationCompleted: anyEvaluationCompleted,\n matchedRules,\n failedRules,\n evaluationTime,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Evaluate a single business rule\n */\nexport async function evaluateSingleRule(\n rule: BusinessRule,\n data: any,\n context: RuleEvaluationContext\n): Promise<SingleRuleResult> {\n const startTime = Date.now()\n\n logger.debug('Evaluating rule', {\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n ruleType: rule.ruleType,\n entityType: rule.entityType,\n eventType: rule.eventType,\n conditions: rule.conditionExpression,\n })\n\n try {\n // Check if rule is enabled\n if (!rule.enabled) {\n logger.debug('Rule is disabled', { ruleId: rule.ruleId })\n return {\n rule,\n conditionsPassed: false,\n evaluationCompleted: false,\n evaluationTime: 0,\n error: 'Rule is disabled',\n }\n }\n\n // Check effective date range\n if (!isRuleEffective(rule)) {\n logger.debug('Rule is not effective (outside date range)', { ruleId: rule.ruleId })\n return {\n rule,\n conditionsPassed: false,\n evaluationCompleted: false,\n evaluationTime: 0,\n error: 'Rule is not effective (outside date range)',\n }\n }\n\n // Evaluate conditions\n const conditionsPassed = await evaluateConditions(rule.conditionExpression, data, context)\n\n const evaluationTime = Math.max(0, Date.now() - startTime)\n\n logger.debug('Rule evaluated', {\n ruleId: rule.ruleId,\n conditionsPassed,\n evaluationTimeMs: evaluationTime,\n })\n\n return {\n rule,\n conditionsPassed,\n evaluationCompleted: true,\n evaluationTime,\n }\n } catch (error) {\n const evaluationTime = Math.max(0, Date.now() - startTime)\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n logger.debug('Rule evaluation error', { ruleId: rule.ruleId, message: errorMessage })\n\n return {\n rule,\n conditionsPassed: false,\n evaluationCompleted: false,\n evaluationTime,\n error: errorMessage,\n }\n }\n}\n\n/**\n * Evaluate rule conditions (delegates to expression evaluator)\n */\nexport async function evaluateConditions(\n conditions: any,\n data: any,\n context: RuleEvaluationContext\n): Promise<boolean> {\n if (!conditions) {\n return true\n }\n\n try {\n const expression = conditions as ConditionExpression\n return evaluateExpression(expression, data, context)\n } catch (error) {\n // If evaluation fails, the rule fails\n throw new Error(`Condition evaluation failed: ${error instanceof Error ? error.message : String(error)}`)\n }\n}\n\n/**\n * Sort rules by priority (higher priority first, then by ID for stability)\n */\nexport function sortRulesByPriority(rules: BusinessRule[]): BusinessRule[] {\n return [...rules].sort((a, b) => {\n if (b.priority !== a.priority) {\n return b.priority - a.priority\n }\n\n // If priority is the same, sort by ruleId for stability\n return a.ruleId.localeCompare(b.ruleId)\n })\n}\n\n/**\n * Check if rule is within its effective date range\n */\nfunction isRuleEffective(rule: BusinessRule): boolean {\n const now = new Date()\n\n // Check effectiveFrom\n if (rule.effectiveFrom && rule.effectiveFrom > now) {\n return false\n }\n\n // Check effectiveTo\n if (rule.effectiveTo && rule.effectiveTo < now) {\n return false\n }\n\n return true\n}\n\n/**\n * Filter rules by entity type and event type\n */\nexport function filterRulesByContext(\n rules: BusinessRule[],\n entityType?: string,\n eventType?: string\n): BusinessRule[] {\n return rules.filter((rule) => {\n // Filter by entity type\n if (entityType && rule.entityType !== entityType) {\n return false\n }\n\n // Filter by event type (if rule specifies one)\n if (eventType && rule.eventType && rule.eventType !== eventType) {\n return false\n }\n\n return true\n })\n}\n\n/**\n * Get applicable rules for a context (filtered, sorted, and enabled)\n */\nexport function getApplicableRules(\n rules: BusinessRule[],\n entityType?: string,\n eventType?: string\n): BusinessRule[] {\n // Filter by context\n const filtered = filterRulesByContext(rules, entityType, eventType)\n\n // Filter by enabled status\n const enabled = filtered.filter((rule) => rule.enabled)\n\n // Filter by effective date\n const effective = enabled.filter((rule) => isRuleEffective(rule))\n\n // Sort by priority\n return sortRulesByPriority(effective)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { BusinessRule } from '../data/entities'\nimport { evaluateExpression, type EvaluationContext, type ConditionExpression } from './expression-evaluator'\nimport { createLogger, isLevelEnabled } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('business_rules').child({ component: 'rule-evaluator' })\n\n/**\n * Rule evaluation context\n */\nexport interface RuleEvaluationContext extends EvaluationContext {\n entityType?: string\n entityId?: string\n eventType?: string\n [key: string]: any\n}\n\n/**\n * Rule evaluation result (for evaluating multiple rules)\n */\nexport interface RuleEvaluationResult {\n conditionsPassed: boolean // At least one rule matched\n evaluationCompleted: boolean // All rules evaluated without critical errors\n matchedRules: BusinessRule[]\n failedRules: BusinessRule[]\n evaluationTime: number\n errors?: string[]\n}\n\n/**\n * Single rule evaluation result\n */\nexport interface SingleRuleResult {\n rule: BusinessRule\n conditionsPassed: boolean // Logical result: did conditions evaluate to true?\n evaluationCompleted: boolean // Technical success: did evaluation finish without errors?\n evaluationTime: number\n error?: string\n}\n\n/**\n * Evaluate multiple rules and return aggregated results\n */\nexport async function evaluate(\n rules: BusinessRule[],\n data: any,\n context: RuleEvaluationContext\n): Promise<RuleEvaluationResult> {\n const startTime = Date.now()\n const matchedRules: BusinessRule[] = []\n const failedRules: BusinessRule[] = []\n const errors: string[] = []\n let anyEvaluationCompleted = false\n\n // Sort rules by priority (higher priority first)\n const sortedRules = sortRulesByPriority(rules)\n\n // Evaluate each rule\n for (const rule of sortedRules) {\n try {\n const result = await evaluateSingleRule(rule, data, context)\n\n // Track if any rule completed evaluation\n if (result.evaluationCompleted) {\n anyEvaluationCompleted = true\n }\n\n if (result.evaluationCompleted && result.conditionsPassed) {\n matchedRules.push(rule)\n } else {\n failedRules.push(rule)\n }\n\n if (result.error) {\n errors.push(`Rule ${rule.ruleId}: ${result.error}`)\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n errors.push(`Rule ${rule.ruleId}: ${errorMessage}`)\n failedRules.push(rule)\n }\n }\n\n const evaluationTime = Math.max(0, Date.now() - startTime)\n\n // Determine if conditions were met (at least one rule matched)\n const conditionsPassed = matchedRules.length > 0\n\n return {\n conditionsPassed,\n evaluationCompleted: anyEvaluationCompleted,\n matchedRules,\n failedRules,\n evaluationTime,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Evaluate a single business rule\n */\nexport async function evaluateSingleRule(\n rule: BusinessRule,\n data: any,\n context: RuleEvaluationContext\n): Promise<SingleRuleResult> {\n const startTime = Date.now()\n\n if (isLevelEnabled('debug')) {\n logger.debug('Evaluating rule', {\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n ruleType: rule.ruleType,\n entityType: rule.entityType,\n eventType: rule.eventType,\n })\n }\n\n try {\n // Check if rule is enabled\n if (!rule.enabled) {\n logger.debug('Rule is disabled', { ruleId: rule.ruleId })\n return {\n rule,\n conditionsPassed: false,\n evaluationCompleted: false,\n evaluationTime: 0,\n error: 'Rule is disabled',\n }\n }\n\n // Check effective date range\n if (!isRuleEffective(rule)) {\n logger.debug('Rule is not effective (outside date range)', { ruleId: rule.ruleId })\n return {\n rule,\n conditionsPassed: false,\n evaluationCompleted: false,\n evaluationTime: 0,\n error: 'Rule is not effective (outside date range)',\n }\n }\n\n // Evaluate conditions\n const conditionsPassed = await evaluateConditions(rule.conditionExpression, data, context)\n\n const evaluationTime = Math.max(0, Date.now() - startTime)\n\n logger.debug('Rule evaluated', {\n ruleId: rule.ruleId,\n conditionsPassed,\n evaluationTimeMs: evaluationTime,\n })\n\n return {\n rule,\n conditionsPassed,\n evaluationCompleted: true,\n evaluationTime,\n }\n } catch (error) {\n const evaluationTime = Math.max(0, Date.now() - startTime)\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n logger.debug('Rule evaluation error', { ruleId: rule.ruleId, message: errorMessage })\n\n return {\n rule,\n conditionsPassed: false,\n evaluationCompleted: false,\n evaluationTime,\n error: errorMessage,\n }\n }\n}\n\n/**\n * Evaluate rule conditions (delegates to expression evaluator)\n */\nexport async function evaluateConditions(\n conditions: any,\n data: any,\n context: RuleEvaluationContext\n): Promise<boolean> {\n if (!conditions) {\n return true\n }\n\n try {\n const expression = conditions as ConditionExpression\n return evaluateExpression(expression, data, context)\n } catch (error) {\n // If evaluation fails, the rule fails\n throw new Error(`Condition evaluation failed: ${error instanceof Error ? error.message : String(error)}`)\n }\n}\n\n/**\n * Sort rules by priority (higher priority first, then by ID for stability)\n */\nexport function sortRulesByPriority(rules: BusinessRule[]): BusinessRule[] {\n return [...rules].sort((a, b) => {\n if (b.priority !== a.priority) {\n return b.priority - a.priority\n }\n\n // If priority is the same, sort by ruleId for stability\n return a.ruleId.localeCompare(b.ruleId)\n })\n}\n\n/**\n * Check if rule is within its effective date range\n */\nfunction isRuleEffective(rule: BusinessRule): boolean {\n const now = new Date()\n\n // Check effectiveFrom\n if (rule.effectiveFrom && rule.effectiveFrom > now) {\n return false\n }\n\n // Check effectiveTo\n if (rule.effectiveTo && rule.effectiveTo < now) {\n return false\n }\n\n return true\n}\n\n/**\n * Filter rules by entity type and event type\n */\nexport function filterRulesByContext(\n rules: BusinessRule[],\n entityType?: string,\n eventType?: string\n): BusinessRule[] {\n return rules.filter((rule) => {\n // Filter by entity type\n if (entityType && rule.entityType !== entityType) {\n return false\n }\n\n // Filter by event type (if rule specifies one)\n if (eventType && rule.eventType && rule.eventType !== eventType) {\n return false\n }\n\n return true\n })\n}\n\n/**\n * Get applicable rules for a context (filtered, sorted, and enabled)\n */\nexport function getApplicableRules(\n rules: BusinessRule[],\n entityType?: string,\n eventType?: string\n): BusinessRule[] {\n // Filter by context\n const filtered = filterRulesByContext(rules, entityType, eventType)\n\n // Filter by enabled status\n const enabled = filtered.filter((rule) => rule.enabled)\n\n // Filter by effective date\n const effective = enabled.filter((rule) => isRuleEffective(rule))\n\n // Sort by priority\n return sortRulesByPriority(effective)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -106,14 +106,25 @@ const analyzeDealsToolDefinition = {
106
106
  lastActivityByDeal.set(refId, activityDate);
107
107
  }
108
108
  }
109
+ const dealById = new Map(deals.map((deal) => [deal.id, deal]));
110
+ const scopedDealIds = Array.from(dealById.keys());
109
111
  const personLinkWhere = {
110
- deal: { $in: dealIds }
112
+ deal: { $in: scopedDealIds }
111
113
  };
112
- const personLinks = await em.find(
114
+ const personLinksRaw = await em.find(
113
115
  CustomerDealPersonLink,
114
116
  personLinkWhere,
115
117
  { limit: 500 }
116
118
  );
119
+ const personLinks = personLinksRaw.filter((link) => {
120
+ const dealRefId = refIdOf(link.deal);
121
+ if (!dealRefId) return false;
122
+ const scopedDeal = dealById.get(dealRefId);
123
+ if (!scopedDeal) return false;
124
+ if (scopedDeal.tenantId !== tenantId) return false;
125
+ if (ctx.organizationId && scopedDeal.organizationId !== ctx.organizationId) return false;
126
+ return true;
127
+ });
117
128
  const linksByDeal = /* @__PURE__ */ new Map();
118
129
  for (const link of personLinks) {
119
130
  const dealRefId = refIdOf(link.deal);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/ai-tools/deal-analyzer-pack.ts"],
4
- "sourcesContent": ["/**\n * `customers.analyze_deals` \u2014 analytical read-only tool for the deal_analyzer\n * agent (Step d1 of the deal-analyzer-demo).\n *\n * Reads deals scoped to the caller tenant/org, enriches each deal with the\n * most recent activity timestamp (days-since-last-activity), computes a simple\n * healthScore, and returns a ranked list. The handler issues two bounded DB\n * reads (deals + activities) \u2014 no N+1.\n *\n * `customers.update_deal_stage` already exists in deals-pack.ts and is\n * imported and re-exported here so the deal_analyzer agent's `allowedTools`\n * list can reference both under a single pack. The tool itself is NOT\n * redeclared here.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerActivity,\n CustomerDealPersonLink,\n CustomerEntity,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n} from './types'\n\nfunction refIdOf(ref: unknown): string | undefined {\n if (!ref) return undefined\n if (typeof ref === 'string') return ref\n if (typeof ref === 'object' && typeof (ref as { id?: unknown }).id === 'string') {\n return (ref as { id: string }).id\n }\n return undefined\n}\n\nfunction resolveEm(ctx: CustomersToolContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * Compute a health score for a deal:\n * healthScore = clamp((30 - daysSinceLastActivity) / 30 * 100, 0, 100)\n * A deal with activity within the last 30 days scores > 0. Deals with no\n * activity default to daysSinceLastActivity = activityWindow so they score 0.\n * Exported for unit testing.\n */\nexport function computeHealthScore(daysSinceLastActivity: number): number {\n return clamp(((30 - daysSinceLastActivity) / 30) * 100, 0, 100)\n}\n\nconst analyzeDealsInput = z.object({\n dealStageFilter: z\n .string()\n .optional()\n .describe(\n 'Optional pipeline stage name or status slug to restrict results (e.g. \"open\", \"qualification\", \"negotiation\").',\n ),\n daysOfActivityWindow: z\n .number()\n .int()\n .min(1)\n .max(365)\n .default(30)\n .describe(\n 'Number of days to look back for \"last activity\" calculation. Deals with no activity in this window are flagged as stalled. Default 30.',\n ),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .default(25)\n .describe('Maximum number of deals to return, ranked by healthScore ascending (most at-risk first). Default 25.'),\n})\n\ntype AnalyzeDealsInput = z.infer<typeof analyzeDealsInput>\n\ntype DealAnalysisItem = {\n id: string\n title: string\n value: number | null\n valueCurrency: string | null\n stage: string | null\n status: string\n daysSinceLastActivity: number\n primaryContact: string | null\n healthScore: number\n}\n\ntype AnalyzeDealsOutput = {\n deals: DealAnalysisItem[]\n totalAnalyzed: number\n stalledCount: number\n windowDays: number\n}\n\nexport function msTodays(ms: number): number {\n return Math.floor(ms / (1000 * 60 * 60 * 24))\n}\n\nconst analyzeDealsToolDefinition: CustomersAiToolDefinition<AnalyzeDealsInput, AnalyzeDealsOutput> = {\n name: 'customers.analyze_deals',\n displayName: 'Analyze deals',\n description:\n 'Fetch and analyze open deals for the caller tenant. Returns each deal with its days-since-last-activity and a health score (0\u2013100, lower = more stalled). Results are ranked most-at-risk first.',\n inputSchema: analyzeDealsInput as z.ZodType<AnalyzeDealsInput>,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers', 'analytics'],\n isMutation: false,\n handler: async (rawInput, ctx): Promise<AnalyzeDealsOutput> => {\n const { tenantId } = assertTenantScope(ctx)\n const input = analyzeDealsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const scope = { tenantId, organizationId: ctx.organizationId }\n\n // Build deal filter\n const dealWhere: Record<string, unknown> = {\n tenantId,\n deletedAt: null,\n }\n if (ctx.organizationId) dealWhere.organizationId = ctx.organizationId\n // Apply stage/status filter when provided. `$or` is composed with the\n // outer `deletedAt: null` filter \u2014 both must hold \u2014 so the caller-supplied\n // value matches either the canonical status slug or the stage label.\n if (input.dealStageFilter) {\n const filterValue = input.dealStageFilter.trim()\n if (filterValue.length) {\n dealWhere.$or = [\n { status: filterValue },\n { pipelineStage: filterValue },\n ]\n }\n }\n\n const deals = await findWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n dealWhere as any,\n {\n orderBy: { updatedAt: 'desc' },\n limit: Math.min(input.limit * 4, 400), // Fetch extra so we can rank properly\n },\n scope,\n )\n\n if (!deals.length) {\n return { deals: [], totalAnalyzed: 0, stalledCount: 0, windowDays: input.daysOfActivityWindow }\n }\n\n const dealIds = deals.map((d) => d.id)\n\n // Fetch most recent activity per deal within the window in one batch.\n // CustomerActivity has encrypted columns (subject, body); use the\n // tenant-aware helper even though this query only reads occurredAt/deal,\n // so future readers cannot accidentally surface ciphertext.\n const activityWhere: Record<string, unknown> = {\n deal: { $in: dealIds },\n tenantId,\n }\n if (ctx.organizationId) activityWhere.organizationId = ctx.organizationId\n const activities = await findWithDecryption<CustomerActivity>(\n em,\n CustomerActivity,\n activityWhere as any,\n {\n orderBy: { occurredAt: 'desc' },\n limit: 1000,\n },\n scope,\n )\n\n const lastActivityByDeal = new Map<string, Date>()\n for (const activity of activities) {\n const refId = refIdOf((activity as { deal?: unknown }).deal)\n if (!refId) continue\n const activityDate = activity.occurredAt ?? activity.createdAt\n if (!activityDate) continue\n const existing = lastActivityByDeal.get(refId)\n if (!existing || activityDate > existing) {\n lastActivityByDeal.set(refId, activityDate)\n }\n }\n\n // Fetch primary contacts for deals in one batch. CustomerDealPersonLink\n // has no encrypted columns, but the linked CustomerEntity.display_name is\n // encrypted, so resolve names via findWithDecryption rather than\n // populating the relation through raw em.find.\n const personLinkWhere: Record<string, unknown> = {\n deal: { $in: dealIds },\n }\n const personLinks = await em.find(\n CustomerDealPersonLink,\n personLinkWhere as any,\n { limit: 500 },\n )\n\n const linksByDeal = new Map<string, string>() // dealId \u2192 personId (first link wins)\n for (const link of personLinks) {\n const dealRefId = refIdOf((link as { deal?: unknown }).deal)\n const personRefId = refIdOf((link as { person?: unknown }).person)\n if (!dealRefId || !personRefId) continue\n if (!linksByDeal.has(dealRefId)) linksByDeal.set(dealRefId, personRefId)\n }\n\n const primaryContactByDeal = new Map<string, string>()\n const personIds = Array.from(new Set(linksByDeal.values()))\n if (personIds.length) {\n const personWhere: Record<string, unknown> = {\n id: { $in: personIds },\n tenantId,\n }\n if (ctx.organizationId) personWhere.organizationId = ctx.organizationId\n const persons = await findWithDecryption<CustomerEntity>(\n em,\n CustomerEntity,\n personWhere as any,\n undefined,\n scope,\n )\n const nameByPersonId = new Map<string, string>()\n for (const person of persons) {\n const name = person.displayName\n if (typeof name === 'string' && name.length) {\n nameByPersonId.set(person.id, name)\n }\n }\n for (const [dealId, personId] of linksByDeal) {\n const name = nameByPersonId.get(personId)\n if (name) primaryContactByDeal.set(dealId, name)\n }\n }\n\n const now = Date.now()\n const analyzed: DealAnalysisItem[] = deals.map((deal) => {\n const lastActivity = lastActivityByDeal.get(deal.id)\n const daysSinceLastActivity = lastActivity\n ? msTodays(now - lastActivity.getTime())\n : input.daysOfActivityWindow // Treat as fully stalled when no activity\n const value = deal.valueAmount ? parseFloat(String(deal.valueAmount)) : null\n return {\n id: deal.id,\n title: deal.title,\n value: Number.isFinite(value) ? value : null,\n valueCurrency: deal.valueCurrency ?? null,\n stage: deal.pipelineStage ?? deal.status ?? null,\n status: deal.status ?? 'open',\n daysSinceLastActivity,\n primaryContact: primaryContactByDeal.get(deal.id) ?? null,\n healthScore: Math.round(computeHealthScore(daysSinceLastActivity)),\n }\n })\n\n // Sort most-at-risk first (lowest health score first, then by value desc)\n analyzed.sort((a, b) => {\n if (a.healthScore !== b.healthScore) return a.healthScore - b.healthScore\n const aVal = a.value ?? 0\n const bVal = b.value ?? 0\n return bVal - aVal\n })\n\n const truncated = analyzed.slice(0, input.limit)\n const stalledCount = truncated.filter(\n (d) => d.daysSinceLastActivity >= input.daysOfActivityWindow,\n ).length\n\n return {\n deals: truncated,\n totalAnalyzed: deals.length,\n stalledCount,\n windowDays: input.daysOfActivityWindow,\n }\n },\n}\n\nexport const dealAnalyzerAiTools: CustomersAiToolDefinition[] = [\n analyzeDealsToolDefinition as unknown as CustomersAiToolDefinition,\n]\n\nexport default dealAnalyzerAiTools\n"],
5
- "mappings": "AAeA,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAGK;AAEP,SAAS,QAAQ,KAAkC;AACjD,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,OAAO,QAAQ,YAAY,OAAQ,IAAyB,OAAO,UAAU;AAC/E,WAAQ,IAAuB;AAAA,EACjC;AACA,SAAO;AACT;AAEA,SAAS,UAAU,KAA0C;AAC3D,SAAO,IAAI,UAAU,QAAuB,IAAI;AAClD;AAEA,SAAS,MAAM,OAAe,KAAa,KAAqB;AAC9D,SAAO,KAAK,IAAI,KAAK,IAAI,OAAO,GAAG,GAAG,GAAG;AAC3C;AASO,SAAS,mBAAmB,uBAAuC;AACxE,SAAO,OAAQ,KAAK,yBAAyB,KAAM,KAAK,GAAG,GAAG;AAChE;AAEA,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACjC,iBAAiB,EACd,OAAO,EACP,SAAS,EACT;AAAA,IACC;AAAA,EACF;AAAA,EACF,sBAAsB,EACnB,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,IAAI,GAAG,EACP,QAAQ,EAAE,EACV;AAAA,IACC;AAAA,EACF;AAAA,EACF,OAAO,EACJ,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,IAAI,GAAG,EACP,QAAQ,EAAE,EACV,SAAS,sGAAsG;AACpH,CAAC;AAuBM,SAAS,SAAS,IAAoB;AAC3C,SAAO,KAAK,MAAM,MAAM,MAAO,KAAK,KAAK,GAAG;AAC9C;AAEA,MAAM,6BAA+F;AAAA,EACnG,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,sBAAsB;AAAA,EACzC,MAAM,CAAC,QAAQ,aAAa,WAAW;AAAA,EACvC,YAAY;AAAA,EACZ,SAAS,OAAO,UAAU,QAAqC;AAC7D,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAAQ,kBAAkB,MAAM,QAAQ;AAC9C,UAAM,KAAK,UAAU,GAAG;AACxB,UAAM,QAAQ,EAAE,UAAU,gBAAgB,IAAI,eAAe;AAG7D,UAAM,YAAqC;AAAA,MACzC;AAAA,MACA,WAAW;AAAA,IACb;AACA,QAAI,IAAI,eAAgB,WAAU,iBAAiB,IAAI;AAIvD,QAAI,MAAM,iBAAiB;AACzB,YAAM,cAAc,MAAM,gBAAgB,KAAK;AAC/C,UAAI,YAAY,QAAQ;AACtB,kBAAU,MAAM;AAAA,UACd,EAAE,QAAQ,YAAY;AAAA,UACtB,EAAE,eAAe,YAAY;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,MAAM;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,QACE,SAAS,EAAE,WAAW,OAAO;AAAA,QAC7B,OAAO,KAAK,IAAI,MAAM,QAAQ,GAAG,GAAG;AAAA;AAAA,MACtC;AAAA,MACA;AAAA,IACF;AAEA,QAAI,CAAC,MAAM,QAAQ;AACjB,aAAO,EAAE,OAAO,CAAC,GAAG,eAAe,GAAG,cAAc,GAAG,YAAY,MAAM,qBAAqB;AAAA,IAChG;AAEA,UAAM,UAAU,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE;AAMrC,UAAM,gBAAyC;AAAA,MAC7C,MAAM,EAAE,KAAK,QAAQ;AAAA,MACrB;AAAA,IACF;AACA,QAAI,IAAI,eAAgB,eAAc,iBAAiB,IAAI;AAC3D,UAAM,aAAa,MAAM;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,QACE,SAAS,EAAE,YAAY,OAAO;AAAA,QAC9B,OAAO;AAAA,MACT;AAAA,MACA;AAAA,IACF;AAEA,UAAM,qBAAqB,oBAAI,IAAkB;AACjD,eAAW,YAAY,YAAY;AACjC,YAAM,QAAQ,QAAS,SAAgC,IAAI;AAC3D,UAAI,CAAC,MAAO;AACZ,YAAM,eAAe,SAAS,cAAc,SAAS;AACrD,UAAI,CAAC,aAAc;AACnB,YAAM,WAAW,mBAAmB,IAAI,KAAK;AAC7C,UAAI,CAAC,YAAY,eAAe,UAAU;AACxC,2BAAmB,IAAI,OAAO,YAAY;AAAA,MAC5C;AAAA,IACF;AAMA,UAAM,kBAA2C;AAAA,MAC/C,MAAM,EAAE,KAAK,QAAQ;AAAA,IACvB;AACA,UAAM,cAAc,MAAM,GAAG;AAAA,MAC3B;AAAA,MACA;AAAA,MACA,EAAE,OAAO,IAAI;AAAA,IACf;AAEA,UAAM,cAAc,oBAAI,IAAoB;AAC5C,eAAW,QAAQ,aAAa;AAC9B,YAAM,YAAY,QAAS,KAA4B,IAAI;AAC3D,YAAM,cAAc,QAAS,KAA8B,MAAM;AACjE,UAAI,CAAC,aAAa,CAAC,YAAa;AAChC,UAAI,CAAC,YAAY,IAAI,SAAS,EAAG,aAAY,IAAI,WAAW,WAAW;AAAA,IACzE;AAEA,UAAM,uBAAuB,oBAAI,IAAoB;AACrD,UAAM,YAAY,MAAM,KAAK,IAAI,IAAI,YAAY,OAAO,CAAC,CAAC;AAC1D,QAAI,UAAU,QAAQ;AACpB,YAAM,cAAuC;AAAA,QAC3C,IAAI,EAAE,KAAK,UAAU;AAAA,QACrB;AAAA,MACF;AACA,UAAI,IAAI,eAAgB,aAAY,iBAAiB,IAAI;AACzD,YAAM,UAAU,MAAM;AAAA,QACpB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,YAAM,iBAAiB,oBAAI,IAAoB;AAC/C,iBAAW,UAAU,SAAS;AAC5B,cAAM,OAAO,OAAO;AACpB,YAAI,OAAO,SAAS,YAAY,KAAK,QAAQ;AAC3C,yBAAe,IAAI,OAAO,IAAI,IAAI;AAAA,QACpC;AAAA,MACF;AACA,iBAAW,CAAC,QAAQ,QAAQ,KAAK,aAAa;AAC5C,cAAM,OAAO,eAAe,IAAI,QAAQ;AACxC,YAAI,KAAM,sBAAqB,IAAI,QAAQ,IAAI;AAAA,MACjD;AAAA,IACF;AAEA,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,WAA+B,MAAM,IAAI,CAAC,SAAS;AACvD,YAAM,eAAe,mBAAmB,IAAI,KAAK,EAAE;AACnD,YAAM,wBAAwB,eAC1B,SAAS,MAAM,aAAa,QAAQ,CAAC,IACrC,MAAM;AACV,YAAM,QAAQ,KAAK,cAAc,WAAW,OAAO,KAAK,WAAW,CAAC,IAAI;AACxE,aAAO;AAAA,QACL,IAAI,KAAK;AAAA,QACT,OAAO,KAAK;AAAA,QACZ,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,QACxC,eAAe,KAAK,iBAAiB;AAAA,QACrC,OAAO,KAAK,iBAAiB,KAAK,UAAU;AAAA,QAC5C,QAAQ,KAAK,UAAU;AAAA,QACvB;AAAA,QACA,gBAAgB,qBAAqB,IAAI,KAAK,EAAE,KAAK;AAAA,QACrD,aAAa,KAAK,MAAM,mBAAmB,qBAAqB,CAAC;AAAA,MACnE;AAAA,IACF,CAAC;AAGD,aAAS,KAAK,CAAC,GAAG,MAAM;AACtB,UAAI,EAAE,gBAAgB,EAAE,YAAa,QAAO,EAAE,cAAc,EAAE;AAC9D,YAAM,OAAO,EAAE,SAAS;AACxB,YAAM,OAAO,EAAE,SAAS;AACxB,aAAO,OAAO;AAAA,IAChB,CAAC;AAED,UAAM,YAAY,SAAS,MAAM,GAAG,MAAM,KAAK;AAC/C,UAAM,eAAe,UAAU;AAAA,MAC7B,CAAC,MAAM,EAAE,yBAAyB,MAAM;AAAA,IAC1C,EAAE;AAEF,WAAO;AAAA,MACL,OAAO;AAAA,MACP,eAAe,MAAM;AAAA,MACrB;AAAA,MACA,YAAY,MAAM;AAAA,IACpB;AAAA,EACF;AACF;AAEO,MAAM,sBAAmD;AAAA,EAC9D;AACF;AAEA,IAAO,6BAAQ;",
4
+ "sourcesContent": ["/**\n * `customers.analyze_deals` \u2014 analytical read-only tool for the deal_analyzer\n * agent (Step d1 of the deal-analyzer-demo).\n *\n * Reads deals scoped to the caller tenant/org, enriches each deal with the\n * most recent activity timestamp (days-since-last-activity), computes a simple\n * healthScore, and returns a ranked list. The handler issues two bounded DB\n * reads (deals + activities) \u2014 no N+1.\n *\n * `customers.update_deal_stage` already exists in deals-pack.ts and is\n * imported and re-exported here so the deal_analyzer agent's `allowedTools`\n * list can reference both under a single pack. The tool itself is NOT\n * redeclared here.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerActivity,\n CustomerDealPersonLink,\n CustomerEntity,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n} from './types'\n\nfunction refIdOf(ref: unknown): string | undefined {\n if (!ref) return undefined\n if (typeof ref === 'string') return ref\n if (typeof ref === 'object' && typeof (ref as { id?: unknown }).id === 'string') {\n return (ref as { id: string }).id\n }\n return undefined\n}\n\nfunction resolveEm(ctx: CustomersToolContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * Compute a health score for a deal:\n * healthScore = clamp((30 - daysSinceLastActivity) / 30 * 100, 0, 100)\n * A deal with activity within the last 30 days scores > 0. Deals with no\n * activity default to daysSinceLastActivity = activityWindow so they score 0.\n * Exported for unit testing.\n */\nexport function computeHealthScore(daysSinceLastActivity: number): number {\n return clamp(((30 - daysSinceLastActivity) / 30) * 100, 0, 100)\n}\n\nconst analyzeDealsInput = z.object({\n dealStageFilter: z\n .string()\n .optional()\n .describe(\n 'Optional pipeline stage name or status slug to restrict results (e.g. \"open\", \"qualification\", \"negotiation\").',\n ),\n daysOfActivityWindow: z\n .number()\n .int()\n .min(1)\n .max(365)\n .default(30)\n .describe(\n 'Number of days to look back for \"last activity\" calculation. Deals with no activity in this window are flagged as stalled. Default 30.',\n ),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .default(25)\n .describe('Maximum number of deals to return, ranked by healthScore ascending (most at-risk first). Default 25.'),\n})\n\ntype AnalyzeDealsInput = z.infer<typeof analyzeDealsInput>\n\ntype DealAnalysisItem = {\n id: string\n title: string\n value: number | null\n valueCurrency: string | null\n stage: string | null\n status: string\n daysSinceLastActivity: number\n primaryContact: string | null\n healthScore: number\n}\n\ntype AnalyzeDealsOutput = {\n deals: DealAnalysisItem[]\n totalAnalyzed: number\n stalledCount: number\n windowDays: number\n}\n\nexport function msTodays(ms: number): number {\n return Math.floor(ms / (1000 * 60 * 60 * 24))\n}\n\nconst analyzeDealsToolDefinition: CustomersAiToolDefinition<AnalyzeDealsInput, AnalyzeDealsOutput> = {\n name: 'customers.analyze_deals',\n displayName: 'Analyze deals',\n description:\n 'Fetch and analyze open deals for the caller tenant. Returns each deal with its days-since-last-activity and a health score (0\u2013100, lower = more stalled). Results are ranked most-at-risk first.',\n inputSchema: analyzeDealsInput as z.ZodType<AnalyzeDealsInput>,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers', 'analytics'],\n isMutation: false,\n handler: async (rawInput, ctx): Promise<AnalyzeDealsOutput> => {\n const { tenantId } = assertTenantScope(ctx)\n const input = analyzeDealsInput.parse(rawInput)\n const em = resolveEm(ctx)\n const scope = { tenantId, organizationId: ctx.organizationId }\n\n // Build deal filter\n const dealWhere: Record<string, unknown> = {\n tenantId,\n deletedAt: null,\n }\n if (ctx.organizationId) dealWhere.organizationId = ctx.organizationId\n // Apply stage/status filter when provided. `$or` is composed with the\n // outer `deletedAt: null` filter \u2014 both must hold \u2014 so the caller-supplied\n // value matches either the canonical status slug or the stage label.\n if (input.dealStageFilter) {\n const filterValue = input.dealStageFilter.trim()\n if (filterValue.length) {\n dealWhere.$or = [\n { status: filterValue },\n { pipelineStage: filterValue },\n ]\n }\n }\n\n const deals = await findWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n dealWhere as any,\n {\n orderBy: { updatedAt: 'desc' },\n limit: Math.min(input.limit * 4, 400), // Fetch extra so we can rank properly\n },\n scope,\n )\n\n if (!deals.length) {\n return { deals: [], totalAnalyzed: 0, stalledCount: 0, windowDays: input.daysOfActivityWindow }\n }\n\n const dealIds = deals.map((d) => d.id)\n\n // Fetch most recent activity per deal within the window in one batch.\n // CustomerActivity has encrypted columns (subject, body); use the\n // tenant-aware helper even though this query only reads occurredAt/deal,\n // so future readers cannot accidentally surface ciphertext.\n const activityWhere: Record<string, unknown> = {\n deal: { $in: dealIds },\n tenantId,\n }\n if (ctx.organizationId) activityWhere.organizationId = ctx.organizationId\n const activities = await findWithDecryption<CustomerActivity>(\n em,\n CustomerActivity,\n activityWhere as any,\n {\n orderBy: { occurredAt: 'desc' },\n limit: 1000,\n },\n scope,\n )\n\n const lastActivityByDeal = new Map<string, Date>()\n for (const activity of activities) {\n const refId = refIdOf((activity as { deal?: unknown }).deal)\n if (!refId) continue\n const activityDate = activity.occurredAt ?? activity.createdAt\n if (!activityDate) continue\n const existing = lastActivityByDeal.get(refId)\n if (!existing || activityDate > existing) {\n lastActivityByDeal.set(refId, activityDate)\n }\n }\n\n // Fetch primary contacts for deals in one batch. CustomerDealPersonLink\n // has no encrypted columns, but the linked CustomerEntity.display_name is\n // encrypted, so resolve names via findWithDecryption rather than\n // populating the relation through raw em.find.\n // Defense-in-depth: `customer_deal_people` carries no tenant/org columns,\n // so DB-level scoping is via the tenant-filtered `dealIds` set. Filter the\n // returned rows against the caller-scoped deals map so a future refactor\n // that widens `dealIds` cannot surface cross-tenant links.\n const dealById = new Map<string, CustomerDeal>(deals.map((deal) => [deal.id, deal]))\n const scopedDealIds = Array.from(dealById.keys())\n const personLinkWhere: Record<string, unknown> = {\n deal: { $in: scopedDealIds },\n }\n const personLinksRaw = await em.find(\n CustomerDealPersonLink,\n personLinkWhere as any,\n { limit: 500 },\n )\n const personLinks = personLinksRaw.filter((link) => {\n const dealRefId = refIdOf((link as { deal?: unknown }).deal)\n if (!dealRefId) return false\n const scopedDeal = dealById.get(dealRefId)\n if (!scopedDeal) return false\n if (scopedDeal.tenantId !== tenantId) return false\n if (ctx.organizationId && scopedDeal.organizationId !== ctx.organizationId) return false\n return true\n })\n\n const linksByDeal = new Map<string, string>() // dealId \u2192 personId (first link wins)\n for (const link of personLinks) {\n const dealRefId = refIdOf((link as { deal?: unknown }).deal)\n const personRefId = refIdOf((link as { person?: unknown }).person)\n if (!dealRefId || !personRefId) continue\n if (!linksByDeal.has(dealRefId)) linksByDeal.set(dealRefId, personRefId)\n }\n\n const primaryContactByDeal = new Map<string, string>()\n const personIds = Array.from(new Set(linksByDeal.values()))\n if (personIds.length) {\n const personWhere: Record<string, unknown> = {\n id: { $in: personIds },\n tenantId,\n }\n if (ctx.organizationId) personWhere.organizationId = ctx.organizationId\n const persons = await findWithDecryption<CustomerEntity>(\n em,\n CustomerEntity,\n personWhere as any,\n undefined,\n scope,\n )\n const nameByPersonId = new Map<string, string>()\n for (const person of persons) {\n const name = person.displayName\n if (typeof name === 'string' && name.length) {\n nameByPersonId.set(person.id, name)\n }\n }\n for (const [dealId, personId] of linksByDeal) {\n const name = nameByPersonId.get(personId)\n if (name) primaryContactByDeal.set(dealId, name)\n }\n }\n\n const now = Date.now()\n const analyzed: DealAnalysisItem[] = deals.map((deal) => {\n const lastActivity = lastActivityByDeal.get(deal.id)\n const daysSinceLastActivity = lastActivity\n ? msTodays(now - lastActivity.getTime())\n : input.daysOfActivityWindow // Treat as fully stalled when no activity\n const value = deal.valueAmount ? parseFloat(String(deal.valueAmount)) : null\n return {\n id: deal.id,\n title: deal.title,\n value: Number.isFinite(value) ? value : null,\n valueCurrency: deal.valueCurrency ?? null,\n stage: deal.pipelineStage ?? deal.status ?? null,\n status: deal.status ?? 'open',\n daysSinceLastActivity,\n primaryContact: primaryContactByDeal.get(deal.id) ?? null,\n healthScore: Math.round(computeHealthScore(daysSinceLastActivity)),\n }\n })\n\n // Sort most-at-risk first (lowest health score first, then by value desc)\n analyzed.sort((a, b) => {\n if (a.healthScore !== b.healthScore) return a.healthScore - b.healthScore\n const aVal = a.value ?? 0\n const bVal = b.value ?? 0\n return bVal - aVal\n })\n\n const truncated = analyzed.slice(0, input.limit)\n const stalledCount = truncated.filter(\n (d) => d.daysSinceLastActivity >= input.daysOfActivityWindow,\n ).length\n\n return {\n deals: truncated,\n totalAnalyzed: deals.length,\n stalledCount,\n windowDays: input.daysOfActivityWindow,\n }\n },\n}\n\nexport const dealAnalyzerAiTools: CustomersAiToolDefinition[] = [\n analyzeDealsToolDefinition as unknown as CustomersAiToolDefinition,\n]\n\nexport default dealAnalyzerAiTools\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -73,7 +73,11 @@ const crud = makeCrudRoute({
73
73
  ];
74
74
  }
75
75
  return filters;
76
- }
76
+ },
77
+ // Zones had no decorator before this route gained custom fields, so there is no
78
+ // legacy consumer reading top-level `cf_*`/`cf:*` — take the canonical single-shape
79
+ // response (#1769) rather than emitting both forms.
80
+ decorateCustomFields: { entityIds: [E.wms.warehouse_zone], stripPrefixedKeys: true }
77
81
  },
78
82
  hooks: {
79
83
  afterList: async (payload, ctx) => {
@@ -115,6 +119,13 @@ const GET = crud.GET;
115
119
  const POST = crud.POST;
116
120
  const PUT = crud.PUT;
117
121
  const DELETE = crud.DELETE;
122
+ const customFieldDisplayEntrySchema = z.object({
123
+ key: z.string(),
124
+ label: z.string().nullable(),
125
+ value: z.unknown(),
126
+ kind: z.string().nullable(),
127
+ multi: z.boolean()
128
+ });
118
129
  const zoneListItemSchema = z.object({
119
130
  id: z.string().uuid().nullable().optional(),
120
131
  organization_id: z.string().uuid().nullable().optional(),
@@ -126,7 +137,9 @@ const zoneListItemSchema = z.object({
126
137
  name: z.string().nullable().optional(),
127
138
  priority: z.number().nullable().optional(),
128
139
  created_at: z.string().nullable().optional(),
129
- updated_at: z.string().nullable().optional()
140
+ updated_at: z.string().nullable().optional(),
141
+ customValues: z.record(z.string(), z.unknown()).nullable().optional(),
142
+ customFields: z.array(customFieldDisplayEntrySchema).optional()
130
143
  });
131
144
  const openApi = createWmsCrudOpenApi({
132
145
  resourceName: "Warehouse zone",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/wms/api/zones/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { parseScopedCommandInput, resolveCrudRecordId } from '@open-mercato/shared/lib/api/scoped'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { E } from '#generated/entities.ids.generated'\nimport { WarehouseZone } from '../../data/entities'\nimport { warehouseZoneCreateSchema, warehouseZoneUpdateSchema } from '../../data/validators'\nimport { createPagedListResponseSchema, createWmsCrudOpenApi, defaultOkResponseSchema } from '../openapi'\nimport { attachWarehouseLabelsToListItems } from '../listEnrichers'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['wms.view'] },\n POST: { requireAuth: true, requireFeatures: ['wms.manage_zones'] },\n PUT: { requireAuth: true, requireFeatures: ['wms.manage_zones'] },\n DELETE: { requireAuth: true, requireFeatures: ['wms.manage_zones'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(25),\n search: z.string().optional(),\n warehouseId: z.string().uuid().optional(),\n ids: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: WarehouseZone,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.wms.warehouse_zone },\n list: {\n schema: listSchema,\n entityId: E.wms.warehouse_zone,\n fields: [\n 'id',\n 'organization_id',\n 'tenant_id',\n 'warehouse_id',\n 'code',\n 'name',\n 'priority',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'name',\n code: 'code',\n priority: 'priority',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (query.warehouseId) filters.warehouse_id = { $eq: query.warehouseId }\n if (typeof query.ids === 'string' && query.ids.trim().length > 0) {\n filters.id = {\n $in: query.ids.split(',').map((value) => value.trim()).filter((value) => value.length > 0),\n }\n }\n const term = query.search?.trim()\n if (term) {\n const like = `%${escapeLikePattern(term)}%`\n filters.$or = [\n { code: { $ilike: like } },\n { name: { $ilike: like } },\n ]\n }\n return filters\n },\n },\n hooks: {\n afterList: async (payload, ctx) => {\n await attachWarehouseLabelsToListItems(payload, ctx)\n },\n },\n actions: {\n create: {\n commandId: 'wms.zones.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(warehouseZoneCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({ id: result?.zoneId ?? null }),\n status: 201,\n },\n update: {\n commandId: 'wms.zones.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(warehouseZoneUpdateSchema, raw ?? {}, ctx, translate)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'wms.zones.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n return { id: resolveCrudRecordId(parsed, ctx, translate) }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst zoneListItemSchema = z.object({\n id: z.string().uuid().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n warehouse_id: z.string().uuid().nullable().optional(),\n warehouse_name: z.string().nullable().optional(),\n warehouse_code: z.string().nullable().optional(),\n code: z.string().nullable().optional(),\n name: z.string().nullable().optional(),\n priority: z.number().nullable().optional(),\n created_at: z.string().nullable().optional(),\n updated_at: z.string().nullable().optional(),\n})\n\nexport const openApi = createWmsCrudOpenApi({\n resourceName: 'Warehouse zone',\n pluralName: 'Warehouse zones',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(zoneListItemSchema),\n create: {\n schema: warehouseZoneCreateSchema,\n description: 'Creates a zone within a warehouse.',\n },\n update: {\n schema: warehouseZoneUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a warehouse zone by id.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a warehouse zone by id.',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { parseScopedCommandInput, resolveCrudRecordId } from '@open-mercato/shared/lib/api/scoped'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { E } from '#generated/entities.ids.generated'\nimport { WarehouseZone } from '../../data/entities'\nimport { warehouseZoneCreateSchema, warehouseZoneUpdateSchema } from '../../data/validators'\nimport { createPagedListResponseSchema, createWmsCrudOpenApi, defaultOkResponseSchema } from '../openapi'\nimport { attachWarehouseLabelsToListItems } from '../listEnrichers'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['wms.view'] },\n POST: { requireAuth: true, requireFeatures: ['wms.manage_zones'] },\n PUT: { requireAuth: true, requireFeatures: ['wms.manage_zones'] },\n DELETE: { requireAuth: true, requireFeatures: ['wms.manage_zones'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(25),\n search: z.string().optional(),\n warehouseId: z.string().uuid().optional(),\n ids: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: WarehouseZone,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.wms.warehouse_zone },\n list: {\n schema: listSchema,\n entityId: E.wms.warehouse_zone,\n fields: [\n 'id',\n 'organization_id',\n 'tenant_id',\n 'warehouse_id',\n 'code',\n 'name',\n 'priority',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'name',\n code: 'code',\n priority: 'priority',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (query.warehouseId) filters.warehouse_id = { $eq: query.warehouseId }\n if (typeof query.ids === 'string' && query.ids.trim().length > 0) {\n filters.id = {\n $in: query.ids.split(',').map((value) => value.trim()).filter((value) => value.length > 0),\n }\n }\n const term = query.search?.trim()\n if (term) {\n const like = `%${escapeLikePattern(term)}%`\n filters.$or = [\n { code: { $ilike: like } },\n { name: { $ilike: like } },\n ]\n }\n return filters\n },\n // Zones had no decorator before this route gained custom fields, so there is no\n // legacy consumer reading top-level `cf_*`/`cf:*` \u2014 take the canonical single-shape\n // response (#1769) rather than emitting both forms.\n decorateCustomFields: { entityIds: [E.wms.warehouse_zone], stripPrefixedKeys: true },\n },\n hooks: {\n afterList: async (payload, ctx) => {\n await attachWarehouseLabelsToListItems(payload, ctx)\n },\n },\n actions: {\n create: {\n commandId: 'wms.zones.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(warehouseZoneCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({ id: result?.zoneId ?? null }),\n status: 201,\n },\n update: {\n commandId: 'wms.zones.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(warehouseZoneUpdateSchema, raw ?? {}, ctx, translate)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'wms.zones.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n return { id: resolveCrudRecordId(parsed, ctx, translate) }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\n// Mirrors `CustomFieldDisplayEntry` from @open-mercato/shared/lib/crud/custom-fields, so\n// the generated OpenAPI documents how to read an entry instead of an opaque record.\nconst customFieldDisplayEntrySchema = z.object({\n key: z.string(),\n label: z.string().nullable(),\n value: z.unknown(),\n kind: z.string().nullable(),\n multi: z.boolean(),\n})\n\nconst zoneListItemSchema = z.object({\n id: z.string().uuid().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n warehouse_id: z.string().uuid().nullable().optional(),\n warehouse_name: z.string().nullable().optional(),\n warehouse_code: z.string().nullable().optional(),\n code: z.string().nullable().optional(),\n name: z.string().nullable().optional(),\n priority: z.number().nullable().optional(),\n created_at: z.string().nullable().optional(),\n updated_at: z.string().nullable().optional(),\n customValues: z.record(z.string(), z.unknown()).nullable().optional(),\n customFields: z.array(customFieldDisplayEntrySchema).optional(),\n})\n\nexport const openApi = createWmsCrudOpenApi({\n resourceName: 'Warehouse zone',\n pluralName: 'Warehouse zones',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(zoneListItemSchema),\n create: {\n schema: warehouseZoneCreateSchema,\n description: 'Creates a zone within a warehouse.',\n },\n update: {\n schema: warehouseZoneUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a warehouse zone by id.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a warehouse zone by id.',\n },\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,17 @@
1
1
  import { registerCommand } from "@open-mercato/shared/lib/commands";
2
2
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
3
+ import {
4
+ emitCrudSideEffects,
5
+ emitCrudUndoSideEffects,
6
+ parseWithCustomFields,
7
+ setCustomFieldsIfAny
8
+ } from "@open-mercato/shared/lib/commands/helpers";
9
+ import { buildCustomFieldResetMap, loadCustomFieldSnapshot } from "@open-mercato/shared/lib/commands/customFieldSnapshots";
3
10
  import { CrudHttpError, isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
4
11
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
12
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
13
  import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
14
+ import { E } from "../../../generated/entities.ids.generated.js";
7
15
  import {
8
16
  InventoryLot,
9
17
  ProductInventoryProfile,
@@ -28,7 +36,8 @@ import {
28
36
  ensureTenantScope,
29
37
  normalizeOptionalString,
30
38
  requireId,
31
- toNumericString
39
+ toNumericString,
40
+ warehouseZoneCrudIndexer
32
41
  } from "./shared.js";
33
42
  import { emitWmsEvent } from "../events.js";
34
43
  function resolveScope(ctx, fallback) {
@@ -141,7 +150,44 @@ function snapshotWarehouseZone(record) {
141
150
  }
142
151
  async function loadWarehouseZoneSnapshot(em, ctx, id) {
143
152
  const record = await findOneWithDecryption(em, WarehouseZone, { id }, void 0, resolveScope(ctx));
144
- return record ? snapshotWarehouseZone(record) : null;
153
+ if (!record) return null;
154
+ const snapshot = snapshotWarehouseZone(record);
155
+ snapshot.custom = await loadCustomFieldSnapshot(em, {
156
+ entityId: E.wms.warehouse_zone,
157
+ recordId: record.id,
158
+ tenantId: record.tenantId,
159
+ organizationId: record.organizationId
160
+ });
161
+ return snapshot;
162
+ }
163
+ async function writeZoneCustomFields(ctx, zone, values) {
164
+ if (!values || !Object.keys(values).length) return;
165
+ await setCustomFieldsIfAny({
166
+ dataEngine: ctx.container.resolve("dataEngine"),
167
+ entityId: E.wms.warehouse_zone,
168
+ recordId: zone.id,
169
+ organizationId: zone.organizationId,
170
+ tenantId: zone.tenantId,
171
+ values
172
+ });
173
+ }
174
+ async function emitZoneCrudSideEffects(ctx, action, zone, origin = "write") {
175
+ const options = {
176
+ dataEngine: ctx.container.resolve("dataEngine"),
177
+ action,
178
+ entity: zone,
179
+ identifiers: {
180
+ id: zone.id,
181
+ organizationId: zone.organizationId,
182
+ tenantId: zone.tenantId
183
+ },
184
+ indexer: warehouseZoneCrudIndexer
185
+ };
186
+ if (origin === "undo") {
187
+ await emitCrudUndoSideEffects(options);
188
+ return;
189
+ }
190
+ await emitCrudSideEffects(options);
145
191
  }
146
192
  function snapshotWarehouseLocation(record) {
147
193
  const warehouseId = typeof record.warehouse === "string" ? record.warehouse : record.warehouse.id;
@@ -720,11 +766,12 @@ async function restoreZoneFromSnapshot(em, before) {
720
766
  record.metadata = before.metadata;
721
767
  record.deletedAt = null;
722
768
  }
769
+ return record;
723
770
  }
724
771
  const createWarehouseZoneCommand = {
725
772
  id: "wms.zones.create",
726
773
  async execute(rawInput, ctx) {
727
- const parsed = warehouseZoneCreateSchema.parse(rawInput ?? {});
774
+ const { parsed, custom } = parseWithCustomFields(warehouseZoneCreateSchema, rawInput ?? {});
728
775
  ensureTenantScope(ctx, parsed.tenantId);
729
776
  ensureOrganizationScope(ctx, parsed.organizationId);
730
777
  const em = resolveEm(ctx);
@@ -740,6 +787,8 @@ const createWarehouseZoneCommand = {
740
787
  metadata: toJsonValue(parsed.metadata)
741
788
  });
742
789
  await em.persist(zone).flush();
790
+ await writeZoneCustomFields(ctx, zone, custom);
791
+ await emitZoneCrudSideEffects(ctx, "created", zone);
743
792
  void emitWmsEvent("wms.zone.created", {
744
793
  id: zone.id,
745
794
  zoneId: zone.id,
@@ -775,6 +824,8 @@ const createWarehouseZoneCommand = {
775
824
  ensureOrganizationScope(ctx, record.organizationId);
776
825
  record.deletedAt = /* @__PURE__ */ new Date();
777
826
  await em.flush();
827
+ await writeZoneCustomFields(ctx, record, buildCustomFieldResetMap(void 0, after.custom));
828
+ await emitZoneCrudSideEffects(ctx, "deleted", record, "undo");
778
829
  }
779
830
  };
780
831
  const updateWarehouseZoneCommand = {
@@ -786,7 +837,7 @@ const updateWarehouseZoneCommand = {
786
837
  return before ? { before } : {};
787
838
  },
788
839
  async execute(rawInput, ctx) {
789
- const parsed = warehouseZoneUpdateSchema.parse(rawInput ?? {});
840
+ const { parsed, custom } = parseWithCustomFields(warehouseZoneUpdateSchema, rawInput ?? {});
790
841
  const em = resolveEm(ctx);
791
842
  const zone = await loadZone(em, ctx, parsed.id);
792
843
  if (parsed.warehouseId !== void 0) {
@@ -802,6 +853,8 @@ const updateWarehouseZoneCommand = {
802
853
  if (parsed.priority !== void 0) zone.priority = parsed.priority;
803
854
  if (parsed.metadata !== void 0) zone.metadata = toJsonValue(parsed.metadata);
804
855
  await em.flush();
856
+ await writeZoneCustomFields(ctx, zone, custom);
857
+ await emitZoneCrudSideEffects(ctx, "updated", zone);
805
858
  void emitWmsEvent("wms.zone.updated", {
806
859
  id: zone.id,
807
860
  zoneId: zone.id,
@@ -828,8 +881,10 @@ const updateWarehouseZoneCommand = {
828
881
  const em = resolveEm(ctx);
829
882
  ensureTenantScope(ctx, before.tenantId);
830
883
  ensureOrganizationScope(ctx, before.organizationId);
831
- await restoreZoneFromSnapshot(em, before);
884
+ const record = await restoreZoneFromSnapshot(em, before);
832
885
  await em.flush();
886
+ await writeZoneCustomFields(ctx, record, buildCustomFieldResetMap(before.custom, payload?.after?.custom));
887
+ await emitZoneCrudSideEffects(ctx, "updated", record, "undo");
833
888
  }
834
889
  };
835
890
  const deleteWarehouseZoneCommand = {
@@ -846,6 +901,7 @@ const deleteWarehouseZoneCommand = {
846
901
  const zone = await loadZone(em, ctx, zoneId);
847
902
  zone.deletedAt = /* @__PURE__ */ new Date();
848
903
  await em.flush();
904
+ await emitZoneCrudSideEffects(ctx, "deleted", zone);
849
905
  return { zoneId: zone.id };
850
906
  },
851
907
  buildLog: async ({ input, result, ctx, snapshots }) => {
@@ -860,8 +916,10 @@ const deleteWarehouseZoneCommand = {
860
916
  const em = resolveEm(ctx);
861
917
  ensureTenantScope(ctx, before.tenantId);
862
918
  ensureOrganizationScope(ctx, before.organizationId);
863
- await restoreZoneFromSnapshot(em, before);
919
+ const record = await restoreZoneFromSnapshot(em, before);
864
920
  await em.flush();
921
+ await writeZoneCustomFields(ctx, record, buildCustomFieldResetMap(before.custom, void 0));
922
+ await emitZoneCrudSideEffects(ctx, "created", record, "undo");
865
923
  }
866
924
  };
867
925
  async function restoreLocationFromSnapshot(em, before) {