@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/business_rules/lib/expression-evaluator.js +9 -9
- package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-evaluator.js +10 -9
- package/dist/modules/business_rules/lib/rule-evaluator.js.map +2 -2
- package/dist/modules/customers/ai-tools/deal-analyzer-pack.js +13 -2
- package/dist/modules/customers/ai-tools/deal-analyzer-pack.js.map +2 -2
- package/dist/modules/wms/api/zones/route.js +15 -2
- package/dist/modules/wms/api/zones/route.js.map +2 -2
- package/dist/modules/wms/commands/configuration.js +64 -6
- package/dist/modules/wms/commands/configuration.js.map +2 -2
- package/dist/modules/wms/commands/shared.js +5 -1
- package/dist/modules/wms/commands/shared.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js +43 -8
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +3 -3
- package/package.json +7 -7
- package/src/modules/business_rules/lib/expression-evaluator.ts +9 -9
- package/src/modules/business_rules/lib/rule-evaluator.ts +10 -9
- package/src/modules/customers/ai-tools/deal-analyzer-pack.ts +17 -2
- package/src/modules/wms/api/zones/route.ts +16 -0
- package/src/modules/wms/commands/configuration.ts +89 -6
- package/src/modules/wms/commands/shared.ts +8 -0
- package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +65 -8
package/.turbo/turbo-build.log
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
5
|
-
"mappings": "AACA,SAAS,uBAAuB;AAChC,SAAS,gBAAgB,2BAA2B;AACpD,SAAS,
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
5
|
-
"mappings": "AACA,SAAS,0BAA4E;AACrF,SAAS,
|
|
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:
|
|
112
|
+
deal: { $in: scopedDealIds }
|
|
111
113
|
};
|
|
112
|
-
const
|
|
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": "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
|
|
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": "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;AAUA,UAAM,WAAW,IAAI,IAA0B,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AACnF,UAAM,gBAAgB,MAAM,KAAK,SAAS,KAAK,CAAC;AAChD,UAAM,kBAA2C;AAAA,MAC/C,MAAM,EAAE,KAAK,cAAc;AAAA,IAC7B;AACA,UAAM,iBAAiB,MAAM,GAAG;AAAA,MAC9B;AAAA,MACA;AAAA,MACA,EAAE,OAAO,IAAI;AAAA,IACf;AACA,UAAM,cAAc,eAAe,OAAO,CAAC,SAAS;AAClD,YAAM,YAAY,QAAS,KAA4B,IAAI;AAC3D,UAAI,CAAC,UAAW,QAAO;AACvB,YAAM,aAAa,SAAS,IAAI,SAAS;AACzC,UAAI,CAAC,WAAY,QAAO;AACxB,UAAI,WAAW,aAAa,SAAU,QAAO;AAC7C,UAAI,IAAI,kBAAkB,WAAW,mBAAmB,IAAI,eAAgB,QAAO;AACnF,aAAO;AAAA,IACT,CAAC;AAED,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;",
|
|
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": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC,SAAS,yBAAyB,2BAA2B;AAC7D,SAAS,yBAAyB;AAClC,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B,iCAAiC;AACrE,SAAS,+BAA+B,sBAAsB,+BAA+B;AAC7F,SAAS,wCAAwC;AAEjD,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,UAAU,EAAE;AAAA,EACxD,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AAAA,EACjE,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AAAA,EAChE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AACrE;AAEO,MAAM,WAAW;AAExB,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,MAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACxC,KAAK,EAAE,OAAO,EAAE,SAAS;AAAA,EACzB,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,SAAS,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAC5C,CAAC,EAAE,YAAY;AAEf,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,SAAS,EAAE,YAAY,EAAE,IAAI,eAAe;AAAA,EAC5C,MAAM;AAAA,IACJ,QAAQ;AAAA,IACR,UAAU,EAAE,IAAI;AAAA,IAChB,QAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,UAAU;AAAA,MACV,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,IACA,cAAc,OAAO,UAAU;AAC7B,YAAM,UAAmC,CAAC;AAC1C,UAAI,MAAM,YAAa,SAAQ,eAAe,EAAE,KAAK,MAAM,YAAY;AACvE,UAAI,OAAO,MAAM,QAAQ,YAAY,MAAM,IAAI,KAAK,EAAE,SAAS,GAAG;AAChE,gBAAQ,KAAK;AAAA,UACX,KAAK,MAAM,IAAI,MAAM,GAAG,EAAE,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAAE,OAAO,CAAC,UAAU,MAAM,SAAS,CAAC;AAAA,QAC3F;AAAA,MACF;AACA,YAAM,OAAO,MAAM,QAAQ,KAAK;AAChC,UAAI,MAAM;AACR,cAAM,OAAO,IAAI,kBAAkB,IAAI,CAAC;AACxC,gBAAQ,MAAM;AAAA,UACZ,EAAE,MAAM,EAAE,QAAQ,KAAK,EAAE;AAAA,UACzB,EAAE,MAAM,EAAE,QAAQ,KAAK,EAAE;AAAA,QAC3B;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,
|
|
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
|
-
|
|
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
|
|
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
|
|
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) {
|