@open-mercato/core 0.6.8-develop.7032.1.70d74d4bfe → 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/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/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
|
@@ -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": "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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": "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) {
|