@open-mercato/ai-assistant 0.6.4-develop.3962.1.70f30e284c → 0.6.4-develop.3976.1.4e72e60206

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.
@@ -124,13 +124,19 @@ async function GET(req, context) {
124
124
  try {
125
125
  const container = await createRequestContainer();
126
126
  const repo = createConversationStorage(container);
127
- const participants = await repo.listParticipants(callerCtx.conversationId, {
127
+ const repoCtx = {
128
128
  tenantId: callerCtx.tenantId,
129
129
  organizationId: callerCtx.organizationId,
130
130
  userId: callerCtx.userId,
131
131
  canManageConversations: callerCtx.canManageConversations
132
- });
132
+ };
133
+ const conversation = await repo.getById(callerCtx.conversationId, repoCtx);
134
+ if (!conversation) {
135
+ return jsonError(404, "Conversation not found.", "conversation_not_found");
136
+ }
137
+ const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx);
133
138
  return NextResponse.json({
139
+ ownerUserId: conversation.ownerUserId,
134
140
  participants: participants.map((p) => ({
135
141
  userId: p.userId,
136
142
  role: p.role,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/participants/route.ts"],
4
- "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport {\n createConversationStorage,\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n} from '../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nconst addParticipantBodySchema = z.object({\n userId: z.string().uuid('userId must be a valid UUID'),\n role: z.enum(['viewer']).default('viewer'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Manage conversation participants',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversationParticipants',\n summary: 'List active participants of a conversation.',\n description:\n 'Returns the list of active (non-revoked) participants for the conversation. ' +\n 'Only the conversation owner or a caller with `ai_assistant.conversations.manage` can call this endpoint.',\n responses: [\n {\n status: 200,\n description: 'List of active participants.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features.' },\n { status: 404, description: 'Conversation not found or not accessible.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantAddConversationParticipant',\n summary: 'Add a participant to a conversation.',\n description:\n 'Grants a named user read access to the conversation. Requires `ai_assistant.conversations.share`. ' +\n 'Only the conversation owner may add participants. If the user was previously revoked, the soft-deleted row is restored.',\n responses: [\n {\n status: 201,\n description: 'Participant added; conversation visibility updated to \"shared\".',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required feature or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n canShare: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\n }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n canShare: hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const participants = await repo.listParticipants(callerCtx.conversationId, {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n })\n return NextResponse.json({\n participants: participants.map((p) => ({\n userId: p.userId,\n role: p.role,\n lastReadAt: p.lastReadAt ? p.lastReadAt.toISOString() : null,\n addedAt: p.createdAt.toISOString(),\n })),\n })\n } catch (err) {\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (!callerCtx.canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return jsonError(400, 'Invalid JSON body.', 'invalid_body')\n }\n const parseResult = addParticipantBodySchema.safeParse(body)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n const targetUserId = parseResult.data.userId\n if (targetUserId === callerCtx.userId) {\n return jsonError(400, 'Cannot share a conversation with yourself.', 'self_share_not_allowed')\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const targetUserFilter: FilterQuery<User> = {\n id: targetUserId,\n tenantId: callerCtx.tenantId,\n deletedAt: null,\n ...(callerCtx.organizationId ? { organizationId: callerCtx.organizationId } : {}),\n }\n const targetUser = await findOneWithDecryption<User>(\n em,\n User,\n targetUserFilter,\n {},\n { tenantId: callerCtx.tenantId, organizationId: callerCtx.organizationId },\n )\n if (!targetUser) {\n return jsonError(\n 400,\n 'Target user must be a staff user in the same tenant and organization.',\n 'user_not_found',\n )\n }\n\n const repo = createConversationStorage(container)\n const participant = await repo.addParticipant(\n callerCtx.conversationId,\n targetUserId,\n parseResult.data.role,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.shared',\n {\n conversationId: callerCtx.conversationId,\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n ownerUserId: callerCtx.userId,\n participantUserId: participant.userId,\n role: participant.role,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return NextResponse.json(\n {\n participant: {\n userId: participant.userId,\n role: participant.role,\n lastReadAt: participant.lastReadAt ? participant.lastReadAt.toISOString() : null,\n addedAt: participant.createdAt.toISOString(),\n },\n },\n { status: 201 },\n )\n } catch (err) {\n if (err instanceof AiChatConversationDuplicateParticipantError) {\n return jsonError(409, err.message, 'duplicate_participant')\n }\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (err instanceof Error && err.message.toLowerCase().includes('owner')) {\n return jsonError(403, err.message, 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport {\n createConversationStorage,\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n} from '../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nconst addParticipantBodySchema = z.object({\n userId: z.string().uuid('userId must be a valid UUID'),\n role: z.enum(['viewer']).default('viewer'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Manage conversation participants',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversationParticipants',\n summary: 'List active participants of a conversation.',\n description:\n 'Returns the list of active (non-revoked) participants for the conversation. ' +\n 'Only the conversation owner or a caller with `ai_assistant.conversations.manage` can call this endpoint.',\n responses: [\n {\n status: 200,\n description: 'List of active participants.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features.' },\n { status: 404, description: 'Conversation not found or not accessible.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantAddConversationParticipant',\n summary: 'Add a participant to a conversation.',\n description:\n 'Grants a named user read access to the conversation. Requires `ai_assistant.conversations.share`. ' +\n 'Only the conversation owner may add participants. If the user was previously revoked, the soft-deleted row is restored.',\n responses: [\n {\n status: 201,\n description: 'Participant added; conversation visibility updated to \"shared\".',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required feature or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n canShare: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\n }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n canShare: hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const repoCtx = {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n }\n const conversation = await repo.getById(callerCtx.conversationId, repoCtx)\n if (!conversation) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx)\n return NextResponse.json({\n ownerUserId: conversation.ownerUserId,\n participants: participants.map((p) => ({\n userId: p.userId,\n role: p.role,\n lastReadAt: p.lastReadAt ? p.lastReadAt.toISOString() : null,\n addedAt: p.createdAt.toISOString(),\n })),\n })\n } catch (err) {\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (!callerCtx.canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return jsonError(400, 'Invalid JSON body.', 'invalid_body')\n }\n const parseResult = addParticipantBodySchema.safeParse(body)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n const targetUserId = parseResult.data.userId\n if (targetUserId === callerCtx.userId) {\n return jsonError(400, 'Cannot share a conversation with yourself.', 'self_share_not_allowed')\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const targetUserFilter: FilterQuery<User> = {\n id: targetUserId,\n tenantId: callerCtx.tenantId,\n deletedAt: null,\n ...(callerCtx.organizationId ? { organizationId: callerCtx.organizationId } : {}),\n }\n const targetUser = await findOneWithDecryption<User>(\n em,\n User,\n targetUserFilter,\n {},\n { tenantId: callerCtx.tenantId, organizationId: callerCtx.organizationId },\n )\n if (!targetUser) {\n return jsonError(\n 400,\n 'Target user must be a staff user in the same tenant and organization.',\n 'user_not_found',\n )\n }\n\n const repo = createConversationStorage(container)\n const participant = await repo.addParticipant(\n callerCtx.conversationId,\n targetUserId,\n parseResult.data.role,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.shared',\n {\n conversationId: callerCtx.conversationId,\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n ownerUserId: callerCtx.userId,\n participantUserId: participant.userId,\n role: participant.role,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return NextResponse.json(\n {\n participant: {\n userId: participant.userId,\n role: participant.role,\n lastReadAt: participant.lastReadAt ? participant.lastReadAt.toISOString() : null,\n addedAt: participant.createdAt.toISOString(),\n },\n },\n { status: 201 },\n )\n } catch (err) {\n if (err instanceof AiChatConversationDuplicateParticipantError) {\n return jsonError(409, err.message, 'duplicate_participant')\n }\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (err instanceof Error && err.message.toLowerCase().includes('owner')) {\n return jsonError(403, err.message, 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -220,7 +220,7 @@
220
220
  "ai_assistant.chat.pending_phase3.body": "Diese interaktive Karte kommt in Phase 3 des vereinheitlichten KI-Frameworks.",
221
221
  "ai_assistant.chat.pending_phase3.title": "Mutations-Freigabe-Karte ausstehend",
222
222
  "ai_assistant.chat.placeholder": "Fragen Sie mich etwas...",
223
- "ai_assistant.chat.readOnlyNotice": "This is a shared conversation. You can read but not reply.",
223
+ "ai_assistant.chat.readOnlyNotice": "Dies ist eine geteilte Konversation. Sie können lesen, aber nicht antworten.",
224
224
  "ai_assistant.chat.reasoning": "Reasoning",
225
225
  "ai_assistant.chat.records.fields.amount": "Betrag",
226
226
  "ai_assistant.chat.records.fields.category": "Kategorie",
@@ -284,6 +284,7 @@
284
284
  "ai_assistant.dock.left": "Links andocken",
285
285
  "ai_assistant.dock.minimize": "Minimieren",
286
286
  "ai_assistant.dock.right": "Rechts andocken",
287
+ "ai_assistant.launcher.composerPlaceholder": "Fragen Sie mich etwas…",
287
288
  "ai_assistant.launcher.dialogTitle": "AI assistants",
288
289
  "ai_assistant.launcher.dock.subtitle": "AI assistant",
289
290
  "ai_assistant.launcher.empty": "No assistants match your search.",
@@ -325,9 +326,9 @@
325
326
  "ai_assistant.modelPicker.triggerAriaLabel": "KI-Modell auswählen",
326
327
  "ai_assistant.modelPicker.useDefault": "Standard des Agenten verwenden",
327
328
  "ai_assistant.modelPicker.useDefaultWithModel": "Standard des Agenten verwenden: {{model}}",
328
- "ai_assistant.notifications.conversation_shared.body": "An AI conversation has been shared with you.",
329
- "ai_assistant.notifications.conversation_shared.title": "Conversation shared with you",
330
- "ai_assistant.notifications.conversation_shared.view_button": "View Conversation",
329
+ "ai_assistant.notifications.conversation_shared.body": "Eine KI-Konversation wurde mit Ihnen geteilt.",
330
+ "ai_assistant.notifications.conversation_shared.title": "Konversation mit Ihnen geteilt",
331
+ "ai_assistant.notifications.conversation_shared.view_button": "Konversation öffnen",
331
332
  "ai_assistant.playground.agentPickerLabel": "Agent",
332
333
  "ai_assistant.playground.chat.notSupportedBody": "Wählen Sie einen Agenten, dessen Ausführungsmodus \"chat\" ist, oder wechseln Sie zum Objekt-Modus-Tab.",
333
334
  "ai_assistant.playground.chat.notSupportedTitle": "Chat-Modus ist für diesen Agenten nicht verfügbar.",
@@ -433,17 +434,17 @@
433
434
  "ai_assistant.settings.visibilityEnabled": "Im Header sichtbar mit aktiviertem Cmd+J-Shortcut.",
434
435
  "ai_assistant.settings.visibilityTitle": "KI-Assistent",
435
436
  "ai_assistant.settings.visibilityToggleLabel": "Sichtbarkeit",
436
- "ai_assistant.share.addParticipant": "Add participant",
437
- "ai_assistant.share.allUsersAdded": "All users already added",
438
- "ai_assistant.share.dialogDescription": "Share this conversation with other users. They will get read-only access.",
439
- "ai_assistant.share.dialogTitle": "Share Conversation",
440
- "ai_assistant.share.noParticipants": "No participants yet. Add someone to share this conversation.",
441
- "ai_assistant.share.participantPlaceholder": "Search by user...",
442
- "ai_assistant.share.removeParticipant": "Remove",
443
- "ai_assistant.share.saved": "Saved",
444
- "ai_assistant.share.saving": "Saving...",
445
- "ai_assistant.share.selectUser": "Select a user...",
446
- "ai_assistant.share.shareButton": "Share",
437
+ "ai_assistant.share.addParticipant": "Teilnehmer hinzufügen",
438
+ "ai_assistant.share.allUsersAdded": "Alle Benutzer bereits hinzugefügt",
439
+ "ai_assistant.share.dialogDescription": "Teilen Sie diese Konversation mit anderen Benutzern. Sie erhalten nur Lesezugriff.",
440
+ "ai_assistant.share.dialogTitle": "Konversation teilen",
441
+ "ai_assistant.share.noParticipants": "Noch keine Teilnehmer. Fügen Sie jemanden hinzu, um diese Konversation zu teilen.",
442
+ "ai_assistant.share.participantPlaceholder": "Nach Benutzer suchen...",
443
+ "ai_assistant.share.removeParticipant": "Entfernen",
444
+ "ai_assistant.share.saved": "Gespeichert",
445
+ "ai_assistant.share.saving": "Wird gespeichert...",
446
+ "ai_assistant.share.selectUser": "Benutzer auswählen...",
447
+ "ai_assistant.share.shareButton": "Teilen",
447
448
  "ai_assistant.status.analyzing": "Anfrage wird analysiert...",
448
449
  "ai_assistant.status.executing": "Tools werden ausgeführt...",
449
450
  "ai_assistant.status.responding": "Antwort wird erstellt...",
@@ -284,6 +284,7 @@
284
284
  "ai_assistant.dock.left": "Dock Left",
285
285
  "ai_assistant.dock.minimize": "Minimize",
286
286
  "ai_assistant.dock.right": "Dock Right",
287
+ "ai_assistant.launcher.composerPlaceholder": "Ask anything…",
287
288
  "ai_assistant.launcher.dialogTitle": "AI assistants",
288
289
  "ai_assistant.launcher.dock.subtitle": "AI assistant",
289
290
  "ai_assistant.launcher.empty": "No assistants match your search.",
@@ -220,7 +220,7 @@
220
220
  "ai_assistant.chat.pending_phase3.body": "Esta tarjeta interactiva se integrará en la Fase 3 del marco unificado de IA.",
221
221
  "ai_assistant.chat.pending_phase3.title": "Tarjeta de aprobación de mutación pendiente",
222
222
  "ai_assistant.chat.placeholder": "Pregunte lo que necesite...",
223
- "ai_assistant.chat.readOnlyNotice": "This is a shared conversation. You can read but not reply.",
223
+ "ai_assistant.chat.readOnlyNotice": "Esta es una conversación compartida. Puede leerla, pero no puede responder.",
224
224
  "ai_assistant.chat.reasoning": "Reasoning",
225
225
  "ai_assistant.chat.records.fields.amount": "Importe",
226
226
  "ai_assistant.chat.records.fields.category": "Categoría",
@@ -284,6 +284,7 @@
284
284
  "ai_assistant.dock.left": "Anclar a la izquierda",
285
285
  "ai_assistant.dock.minimize": "Minimizar",
286
286
  "ai_assistant.dock.right": "Anclar a la derecha",
287
+ "ai_assistant.launcher.composerPlaceholder": "Pregunte lo que necesite…",
287
288
  "ai_assistant.launcher.dialogTitle": "AI assistants",
288
289
  "ai_assistant.launcher.dock.subtitle": "AI assistant",
289
290
  "ai_assistant.launcher.empty": "No assistants match your search.",
@@ -325,9 +326,9 @@
325
326
  "ai_assistant.modelPicker.triggerAriaLabel": "Selector de modelo de IA",
326
327
  "ai_assistant.modelPicker.useDefault": "Usar predeterminado",
327
328
  "ai_assistant.modelPicker.useDefaultWithModel": "Usar predeterminado del agente: {{model}}",
328
- "ai_assistant.notifications.conversation_shared.body": "An AI conversation has been shared with you.",
329
- "ai_assistant.notifications.conversation_shared.title": "Conversation shared with you",
330
- "ai_assistant.notifications.conversation_shared.view_button": "View Conversation",
329
+ "ai_assistant.notifications.conversation_shared.body": "Se ha compartido una conversación de IA con usted.",
330
+ "ai_assistant.notifications.conversation_shared.title": "Conversación compartida con usted",
331
+ "ai_assistant.notifications.conversation_shared.view_button": "Ver conversación",
331
332
  "ai_assistant.playground.agentPickerLabel": "Agente",
332
333
  "ai_assistant.playground.chat.notSupportedBody": "Elija un agente cuyo modo de ejecución sea \"chat\" o cambie a la pestaña de modo objeto.",
333
334
  "ai_assistant.playground.chat.notSupportedTitle": "El modo chat no está disponible para este agente.",
@@ -433,17 +434,17 @@
433
434
  "ai_assistant.settings.visibilityEnabled": "Habilitado",
434
435
  "ai_assistant.settings.visibilityTitle": "Visibilidad del asistente de IA",
435
436
  "ai_assistant.settings.visibilityToggleLabel": "Mostrar el asistente de IA a los usuarios",
436
- "ai_assistant.share.addParticipant": "Add participant",
437
- "ai_assistant.share.allUsersAdded": "All users already added",
438
- "ai_assistant.share.dialogDescription": "Share this conversation with other users. They will get read-only access.",
439
- "ai_assistant.share.dialogTitle": "Share Conversation",
440
- "ai_assistant.share.noParticipants": "No participants yet. Add someone to share this conversation.",
441
- "ai_assistant.share.participantPlaceholder": "Search by user...",
442
- "ai_assistant.share.removeParticipant": "Remove",
443
- "ai_assistant.share.saved": "Saved",
444
- "ai_assistant.share.saving": "Saving...",
445
- "ai_assistant.share.selectUser": "Select a user...",
446
- "ai_assistant.share.shareButton": "Share",
437
+ "ai_assistant.share.addParticipant": "Añadir participante",
438
+ "ai_assistant.share.allUsersAdded": "Todos los usuarios ya añadidos",
439
+ "ai_assistant.share.dialogDescription": "Comparta esta conversación con otros usuarios. Obtendrán acceso de solo lectura.",
440
+ "ai_assistant.share.dialogTitle": "Compartir conversación",
441
+ "ai_assistant.share.noParticipants": "Aún no hay participantes. Añada a alguien para compartir esta conversación.",
442
+ "ai_assistant.share.participantPlaceholder": "Buscar usuario...",
443
+ "ai_assistant.share.removeParticipant": "Quitar",
444
+ "ai_assistant.share.saved": "Guardado",
445
+ "ai_assistant.share.saving": "Guardando...",
446
+ "ai_assistant.share.selectUser": "Seleccione un usuario...",
447
+ "ai_assistant.share.shareButton": "Compartir",
447
448
  "ai_assistant.status.analyzing": "Analizando solicitud...",
448
449
  "ai_assistant.status.executing": "Ejecutando herramientas...",
449
450
  "ai_assistant.status.responding": "Respondiendo...",
@@ -220,7 +220,7 @@
220
220
  "ai_assistant.chat.pending_phase3.body": "Ta interaktywna karta pojawi się w Fazie 3 zunifikowanego frameworku AI.",
221
221
  "ai_assistant.chat.pending_phase3.title": "Karta zatwierdzania mutacji oczekuje",
222
222
  "ai_assistant.chat.placeholder": "Zapytaj mnie o cokolwiek...",
223
- "ai_assistant.chat.readOnlyNotice": "This is a shared conversation. You can read but not reply.",
223
+ "ai_assistant.chat.readOnlyNotice": "To jest udostępniona konwersacja. Możesz czytać, ale nie możesz odpowiadać.",
224
224
  "ai_assistant.chat.reasoning": "Reasoning",
225
225
  "ai_assistant.chat.records.fields.amount": "Kwota",
226
226
  "ai_assistant.chat.records.fields.category": "Kategoria",
@@ -284,6 +284,7 @@
284
284
  "ai_assistant.dock.left": "Dokuj po lewej",
285
285
  "ai_assistant.dock.minimize": "Minimalizuj",
286
286
  "ai_assistant.dock.right": "Dokuj po prawej",
287
+ "ai_assistant.launcher.composerPlaceholder": "Zapytaj o cokolwiek…",
287
288
  "ai_assistant.launcher.dialogTitle": "AI assistants",
288
289
  "ai_assistant.launcher.dock.subtitle": "AI assistant",
289
290
  "ai_assistant.launcher.empty": "No assistants match your search.",
@@ -325,9 +326,9 @@
325
326
  "ai_assistant.modelPicker.triggerAriaLabel": "Wybierz model AI",
326
327
  "ai_assistant.modelPicker.useDefault": "Użyj domyślnego agenta",
327
328
  "ai_assistant.modelPicker.useDefaultWithModel": "Użyj domyślnego agenta: {{model}}",
328
- "ai_assistant.notifications.conversation_shared.body": "An AI conversation has been shared with you.",
329
- "ai_assistant.notifications.conversation_shared.title": "Conversation shared with you",
330
- "ai_assistant.notifications.conversation_shared.view_button": "View Conversation",
329
+ "ai_assistant.notifications.conversation_shared.body": "Udostępniono Ci konwersację z asystentem AI.",
330
+ "ai_assistant.notifications.conversation_shared.title": "Udostępniono Ci konwersację",
331
+ "ai_assistant.notifications.conversation_shared.view_button": "Otwórz konwersację",
331
332
  "ai_assistant.playground.agentPickerLabel": "Agent",
332
333
  "ai_assistant.playground.chat.notSupportedBody": "Wybierz agenta, którego tryb wykonywania to \"chat\", lub przełącz się na zakładkę trybu obiektowego.",
333
334
  "ai_assistant.playground.chat.notSupportedTitle": "Tryb czatu nie jest dostępny dla tego agenta.",
@@ -433,17 +434,17 @@
433
434
  "ai_assistant.settings.visibilityEnabled": "Widoczny w nagłówku ze skrótem Cmd+J.",
434
435
  "ai_assistant.settings.visibilityTitle": "Asystent AI",
435
436
  "ai_assistant.settings.visibilityToggleLabel": "Widoczność",
436
- "ai_assistant.share.addParticipant": "Add participant",
437
- "ai_assistant.share.allUsersAdded": "All users already added",
438
- "ai_assistant.share.dialogDescription": "Share this conversation with other users. They will get read-only access.",
439
- "ai_assistant.share.dialogTitle": "Share Conversation",
440
- "ai_assistant.share.noParticipants": "No participants yet. Add someone to share this conversation.",
441
- "ai_assistant.share.participantPlaceholder": "Search by user...",
442
- "ai_assistant.share.removeParticipant": "Remove",
443
- "ai_assistant.share.saved": "Saved",
444
- "ai_assistant.share.saving": "Saving...",
445
- "ai_assistant.share.selectUser": "Select a user...",
446
- "ai_assistant.share.shareButton": "Share",
437
+ "ai_assistant.share.addParticipant": "Dodaj uczestnika",
438
+ "ai_assistant.share.allUsersAdded": "Dodano już wszystkich użytkowników",
439
+ "ai_assistant.share.dialogDescription": "Udostępnij konwersację innym użytkownikom. Otrzymają dostęp tylko do odczytu.",
440
+ "ai_assistant.share.dialogTitle": "Udostępnij konwersację",
441
+ "ai_assistant.share.noParticipants": "Brak uczestników. Dodaj kogoś, aby udostępnić konwersację.",
442
+ "ai_assistant.share.participantPlaceholder": "Szukaj użytkownika...",
443
+ "ai_assistant.share.removeParticipant": "Usuń",
444
+ "ai_assistant.share.saved": "Zapisano",
445
+ "ai_assistant.share.saving": "Zapisywanie...",
446
+ "ai_assistant.share.selectUser": "Wybierz użytkownika...",
447
+ "ai_assistant.share.shareButton": "Udostępnij",
447
448
  "ai_assistant.status.analyzing": "Analizowanie żądania...",
448
449
  "ai_assistant.status.executing": "Wykonywanie narzędzi...",
449
450
  "ai_assistant.status.responding": "Odpowiadanie...",
@@ -1,11 +1,27 @@
1
1
  import { resolveNotificationService } from "@open-mercato/core/modules/notifications/lib/notificationService";
2
2
  import { buildNotificationFromType } from "@open-mercato/core/modules/notifications/lib/notificationBuilder";
3
+ import { defaultLocale } from "@open-mercato/shared/lib/i18n/config";
4
+ import { loadDictionary } from "@open-mercato/shared/lib/i18n/server";
5
+ import { createTranslator } from "@open-mercato/shared/lib/i18n/translate";
3
6
  import { notificationTypes } from "../notifications.js";
4
7
  const metadata = {
5
8
  event: "ai_assistant.conversation.shared",
6
9
  persistent: true,
7
10
  id: "ai_assistant:conversation-shared-notify"
8
11
  };
12
+ async function resolveDefaultLocaleStrings(titleKey, bodyKey) {
13
+ if (!titleKey && !bodyKey) return { title: titleKey, body: bodyKey };
14
+ try {
15
+ const dict = await loadDictionary(defaultLocale);
16
+ const t = createTranslator(dict);
17
+ return {
18
+ title: titleKey ? t(titleKey) : titleKey,
19
+ body: bodyKey ? t(bodyKey) : bodyKey
20
+ };
21
+ } catch {
22
+ return { title: titleKey, body: bodyKey };
23
+ }
24
+ }
9
25
  async function handleConversationShared(payload, ctx) {
10
26
  if (!payload?.participantUserId || !payload.tenantId) return;
11
27
  const typeDef = notificationTypes.find((t) => t.type === "ai_assistant.conversation_shared");
@@ -24,6 +40,9 @@ async function handleConversationShared(payload, ctx) {
24
40
  sourceEntityId: payload.conversationId,
25
41
  linkHref: `/backend?openAiConversation=${payload.conversationId}`
26
42
  });
43
+ const resolved = await resolveDefaultLocaleStrings(typeDef.titleKey, typeDef.bodyKey);
44
+ if (resolved.title !== void 0) notificationInput.title = resolved.title;
45
+ if (resolved.body !== void 0) notificationInput.body = resolved.body;
27
46
  try {
28
47
  await notificationService.create(notificationInput, {
29
48
  tenantId: payload.tenantId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/subscribers/conversation-shared-notify.ts"],
4
- "sourcesContent": ["import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { notificationTypes } from '../notifications'\nimport type { AiConversationSharedPayload } from '../events'\n\nexport const metadata = {\n event: 'ai_assistant.conversation.shared',\n persistent: true,\n id: 'ai_assistant:conversation-shared-notify',\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n container?: { resolve<T = unknown>(name: string): T }\n}\n\nexport default async function handleConversationShared(\n payload: AiConversationSharedPayload,\n ctx: ResolverContext,\n): Promise<void> {\n if (!payload?.participantUserId || !payload.tenantId) return\n\n const typeDef = notificationTypes.find((t) => t.type === 'ai_assistant.conversation_shared')\n if (!typeDef) return\n\n const container = ctx.container ?? { resolve: ctx.resolve }\n let notificationService: ReturnType<typeof resolveNotificationService> | null\n try {\n notificationService = resolveNotificationService(container)\n } catch {\n return\n }\n\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: payload.participantUserId,\n bodyVariables: {},\n sourceEntityType: 'ai_assistant:ai_chat_conversation',\n sourceEntityId: payload.conversationId,\n linkHref: `/backend?openAiConversation=${payload.conversationId}`,\n })\n\n try {\n await notificationService.create(notificationInput, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n } catch (err) {\n console.warn('[ai_assistant.conversationSharedNotify] create failed', err)\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,kCAAkC;AAC3C,SAAS,iCAAiC;AAC1C,SAAS,yBAAyB;AAG3B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAOA,eAAO,yBACL,SACA,KACe;AACf,MAAI,CAAC,SAAS,qBAAqB,CAAC,QAAQ,SAAU;AAEtD,QAAM,UAAU,kBAAkB,KAAK,CAAC,MAAM,EAAE,SAAS,kCAAkC;AAC3F,MAAI,CAAC,QAAS;AAEd,QAAM,YAAY,IAAI,aAAa,EAAE,SAAS,IAAI,QAAQ;AAC1D,MAAI;AACJ,MAAI;AACF,0BAAsB,2BAA2B,SAAS;AAAA,EAC5D,QAAQ;AACN;AAAA,EACF;AAEA,QAAM,oBAAoB,0BAA0B,SAAS;AAAA,IAC3D,iBAAiB,QAAQ;AAAA,IACzB,eAAe,CAAC;AAAA,IAChB,kBAAkB;AAAA,IAClB,gBAAgB,QAAQ;AAAA,IACxB,UAAU,+BAA+B,QAAQ,cAAc;AAAA,EACjE,CAAC;AAED,MAAI;AACF,UAAM,oBAAoB,OAAO,mBAAmB;AAAA,MAClD,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ,kBAAkB;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,YAAQ,KAAK,yDAAyD,GAAG;AAAA,EAC3E;AACF;",
4
+ "sourcesContent": ["import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { defaultLocale } from '@open-mercato/shared/lib/i18n/config'\nimport { loadDictionary } from '@open-mercato/shared/lib/i18n/server'\nimport { createTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport { notificationTypes } from '../notifications'\nimport type { AiConversationSharedPayload } from '../events'\n\nexport const metadata = {\n event: 'ai_assistant.conversation.shared',\n persistent: true,\n id: 'ai_assistant:conversation-shared-notify',\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n container?: { resolve<T = unknown>(name: string): T }\n}\n\nasync function resolveDefaultLocaleStrings(\n titleKey: string | undefined,\n bodyKey: string | undefined,\n): Promise<{ title: string | undefined; body: string | undefined }> {\n if (!titleKey && !bodyKey) return { title: titleKey, body: bodyKey }\n try {\n const dict = await loadDictionary(defaultLocale)\n const t = createTranslator(dict)\n return {\n title: titleKey ? t(titleKey) : titleKey,\n body: bodyKey ? t(bodyKey) : bodyKey,\n }\n } catch {\n return { title: titleKey, body: bodyKey }\n }\n}\n\nexport default async function handleConversationShared(\n payload: AiConversationSharedPayload,\n ctx: ResolverContext,\n): Promise<void> {\n if (!payload?.participantUserId || !payload.tenantId) return\n\n const typeDef = notificationTypes.find((t) => t.type === 'ai_assistant.conversation_shared')\n if (!typeDef) return\n\n const container = ctx.container ?? { resolve: ctx.resolve }\n let notificationService: ReturnType<typeof resolveNotificationService> | null\n try {\n notificationService = resolveNotificationService(container)\n } catch {\n return\n }\n\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: payload.participantUserId,\n bodyVariables: {},\n sourceEntityType: 'ai_assistant:ai_chat_conversation',\n sourceEntityId: payload.conversationId,\n linkHref: `/backend?openAiConversation=${payload.conversationId}`,\n })\n\n // Persist a human-readable fallback alongside the i18n keys so consumers\n // that do not run the client renderer (email digests, exports) display a\n // resolved string instead of the raw key. The client renderer continues\n // to re-translate via titleKey/bodyKey for the viewer's locale.\n const resolved = await resolveDefaultLocaleStrings(typeDef.titleKey, typeDef.bodyKey)\n if (resolved.title !== undefined) notificationInput.title = resolved.title\n if (resolved.body !== undefined) notificationInput.body = resolved.body\n\n try {\n await notificationService.create(notificationInput, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n } catch (err) {\n console.warn('[ai_assistant.conversationSharedNotify] create failed', err)\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,kCAAkC;AAC3C,SAAS,iCAAiC;AAC1C,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC,SAAS,yBAAyB;AAG3B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAOA,eAAe,4BACb,UACA,SACkE;AAClE,MAAI,CAAC,YAAY,CAAC,QAAS,QAAO,EAAE,OAAO,UAAU,MAAM,QAAQ;AACnE,MAAI;AACF,UAAM,OAAO,MAAM,eAAe,aAAa;AAC/C,UAAM,IAAI,iBAAiB,IAAI;AAC/B,WAAO;AAAA,MACL,OAAO,WAAW,EAAE,QAAQ,IAAI;AAAA,MAChC,MAAM,UAAU,EAAE,OAAO,IAAI;AAAA,IAC/B;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,OAAO,UAAU,MAAM,QAAQ;AAAA,EAC1C;AACF;AAEA,eAAO,yBACL,SACA,KACe;AACf,MAAI,CAAC,SAAS,qBAAqB,CAAC,QAAQ,SAAU;AAEtD,QAAM,UAAU,kBAAkB,KAAK,CAAC,MAAM,EAAE,SAAS,kCAAkC;AAC3F,MAAI,CAAC,QAAS;AAEd,QAAM,YAAY,IAAI,aAAa,EAAE,SAAS,IAAI,QAAQ;AAC1D,MAAI;AACJ,MAAI;AACF,0BAAsB,2BAA2B,SAAS;AAAA,EAC5D,QAAQ;AACN;AAAA,EACF;AAEA,QAAM,oBAAoB,0BAA0B,SAAS;AAAA,IAC3D,iBAAiB,QAAQ;AAAA,IACzB,eAAe,CAAC;AAAA,IAChB,kBAAkB;AAAA,IAClB,gBAAgB,QAAQ;AAAA,IACxB,UAAU,+BAA+B,QAAQ,cAAc;AAAA,EACjE,CAAC;AAMD,QAAM,WAAW,MAAM,4BAA4B,QAAQ,UAAU,QAAQ,OAAO;AACpF,MAAI,SAAS,UAAU,OAAW,mBAAkB,QAAQ,SAAS;AACrE,MAAI,SAAS,SAAS,OAAW,mBAAkB,OAAO,SAAS;AAEnE,MAAI;AACF,UAAM,oBAAoB,OAAO,mBAAmB;AAAA,MAClD,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ,kBAAkB;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,YAAQ,KAAK,yDAAyD,GAAG;AAAA,EAC3E;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.6.4-develop.3962.1.70f30e284c",
3
+ "version": "0.6.4-develop.3976.1.4e72e60206",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -98,16 +98,16 @@
98
98
  "zod-to-json-schema": "^3.25.2"
99
99
  },
100
100
  "peerDependencies": {
101
- "@open-mercato/shared": "0.6.4-develop.3962.1.70f30e284c",
102
- "@open-mercato/ui": "0.6.4-develop.3962.1.70f30e284c",
101
+ "@open-mercato/shared": "0.6.4-develop.3976.1.4e72e60206",
102
+ "@open-mercato/ui": "0.6.4-develop.3976.1.4e72e60206",
103
103
  "react": "^19.0.0",
104
104
  "react-dom": "^19.0.0",
105
105
  "zod": ">=3.23.0"
106
106
  },
107
107
  "devDependencies": {
108
- "@open-mercato/cli": "0.6.4-develop.3962.1.70f30e284c",
109
- "@open-mercato/shared": "0.6.4-develop.3962.1.70f30e284c",
110
- "@open-mercato/ui": "0.6.4-develop.3962.1.70f30e284c",
108
+ "@open-mercato/cli": "0.6.4-develop.3976.1.4e72e60206",
109
+ "@open-mercato/shared": "0.6.4-develop.3976.1.4e72e60206",
110
+ "@open-mercato/ui": "0.6.4-develop.3976.1.4e72e60206",
111
111
  "@types/react": "^19.2.15",
112
112
  "@types/react-dom": "^19.2.3",
113
113
  "react": "19.2.6",
@@ -168,13 +168,19 @@ export async function GET(req: NextRequest, context: RouteContext): Promise<Resp
168
168
  try {
169
169
  const container = await createRequestContainer()
170
170
  const repo = createConversationStorage(container)
171
- const participants = await repo.listParticipants(callerCtx.conversationId, {
171
+ const repoCtx = {
172
172
  tenantId: callerCtx.tenantId,
173
173
  organizationId: callerCtx.organizationId,
174
174
  userId: callerCtx.userId,
175
175
  canManageConversations: callerCtx.canManageConversations,
176
- })
176
+ }
177
+ const conversation = await repo.getById(callerCtx.conversationId, repoCtx)
178
+ if (!conversation) {
179
+ return jsonError(404, 'Conversation not found.', 'conversation_not_found')
180
+ }
181
+ const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx)
177
182
  return NextResponse.json({
183
+ ownerUserId: conversation.ownerUserId,
178
184
  participants: participants.map((p) => ({
179
185
  userId: p.userId,
180
186
  role: p.role,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Regression coverage for issue #2097 (BUG-004): the conversation-sharing
3
+ * UI and notification strings MUST ship localized values in every supported
4
+ * locale, not the English copy. Catches drift when keys are renamed/added
5
+ * but their PL/DE/ES values are forgotten.
6
+ */
7
+
8
+ import enDict from '../en.json'
9
+ import plDict from '../pl.json'
10
+ import deDict from '../de.json'
11
+ import esDict from '../es.json'
12
+
13
+ type Dict = Record<string, string>
14
+
15
+ const KEYS_TO_LOCALIZE = [
16
+ 'ai_assistant.chat.readOnlyNotice',
17
+ 'ai_assistant.launcher.composerPlaceholder',
18
+ 'ai_assistant.notifications.conversation_shared.title',
19
+ 'ai_assistant.notifications.conversation_shared.body',
20
+ 'ai_assistant.notifications.conversation_shared.view_button',
21
+ 'ai_assistant.share.addParticipant',
22
+ 'ai_assistant.share.allUsersAdded',
23
+ 'ai_assistant.share.dialogDescription',
24
+ 'ai_assistant.share.dialogTitle',
25
+ 'ai_assistant.share.noParticipants',
26
+ 'ai_assistant.share.participantPlaceholder',
27
+ 'ai_assistant.share.removeParticipant',
28
+ 'ai_assistant.share.saved',
29
+ 'ai_assistant.share.saving',
30
+ 'ai_assistant.share.selectUser',
31
+ 'ai_assistant.share.shareButton',
32
+ ]
33
+
34
+ const LOCALE_DICTS: Array<[string, Dict]> = [
35
+ ['pl', plDict as Dict],
36
+ ['de', deDict as Dict],
37
+ ['es', esDict as Dict],
38
+ ]
39
+
40
+ describe('ai_assistant conversation-share i18n keys', () => {
41
+ it.each(KEYS_TO_LOCALIZE)('en.json defines %s', (key) => {
42
+ expect((enDict as Dict)[key]).toBeTruthy()
43
+ })
44
+
45
+ for (const [locale, dict] of LOCALE_DICTS) {
46
+ describe(`${locale}.json`, () => {
47
+ it.each(KEYS_TO_LOCALIZE)('defines %s', (key) => {
48
+ expect(dict[key]).toBeTruthy()
49
+ })
50
+
51
+ it.each(KEYS_TO_LOCALIZE)('localizes %s (value differs from en)', (key) => {
52
+ const enValue = (enDict as Dict)[key]
53
+ const localizedValue = dict[key]
54
+ expect(localizedValue).toBeTruthy()
55
+ expect(localizedValue).not.toBe(enValue)
56
+ })
57
+ })
58
+ }
59
+ })
@@ -220,7 +220,7 @@
220
220
  "ai_assistant.chat.pending_phase3.body": "Diese interaktive Karte kommt in Phase 3 des vereinheitlichten KI-Frameworks.",
221
221
  "ai_assistant.chat.pending_phase3.title": "Mutations-Freigabe-Karte ausstehend",
222
222
  "ai_assistant.chat.placeholder": "Fragen Sie mich etwas...",
223
- "ai_assistant.chat.readOnlyNotice": "This is a shared conversation. You can read but not reply.",
223
+ "ai_assistant.chat.readOnlyNotice": "Dies ist eine geteilte Konversation. Sie können lesen, aber nicht antworten.",
224
224
  "ai_assistant.chat.reasoning": "Reasoning",
225
225
  "ai_assistant.chat.records.fields.amount": "Betrag",
226
226
  "ai_assistant.chat.records.fields.category": "Kategorie",
@@ -284,6 +284,7 @@
284
284
  "ai_assistant.dock.left": "Links andocken",
285
285
  "ai_assistant.dock.minimize": "Minimieren",
286
286
  "ai_assistant.dock.right": "Rechts andocken",
287
+ "ai_assistant.launcher.composerPlaceholder": "Fragen Sie mich etwas…",
287
288
  "ai_assistant.launcher.dialogTitle": "AI assistants",
288
289
  "ai_assistant.launcher.dock.subtitle": "AI assistant",
289
290
  "ai_assistant.launcher.empty": "No assistants match your search.",
@@ -325,9 +326,9 @@
325
326
  "ai_assistant.modelPicker.triggerAriaLabel": "KI-Modell auswählen",
326
327
  "ai_assistant.modelPicker.useDefault": "Standard des Agenten verwenden",
327
328
  "ai_assistant.modelPicker.useDefaultWithModel": "Standard des Agenten verwenden: {{model}}",
328
- "ai_assistant.notifications.conversation_shared.body": "An AI conversation has been shared with you.",
329
- "ai_assistant.notifications.conversation_shared.title": "Conversation shared with you",
330
- "ai_assistant.notifications.conversation_shared.view_button": "View Conversation",
329
+ "ai_assistant.notifications.conversation_shared.body": "Eine KI-Konversation wurde mit Ihnen geteilt.",
330
+ "ai_assistant.notifications.conversation_shared.title": "Konversation mit Ihnen geteilt",
331
+ "ai_assistant.notifications.conversation_shared.view_button": "Konversation öffnen",
331
332
  "ai_assistant.playground.agentPickerLabel": "Agent",
332
333
  "ai_assistant.playground.chat.notSupportedBody": "Wählen Sie einen Agenten, dessen Ausführungsmodus \"chat\" ist, oder wechseln Sie zum Objekt-Modus-Tab.",
333
334
  "ai_assistant.playground.chat.notSupportedTitle": "Chat-Modus ist für diesen Agenten nicht verfügbar.",
@@ -433,17 +434,17 @@
433
434
  "ai_assistant.settings.visibilityEnabled": "Im Header sichtbar mit aktiviertem Cmd+J-Shortcut.",
434
435
  "ai_assistant.settings.visibilityTitle": "KI-Assistent",
435
436
  "ai_assistant.settings.visibilityToggleLabel": "Sichtbarkeit",
436
- "ai_assistant.share.addParticipant": "Add participant",
437
- "ai_assistant.share.allUsersAdded": "All users already added",
438
- "ai_assistant.share.dialogDescription": "Share this conversation with other users. They will get read-only access.",
439
- "ai_assistant.share.dialogTitle": "Share Conversation",
440
- "ai_assistant.share.noParticipants": "No participants yet. Add someone to share this conversation.",
441
- "ai_assistant.share.participantPlaceholder": "Search by user...",
442
- "ai_assistant.share.removeParticipant": "Remove",
443
- "ai_assistant.share.saved": "Saved",
444
- "ai_assistant.share.saving": "Saving...",
445
- "ai_assistant.share.selectUser": "Select a user...",
446
- "ai_assistant.share.shareButton": "Share",
437
+ "ai_assistant.share.addParticipant": "Teilnehmer hinzufügen",
438
+ "ai_assistant.share.allUsersAdded": "Alle Benutzer bereits hinzugefügt",
439
+ "ai_assistant.share.dialogDescription": "Teilen Sie diese Konversation mit anderen Benutzern. Sie erhalten nur Lesezugriff.",
440
+ "ai_assistant.share.dialogTitle": "Konversation teilen",
441
+ "ai_assistant.share.noParticipants": "Noch keine Teilnehmer. Fügen Sie jemanden hinzu, um diese Konversation zu teilen.",
442
+ "ai_assistant.share.participantPlaceholder": "Nach Benutzer suchen...",
443
+ "ai_assistant.share.removeParticipant": "Entfernen",
444
+ "ai_assistant.share.saved": "Gespeichert",
445
+ "ai_assistant.share.saving": "Wird gespeichert...",
446
+ "ai_assistant.share.selectUser": "Benutzer auswählen...",
447
+ "ai_assistant.share.shareButton": "Teilen",
447
448
  "ai_assistant.status.analyzing": "Anfrage wird analysiert...",
448
449
  "ai_assistant.status.executing": "Tools werden ausgeführt...",
449
450
  "ai_assistant.status.responding": "Antwort wird erstellt...",
@@ -284,6 +284,7 @@
284
284
  "ai_assistant.dock.left": "Dock Left",
285
285
  "ai_assistant.dock.minimize": "Minimize",
286
286
  "ai_assistant.dock.right": "Dock Right",
287
+ "ai_assistant.launcher.composerPlaceholder": "Ask anything…",
287
288
  "ai_assistant.launcher.dialogTitle": "AI assistants",
288
289
  "ai_assistant.launcher.dock.subtitle": "AI assistant",
289
290
  "ai_assistant.launcher.empty": "No assistants match your search.",
@@ -220,7 +220,7 @@
220
220
  "ai_assistant.chat.pending_phase3.body": "Esta tarjeta interactiva se integrará en la Fase 3 del marco unificado de IA.",
221
221
  "ai_assistant.chat.pending_phase3.title": "Tarjeta de aprobación de mutación pendiente",
222
222
  "ai_assistant.chat.placeholder": "Pregunte lo que necesite...",
223
- "ai_assistant.chat.readOnlyNotice": "This is a shared conversation. You can read but not reply.",
223
+ "ai_assistant.chat.readOnlyNotice": "Esta es una conversación compartida. Puede leerla, pero no puede responder.",
224
224
  "ai_assistant.chat.reasoning": "Reasoning",
225
225
  "ai_assistant.chat.records.fields.amount": "Importe",
226
226
  "ai_assistant.chat.records.fields.category": "Categoría",
@@ -284,6 +284,7 @@
284
284
  "ai_assistant.dock.left": "Anclar a la izquierda",
285
285
  "ai_assistant.dock.minimize": "Minimizar",
286
286
  "ai_assistant.dock.right": "Anclar a la derecha",
287
+ "ai_assistant.launcher.composerPlaceholder": "Pregunte lo que necesite…",
287
288
  "ai_assistant.launcher.dialogTitle": "AI assistants",
288
289
  "ai_assistant.launcher.dock.subtitle": "AI assistant",
289
290
  "ai_assistant.launcher.empty": "No assistants match your search.",
@@ -325,9 +326,9 @@
325
326
  "ai_assistant.modelPicker.triggerAriaLabel": "Selector de modelo de IA",
326
327
  "ai_assistant.modelPicker.useDefault": "Usar predeterminado",
327
328
  "ai_assistant.modelPicker.useDefaultWithModel": "Usar predeterminado del agente: {{model}}",
328
- "ai_assistant.notifications.conversation_shared.body": "An AI conversation has been shared with you.",
329
- "ai_assistant.notifications.conversation_shared.title": "Conversation shared with you",
330
- "ai_assistant.notifications.conversation_shared.view_button": "View Conversation",
329
+ "ai_assistant.notifications.conversation_shared.body": "Se ha compartido una conversación de IA con usted.",
330
+ "ai_assistant.notifications.conversation_shared.title": "Conversación compartida con usted",
331
+ "ai_assistant.notifications.conversation_shared.view_button": "Ver conversación",
331
332
  "ai_assistant.playground.agentPickerLabel": "Agente",
332
333
  "ai_assistant.playground.chat.notSupportedBody": "Elija un agente cuyo modo de ejecución sea \"chat\" o cambie a la pestaña de modo objeto.",
333
334
  "ai_assistant.playground.chat.notSupportedTitle": "El modo chat no está disponible para este agente.",
@@ -433,17 +434,17 @@
433
434
  "ai_assistant.settings.visibilityEnabled": "Habilitado",
434
435
  "ai_assistant.settings.visibilityTitle": "Visibilidad del asistente de IA",
435
436
  "ai_assistant.settings.visibilityToggleLabel": "Mostrar el asistente de IA a los usuarios",
436
- "ai_assistant.share.addParticipant": "Add participant",
437
- "ai_assistant.share.allUsersAdded": "All users already added",
438
- "ai_assistant.share.dialogDescription": "Share this conversation with other users. They will get read-only access.",
439
- "ai_assistant.share.dialogTitle": "Share Conversation",
440
- "ai_assistant.share.noParticipants": "No participants yet. Add someone to share this conversation.",
441
- "ai_assistant.share.participantPlaceholder": "Search by user...",
442
- "ai_assistant.share.removeParticipant": "Remove",
443
- "ai_assistant.share.saved": "Saved",
444
- "ai_assistant.share.saving": "Saving...",
445
- "ai_assistant.share.selectUser": "Select a user...",
446
- "ai_assistant.share.shareButton": "Share",
437
+ "ai_assistant.share.addParticipant": "Añadir participante",
438
+ "ai_assistant.share.allUsersAdded": "Todos los usuarios ya añadidos",
439
+ "ai_assistant.share.dialogDescription": "Comparta esta conversación con otros usuarios. Obtendrán acceso de solo lectura.",
440
+ "ai_assistant.share.dialogTitle": "Compartir conversación",
441
+ "ai_assistant.share.noParticipants": "Aún no hay participantes. Añada a alguien para compartir esta conversación.",
442
+ "ai_assistant.share.participantPlaceholder": "Buscar usuario...",
443
+ "ai_assistant.share.removeParticipant": "Quitar",
444
+ "ai_assistant.share.saved": "Guardado",
445
+ "ai_assistant.share.saving": "Guardando...",
446
+ "ai_assistant.share.selectUser": "Seleccione un usuario...",
447
+ "ai_assistant.share.shareButton": "Compartir",
447
448
  "ai_assistant.status.analyzing": "Analizando solicitud...",
448
449
  "ai_assistant.status.executing": "Ejecutando herramientas...",
449
450
  "ai_assistant.status.responding": "Respondiendo...",
@@ -220,7 +220,7 @@
220
220
  "ai_assistant.chat.pending_phase3.body": "Ta interaktywna karta pojawi się w Fazie 3 zunifikowanego frameworku AI.",
221
221
  "ai_assistant.chat.pending_phase3.title": "Karta zatwierdzania mutacji oczekuje",
222
222
  "ai_assistant.chat.placeholder": "Zapytaj mnie o cokolwiek...",
223
- "ai_assistant.chat.readOnlyNotice": "This is a shared conversation. You can read but not reply.",
223
+ "ai_assistant.chat.readOnlyNotice": "To jest udostępniona konwersacja. Możesz czytać, ale nie możesz odpowiadać.",
224
224
  "ai_assistant.chat.reasoning": "Reasoning",
225
225
  "ai_assistant.chat.records.fields.amount": "Kwota",
226
226
  "ai_assistant.chat.records.fields.category": "Kategoria",
@@ -284,6 +284,7 @@
284
284
  "ai_assistant.dock.left": "Dokuj po lewej",
285
285
  "ai_assistant.dock.minimize": "Minimalizuj",
286
286
  "ai_assistant.dock.right": "Dokuj po prawej",
287
+ "ai_assistant.launcher.composerPlaceholder": "Zapytaj o cokolwiek…",
287
288
  "ai_assistant.launcher.dialogTitle": "AI assistants",
288
289
  "ai_assistant.launcher.dock.subtitle": "AI assistant",
289
290
  "ai_assistant.launcher.empty": "No assistants match your search.",
@@ -325,9 +326,9 @@
325
326
  "ai_assistant.modelPicker.triggerAriaLabel": "Wybierz model AI",
326
327
  "ai_assistant.modelPicker.useDefault": "Użyj domyślnego agenta",
327
328
  "ai_assistant.modelPicker.useDefaultWithModel": "Użyj domyślnego agenta: {{model}}",
328
- "ai_assistant.notifications.conversation_shared.body": "An AI conversation has been shared with you.",
329
- "ai_assistant.notifications.conversation_shared.title": "Conversation shared with you",
330
- "ai_assistant.notifications.conversation_shared.view_button": "View Conversation",
329
+ "ai_assistant.notifications.conversation_shared.body": "Udostępniono Ci konwersację z asystentem AI.",
330
+ "ai_assistant.notifications.conversation_shared.title": "Udostępniono Ci konwersację",
331
+ "ai_assistant.notifications.conversation_shared.view_button": "Otwórz konwersację",
331
332
  "ai_assistant.playground.agentPickerLabel": "Agent",
332
333
  "ai_assistant.playground.chat.notSupportedBody": "Wybierz agenta, którego tryb wykonywania to \"chat\", lub przełącz się na zakładkę trybu obiektowego.",
333
334
  "ai_assistant.playground.chat.notSupportedTitle": "Tryb czatu nie jest dostępny dla tego agenta.",
@@ -433,17 +434,17 @@
433
434
  "ai_assistant.settings.visibilityEnabled": "Widoczny w nagłówku ze skrótem Cmd+J.",
434
435
  "ai_assistant.settings.visibilityTitle": "Asystent AI",
435
436
  "ai_assistant.settings.visibilityToggleLabel": "Widoczność",
436
- "ai_assistant.share.addParticipant": "Add participant",
437
- "ai_assistant.share.allUsersAdded": "All users already added",
438
- "ai_assistant.share.dialogDescription": "Share this conversation with other users. They will get read-only access.",
439
- "ai_assistant.share.dialogTitle": "Share Conversation",
440
- "ai_assistant.share.noParticipants": "No participants yet. Add someone to share this conversation.",
441
- "ai_assistant.share.participantPlaceholder": "Search by user...",
442
- "ai_assistant.share.removeParticipant": "Remove",
443
- "ai_assistant.share.saved": "Saved",
444
- "ai_assistant.share.saving": "Saving...",
445
- "ai_assistant.share.selectUser": "Select a user...",
446
- "ai_assistant.share.shareButton": "Share",
437
+ "ai_assistant.share.addParticipant": "Dodaj uczestnika",
438
+ "ai_assistant.share.allUsersAdded": "Dodano już wszystkich użytkowników",
439
+ "ai_assistant.share.dialogDescription": "Udostępnij konwersację innym użytkownikom. Otrzymają dostęp tylko do odczytu.",
440
+ "ai_assistant.share.dialogTitle": "Udostępnij konwersację",
441
+ "ai_assistant.share.noParticipants": "Brak uczestników. Dodaj kogoś, aby udostępnić konwersację.",
442
+ "ai_assistant.share.participantPlaceholder": "Szukaj użytkownika...",
443
+ "ai_assistant.share.removeParticipant": "Usuń",
444
+ "ai_assistant.share.saved": "Zapisano",
445
+ "ai_assistant.share.saving": "Zapisywanie...",
446
+ "ai_assistant.share.selectUser": "Wybierz użytkownika...",
447
+ "ai_assistant.share.shareButton": "Udostępnij",
447
448
  "ai_assistant.status.analyzing": "Analizowanie żądania...",
448
449
  "ai_assistant.status.executing": "Wykonywanie narzędzi...",
449
450
  "ai_assistant.status.responding": "Odpowiadanie...",
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Regression coverage for issue #2097 (BUG-004).
3
+ *
4
+ * Verifies that the conversation-shared notification subscriber persists a
5
+ * resolved title/body string rather than the raw i18n key, so that
6
+ * consumers that do not run the client renderer (email, export, digest)
7
+ * see human-readable text instead of `ai_assistant.notifications.…`.
8
+ */
9
+
10
+ jest.mock('@open-mercato/shared/lib/i18n/server', () => ({
11
+ loadDictionary: jest.fn(async () => ({
12
+ 'ai_assistant.notifications.conversation_shared.title': 'Conversation shared with you',
13
+ 'ai_assistant.notifications.conversation_shared.body':
14
+ 'An AI conversation has been shared with you.',
15
+ })),
16
+ }))
17
+
18
+ jest.mock('@open-mercato/core/modules/notifications/lib/notificationService', () => ({
19
+ resolveNotificationService: jest.fn(),
20
+ }))
21
+
22
+ import handleConversationShared from '../conversation-shared-notify'
23
+ import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'
24
+ import type { AiConversationSharedPayload } from '../../events'
25
+
26
+ const RESOLVE_NOTIFICATION_SERVICE = resolveNotificationService as jest.MockedFunction<
27
+ typeof resolveNotificationService
28
+ >
29
+
30
+ function makePayload(
31
+ overrides: Partial<AiConversationSharedPayload> = {},
32
+ ): AiConversationSharedPayload {
33
+ return {
34
+ conversationId: 'conv-1',
35
+ tenantId: 'tenant-1',
36
+ organizationId: 'org-1',
37
+ ownerUserId: 'user-owner',
38
+ participantUserId: 'user-recipient',
39
+ role: 'viewer',
40
+ ...overrides,
41
+ }
42
+ }
43
+
44
+ function makeCtx(create: jest.Mock) {
45
+ const container = { resolve: jest.fn() }
46
+ RESOLVE_NOTIFICATION_SERVICE.mockReturnValue({ create } as never)
47
+ return { resolve: (name: string) => container.resolve(name), container }
48
+ }
49
+
50
+ describe('conversation-shared-notify subscriber', () => {
51
+ beforeEach(() => {
52
+ jest.clearAllMocks()
53
+ })
54
+
55
+ it('persists a resolved default-locale title and body, not the raw i18n key', async () => {
56
+ const create = jest.fn(async () => ({ ok: true }))
57
+ const ctx = makeCtx(create)
58
+
59
+ await handleConversationShared(makePayload(), ctx)
60
+
61
+ expect(create).toHaveBeenCalledTimes(1)
62
+ const [input] = create.mock.calls[0]
63
+ expect(input.title).toBe('Conversation shared with you')
64
+ expect(input.body).toBe('An AI conversation has been shared with you.')
65
+ expect(input.titleKey).toBe('ai_assistant.notifications.conversation_shared.title')
66
+ expect(input.bodyKey).toBe('ai_assistant.notifications.conversation_shared.body')
67
+ })
68
+
69
+ it('still calls the notification service with the recipient tenant scope', async () => {
70
+ const create = jest.fn(async () => ({ ok: true }))
71
+ const ctx = makeCtx(create)
72
+
73
+ await handleConversationShared(makePayload(), ctx)
74
+
75
+ const [, scope] = create.mock.calls[0]
76
+ expect(scope).toEqual({ tenantId: 'tenant-1', organizationId: 'org-1' })
77
+ })
78
+
79
+ it('returns early without calling create when participantUserId is missing', async () => {
80
+ const create = jest.fn(async () => ({ ok: true }))
81
+ const ctx = makeCtx(create)
82
+
83
+ await handleConversationShared(
84
+ makePayload({ participantUserId: '' as unknown as string }),
85
+ ctx,
86
+ )
87
+
88
+ expect(create).not.toHaveBeenCalled()
89
+ })
90
+
91
+ it('returns early without calling create when tenantId is missing', async () => {
92
+ const create = jest.fn(async () => ({ ok: true }))
93
+ const ctx = makeCtx(create)
94
+
95
+ await handleConversationShared(
96
+ makePayload({ tenantId: '' as unknown as string }),
97
+ ctx,
98
+ )
99
+
100
+ expect(create).not.toHaveBeenCalled()
101
+ })
102
+
103
+ it('falls back to the i18n key when the dictionary lookup throws', async () => {
104
+ const i18nServer = jest.requireMock('@open-mercato/shared/lib/i18n/server')
105
+ i18nServer.loadDictionary.mockRejectedValueOnce(new Error('dictionary unavailable'))
106
+ const create = jest.fn(async () => ({ ok: true }))
107
+ const ctx = makeCtx(create)
108
+
109
+ await handleConversationShared(makePayload(), ctx)
110
+
111
+ expect(create).toHaveBeenCalledTimes(1)
112
+ const [input] = create.mock.calls[0]
113
+ expect(input.title).toBe('ai_assistant.notifications.conversation_shared.title')
114
+ expect(input.body).toBe('ai_assistant.notifications.conversation_shared.body')
115
+ })
116
+ })
@@ -1,5 +1,8 @@
1
1
  import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'
2
2
  import { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'
3
+ import { defaultLocale } from '@open-mercato/shared/lib/i18n/config'
4
+ import { loadDictionary } from '@open-mercato/shared/lib/i18n/server'
5
+ import { createTranslator } from '@open-mercato/shared/lib/i18n/translate'
3
6
  import { notificationTypes } from '../notifications'
4
7
  import type { AiConversationSharedPayload } from '../events'
5
8
 
@@ -14,6 +17,23 @@ type ResolverContext = {
14
17
  container?: { resolve<T = unknown>(name: string): T }
15
18
  }
16
19
 
20
+ async function resolveDefaultLocaleStrings(
21
+ titleKey: string | undefined,
22
+ bodyKey: string | undefined,
23
+ ): Promise<{ title: string | undefined; body: string | undefined }> {
24
+ if (!titleKey && !bodyKey) return { title: titleKey, body: bodyKey }
25
+ try {
26
+ const dict = await loadDictionary(defaultLocale)
27
+ const t = createTranslator(dict)
28
+ return {
29
+ title: titleKey ? t(titleKey) : titleKey,
30
+ body: bodyKey ? t(bodyKey) : bodyKey,
31
+ }
32
+ } catch {
33
+ return { title: titleKey, body: bodyKey }
34
+ }
35
+ }
36
+
17
37
  export default async function handleConversationShared(
18
38
  payload: AiConversationSharedPayload,
19
39
  ctx: ResolverContext,
@@ -39,6 +59,14 @@ export default async function handleConversationShared(
39
59
  linkHref: `/backend?openAiConversation=${payload.conversationId}`,
40
60
  })
41
61
 
62
+ // Persist a human-readable fallback alongside the i18n keys so consumers
63
+ // that do not run the client renderer (email digests, exports) display a
64
+ // resolved string instead of the raw key. The client renderer continues
65
+ // to re-translate via titleKey/bodyKey for the viewer's locale.
66
+ const resolved = await resolveDefaultLocaleStrings(typeDef.titleKey, typeDef.bodyKey)
67
+ if (resolved.title !== undefined) notificationInput.title = resolved.title
68
+ if (resolved.body !== undefined) notificationInput.body = resolved.body
69
+
42
70
  try {
43
71
  await notificationService.create(notificationInput, {
44
72
  tenantId: payload.tenantId,