@open-mercato/core 0.6.6-develop.6515.1.01911d00e2 → 0.6.6-develop.6519.1.bdc91fb755

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/configs/lib/upgrade-actions.js +31 -1
  3. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  4. package/dist/modules/customers/ai-tools/activities-tasks-pack.js +11 -2
  5. package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +2 -2
  6. package/dist/modules/customers/api/companies/[id]/route.js +3 -2
  7. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  8. package/dist/modules/customers/api/dictionaries/context.js +4 -2
  9. package/dist/modules/customers/api/dictionaries/context.js.map +2 -2
  10. package/dist/modules/customers/api/interactions/conflicts/route.js +2 -1
  11. package/dist/modules/customers/api/interactions/conflicts/route.js.map +2 -2
  12. package/dist/modules/customers/api/interactions/counts/route.js +7 -2
  13. package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
  14. package/dist/modules/customers/api/people/[id]/route.js +3 -2
  15. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  16. package/dist/modules/customers/cli.js +19 -0
  17. package/dist/modules/customers/cli.js.map +2 -2
  18. package/dist/modules/customers/commands/interactions.js +8 -3
  19. package/dist/modules/customers/commands/interactions.js.map +2 -2
  20. package/dist/modules/customers/commands/shared.js +2 -1
  21. package/dist/modules/customers/commands/shared.js.map +2 -2
  22. package/dist/modules/customers/components/DictionarySettings.js +5 -0
  23. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  24. package/dist/modules/customers/components/DictionarySortSettings.js +5 -0
  25. package/dist/modules/customers/components/DictionarySortSettings.js.map +2 -2
  26. package/dist/modules/customers/components/detail/ActivitiesCard.js +3 -2
  27. package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
  28. package/dist/modules/customers/components/detail/ActivityCard.js +2 -1
  29. package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
  30. package/dist/modules/customers/components/detail/ActivityTimeline.js +3 -2
  31. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
  32. package/dist/modules/customers/components/detail/TaskDialog.js +3 -2
  33. package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
  34. package/dist/modules/customers/components/detail/TaskForm.js +170 -69
  35. package/dist/modules/customers/components/detail/TaskForm.js.map +2 -2
  36. package/dist/modules/customers/components/detail/TasksSection.js +27 -4
  37. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  38. package/dist/modules/customers/components/detail/hooks/usePersonTasks.js.map +2 -2
  39. package/dist/modules/customers/data/enrichers.js +2 -2
  40. package/dist/modules/customers/data/enrichers.js.map +2 -2
  41. package/dist/modules/customers/data/validators.js +6 -2
  42. package/dist/modules/customers/data/validators.js.map +2 -2
  43. package/dist/modules/customers/lib/dictionaries.js +2 -1
  44. package/dist/modules/customers/lib/dictionaries.js.map +2 -2
  45. package/dist/modules/customers/lib/interactionProjection.js +2 -1
  46. package/dist/modules/customers/lib/interactionProjection.js.map +2 -2
  47. package/dist/modules/customers/lib/interactionStatus.js +24 -0
  48. package/dist/modules/customers/lib/interactionStatus.js.map +7 -0
  49. package/dist/modules/customers/lib/link-channel-message-handler.js +4 -5
  50. package/dist/modules/customers/lib/link-channel-message-handler.js.map +2 -2
  51. package/dist/modules/dictionaries/components/AppearanceSelector.js +7 -3
  52. package/dist/modules/dictionaries/components/AppearanceSelector.js.map +2 -2
  53. package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js +1 -0
  54. package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
  55. package/package.json +7 -7
  56. package/src/modules/configs/lib/upgrade-actions.ts +31 -1
  57. package/src/modules/customers/ai-tools/activities-tasks-pack.ts +12 -3
  58. package/src/modules/customers/api/companies/[id]/route.ts +3 -2
  59. package/src/modules/customers/api/dictionaries/context.ts +2 -0
  60. package/src/modules/customers/api/interactions/conflicts/route.ts +2 -1
  61. package/src/modules/customers/api/interactions/counts/route.ts +7 -2
  62. package/src/modules/customers/api/people/[id]/route.ts +3 -2
  63. package/src/modules/customers/cli.ts +19 -0
  64. package/src/modules/customers/commands/interactions.ts +8 -3
  65. package/src/modules/customers/commands/shared.ts +1 -0
  66. package/src/modules/customers/components/DictionarySettings.tsx +5 -0
  67. package/src/modules/customers/components/DictionarySortSettings.tsx +5 -0
  68. package/src/modules/customers/components/detail/ActivitiesCard.tsx +3 -2
  69. package/src/modules/customers/components/detail/ActivityCard.tsx +2 -1
  70. package/src/modules/customers/components/detail/ActivityTimeline.tsx +3 -2
  71. package/src/modules/customers/components/detail/TaskDialog.tsx +4 -1
  72. package/src/modules/customers/components/detail/TaskForm.tsx +218 -71
  73. package/src/modules/customers/components/detail/TasksSection.tsx +28 -2
  74. package/src/modules/customers/components/detail/hooks/usePersonTasks.ts +1 -0
  75. package/src/modules/customers/data/enrichers.ts +2 -7
  76. package/src/modules/customers/data/validators.ts +16 -2
  77. package/src/modules/customers/i18n/de.json +10 -0
  78. package/src/modules/customers/i18n/en.json +10 -0
  79. package/src/modules/customers/i18n/es.json +10 -0
  80. package/src/modules/customers/i18n/pl.json +10 -0
  81. package/src/modules/customers/lib/dictionaries.ts +1 -0
  82. package/src/modules/customers/lib/interactionProjection.ts +6 -2
  83. package/src/modules/customers/lib/interactionStatus.ts +30 -0
  84. package/src/modules/customers/lib/link-channel-message-handler.ts +4 -5
  85. package/src/modules/dictionaries/components/AppearanceSelector.tsx +10 -1
  86. package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +1 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/data/validators.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { isValidPhoneNumber } from '@open-mercato/shared/lib/phone'\nimport { COORDINATE_RANGES } from '@open-mercato/shared/lib/location/coordinates'\nimport { dictionaryEntrySortModeSchema } from '@open-mercato/core/modules/dictionaries/lib/entrySort'\n\nconst uuid = () => z.string().uuid()\n\nexport const CUSTOMER_PHONE_INVALID_MESSAGE_KEY = 'customers.people.form.primaryPhone.invalid'\nexport const ACTIVITY_DATE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.dateRequired'\nexport const ACTIVITY_TIME_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.timeRequired'\nexport const ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.phoneRequired'\nexport const ACTIVITY_PHONE_INVALID_MESSAGE_KEY = 'customers.activities.errors.phoneInvalid'\n\nconst emptyStringToNull = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n}\n\nconst phoneSchema = z.preprocess(\n emptyStringToNull,\n z\n .string()\n .trim()\n .max(50)\n .refine((val) => isValidPhoneNumber(val), { message: CUSTOMER_PHONE_INVALID_MESSAGE_KEY })\n .nullable()\n .optional(),\n)\n\nconst clearableEmailSchema = z.preprocess(\n emptyStringToNull,\n z.string().email().max(320).nullable().optional(),\n)\n\nconst clearableUrlSchema = z.preprocess(\n emptyStringToNull,\n z.string().url().max(300).nullable().optional(),\n)\n\n// Domain is a plain (non-URL) string that maps to a nullable column, so blanking\n// a previously-set value on edit must transmit null to clear it. See #2529.\nconst clearableDomainSchema = z.preprocess(\n emptyStringToNull,\n z.string().trim().max(200).nullable().optional(),\n)\n\n// Plain optional string fields that map to nullable columns: blanking a previously-set\n// value on edit must transmit null to clear it, not be silently dropped. See #3050.\nconst clearableStringSchema = (max: number) =>\n z.preprocess(emptyStringToNull, z.string().trim().max(max).nullable().optional())\n\n// Annual revenue maps to a nullable numeric column. `''`/whitespace/null all clear it;\n// `.nullable()` short-circuits before coercion so null does not coerce to 0. See #3050.\nconst clearableRevenueSchema = z.preprocess(\n (value) => {\n if (typeof value === 'string' && value.trim().length === 0) return null\n return value\n },\n z.coerce.number().min(0).nullable().optional(),\n)\n\nconst interactionPhoneNumberSchema = z.string().trim().max(50).optional().nullable()\n\nconst scopedSchema = z.object({\n organizationId: uuid(),\n tenantId: uuid(),\n})\n\nconst nextInteractionSchema = z\n .object({\n at: z.coerce.date(),\n name: z.string().trim().min(1).max(200),\n refId: z.string().trim().max(191).optional().nullable(),\n icon: z.string().trim().max(100).optional().nullable(),\n color: z\n .string()\n .trim()\n .regex(/^#([0-9a-fA-F]{6})$/)\n .optional()\n .nullable(),\n })\n .strict()\n\nconst displayNameSchema = z.string().trim().min(1).max(200)\n\nconst baseEntitySchema = {\n displayName: displayNameSchema,\n // Nullable so a blanked description on edit clears the column instead of being dropped. See #3050.\n description: clearableStringSchema(4000),\n ownerUserId: uuid().optional(),\n primaryEmail: clearableEmailSchema,\n primaryPhone: phoneSchema,\n status: z.string().trim().max(100).optional(),\n lifecycleStage: z.string().trim().max(100).optional(),\n source: z.string().trim().max(150).optional(),\n temperature: z.string().trim().max(100).optional(),\n renewalQuarter: z.string().trim().max(100).optional(),\n isActive: z.boolean().optional(),\n nextInteraction: nextInteractionSchema.nullable().optional(),\n tags: z.array(uuid()).optional(),\n}\n\nconst personDetailsSchema = {\n preferredName: z.string().trim().max(120).optional(),\n jobTitle: z.string().trim().max(150).optional(),\n department: z.string().trim().max(150).optional(),\n seniority: z.string().trim().max(100).optional(),\n timezone: z.string().trim().max(120).optional(),\n linkedInUrl: clearableUrlSchema,\n twitterUrl: clearableUrlSchema,\n companyEntityId: uuid().nullable().optional(),\n}\n\nconst personFirstNameSchema = z.string().trim().min(1).max(120)\nconst personLastNameSchema = z.string().trim().min(1).max(120)\n\nconst companyDetailsSchema = {\n // Nullable so blanked values on edit clear the columns instead of being dropped. See #3050.\n legalName: clearableStringSchema(200),\n brandName: clearableStringSchema(200),\n domain: clearableDomainSchema,\n websiteUrl: clearableUrlSchema,\n industry: z.string().trim().max(150).optional(),\n sizeBucket: clearableStringSchema(100),\n annualRevenue: clearableRevenueSchema,\n}\n\nexport const personCreateSchema = scopedSchema.extend({\n ...baseEntitySchema,\n displayName: displayNameSchema.optional(),\n firstName: personFirstNameSchema,\n lastName: personLastNameSchema,\n ...personDetailsSchema,\n})\n\nexport const personUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(\n scopedSchema.extend({\n ...baseEntitySchema,\n ...personDetailsSchema,\n firstName: personFirstNameSchema.optional(),\n lastName: personLastNameSchema.optional(),\n }).partial()\n )\n\nexport const companyCreateSchema = scopedSchema.extend({\n ...baseEntitySchema,\n displayName: displayNameSchema,\n ...companyDetailsSchema,\n})\n\nexport const companyUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(companyCreateSchema.partial())\n\nexport const dealCreateSchema = scopedSchema.extend({\n title: z.string().min(1).max(200),\n description: z.string().max(4000).optional(),\n status: z.string().max(50).optional(),\n pipelineStage: z.string().max(100).optional(),\n pipelineId: uuid().optional(),\n pipelineStageId: uuid().optional(),\n valueAmount: z.coerce.number().min(0).optional(),\n valueCurrency: z.string().min(3).max(3).optional(),\n probability: z.number().min(0).max(100).optional(),\n expectedCloseAt: z.coerce.date().optional(),\n // Nullable: the bulk owner-update worker passes `null` to clear ownership.\n // Without `.nullable()`, dealUpdateSchema.parse({ ownerUserId: null }) throws\n // ZodError \"expected string, received null\" inside the queue worker (TC-CRM-069).\n ownerUserId: uuid().optional().nullable(),\n source: z.string().max(150).optional(),\n closureOutcome: z.enum(['won', 'lost']).optional(),\n lossReasonId: uuid().optional(),\n lossNotes: z.string().max(4000).optional(),\n companyIds: z.array(uuid()).optional(),\n personIds: z.array(uuid()).optional(),\n})\n\nexport const dealUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(dealCreateSchema.partial())\n\n// Bulk update schemas \u2014 used by `api/deals/bulk-update-{owner,stage}/route.ts`. Kept here\n// so all deal-write contracts live next to `dealCreateSchema` / `dealUpdateSchema`.\nexport const dealsBulkUpdateOwnerSchema = z.object({\n ids: z.array(uuid()).min(1).max(10000),\n ownerUserId: uuid().nullable(),\n})\n\nexport const dealsBulkUpdateStageSchema = z.object({\n ids: z.array(uuid()).min(1).max(10000),\n pipelineStageId: uuid(),\n})\n\nexport const dealsBulkUpdateResponseSchema = z.object({\n ok: z.boolean(),\n progressJobId: uuid().nullable(),\n message: z.string(),\n})\n\nexport const activityCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n activityType: z.string().min(1).max(100),\n subject: z.string().max(200).optional(),\n body: z.string().max(8000).optional(),\n date: z.string().trim().min(1, ACTIVITY_DATE_REQUIRED_MESSAGE_KEY).optional(),\n time: z.string().trim().min(1, ACTIVITY_TIME_REQUIRED_MESSAGE_KEY).optional(),\n phoneNumber: interactionPhoneNumberSchema,\n occurredAt: z.coerce.date().optional(),\n dealId: uuid().optional(),\n authorUserId: uuid().optional(),\n appearanceIcon: z.string().trim().max(100).optional().nullable(),\n appearanceColor: z\n .string()\n .trim()\n .regex(/^#([0-9a-fA-F]{6})$/)\n .optional()\n .nullable(),\n})\n\nexport const activityUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(activityCreateSchema.partial())\n\nexport const commentCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n dealId: uuid().optional(),\n body: z.string().min(1).max(8000),\n authorUserId: uuid().optional(),\n appearanceIcon: z.string().trim().max(100).optional().nullable(),\n appearanceColor: z\n .string()\n .trim()\n .regex(/^#([0-9a-fA-F]{6})$/)\n .optional()\n .nullable(),\n})\n\nexport const commentUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(commentCreateSchema.partial())\n\nexport const addressCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n name: z.string().max(150).optional(),\n purpose: z.string().max(150).optional(),\n companyName: z.string().max(200).optional(),\n addressLine1: z.string().min(1).max(300),\n addressLine2: z.string().max(300).optional(),\n buildingNumber: z.string().max(50).optional(),\n flatNumber: z.string().max(50).optional(),\n city: z.string().max(150).optional(),\n region: z.string().max(150).optional(),\n postalCode: z.string().max(30).optional(),\n country: z.string().max(150).optional(),\n latitude: z.coerce\n .number()\n .min(COORDINATE_RANGES.latitude.min)\n .max(COORDINATE_RANGES.latitude.max)\n .nullable()\n .optional(),\n longitude: z.coerce\n .number()\n .min(COORDINATE_RANGES.longitude.min)\n .max(COORDINATE_RANGES.longitude.max)\n .nullable()\n .optional(),\n isPrimary: z.boolean().optional(),\n})\n\nexport const addressUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(addressCreateSchema.partial())\n\nexport const tagCreateSchema = scopedSchema.extend({\n slug: z\n .string()\n .min(1)\n .max(80)\n .regex(/^[a-z0-9_-]+$/, 'Slug must be lowercase and may contain dashes or underscores'),\n label: z.string().min(1).max(120),\n color: z.string().max(30).optional(),\n description: z.string().max(400).optional(),\n})\n\nexport const tagUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(tagCreateSchema.partial())\n\nconst KNOWN_DICTIONARY_KINDS = [\n 'status',\n 'source',\n 'lifecycle_stage',\n 'address_type',\n 'activity_type',\n 'deal_status',\n 'pipeline_stage',\n 'job_title',\n 'industry',\n 'temperature',\n 'renewal_quarter',\n 'person_company_role',\n] as const\nconst CUSTOM_DICTIONARY_KIND_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/\nconst dictionaryKindEnum = z.string().trim().refine(\n (value) =>\n (KNOWN_DICTIONARY_KINDS as readonly string[]).includes(value) ||\n CUSTOM_DICTIONARY_KIND_PATTERN.test(value),\n { message: 'Unsupported dictionary kind' },\n)\n\nconst dictionaryValueSchema = z.string().trim().min(1).max(150)\nconst dictionaryLabelSchema = z.string().trim().max(150)\n// Pipeline-stage rows migrated to semantic tone identifiers in\n// Migration20260519120000_pipeline_stage_color_tones; AddStageDialog now writes those\n// directly. Other dictionary kinds still store hex. Accept either format so round-tripping\n// a migrated pipeline-stage entry through the dictionary edit UI doesn't fail validation.\nconst DICTIONARY_COLOR_TONES = ['success', 'warning', 'info', 'error', 'neutral', 'brand', 'pink'] as const\nconst dictionaryColorSchema = z\n .string()\n .trim()\n .regex(\n new RegExp(`^(#[0-9a-fA-F]{6}|${DICTIONARY_COLOR_TONES.join('|')})$`),\n 'Color must be a six-digit hex code (e.g. #3366ff) or a semantic tone identifier',\n )\nconst dictionaryIconSchema = z.string().trim().max(48)\n\nexport const customerDictionaryEntryCreateSchema = scopedSchema.extend({\n kind: dictionaryKindEnum,\n value: dictionaryValueSchema,\n label: dictionaryLabelSchema.optional(),\n color: dictionaryColorSchema.nullable().optional(),\n icon: dictionaryIconSchema.nullable().optional(),\n})\n\nexport type CustomerDictionaryEntryCreateInput = z.infer<typeof customerDictionaryEntryCreateSchema>\n\nexport const customerDictionaryEntryUpdateSchema = scopedSchema\n .extend({\n id: uuid(),\n kind: dictionaryKindEnum,\n value: dictionaryValueSchema.optional(),\n label: dictionaryLabelSchema.optional(),\n color: dictionaryColorSchema.nullable().optional(),\n icon: dictionaryIconSchema.nullable().optional(),\n })\n .refine(\n (payload) =>\n payload.value !== undefined ||\n payload.label !== undefined ||\n payload.color !== undefined ||\n payload.icon !== undefined,\n {\n message: 'Provide at least one field to update.',\n path: ['value'],\n }\n )\n\nexport type CustomerDictionaryEntryUpdateInput = z.infer<typeof customerDictionaryEntryUpdateSchema>\n\nexport const customerDictionaryEntryDeleteSchema = scopedSchema.extend({\n id: uuid(),\n kind: dictionaryKindEnum,\n})\n\nexport type CustomerDictionaryEntryDeleteInput = z.infer<typeof customerDictionaryEntryDeleteSchema>\n\nexport const tagAssignmentSchema = scopedSchema.extend({\n tagId: uuid(),\n entityId: uuid(),\n})\n\nexport const todoLinkCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n todoId: uuid(),\n todoSource: z.string().min(1).max(120).default('customers:interaction'),\n createdByUserId: uuid().optional(),\n})\n\nexport const todoLinkWithTodoCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n title: z.string().min(1).max(200),\n isDone: z.boolean().optional(),\n is_done: z.boolean().optional(),\n todoSource: z.string().min(1).max(120).default('customers:interaction'),\n createdByUserId: uuid().optional(),\n todoCustom: z.record(z.string(), z.any()).optional(),\n custom: z.record(z.string(), z.any()).optional(),\n})\n\n// --- Interaction schemas ---\n\nexport const interactionStatusValues = ['planned', 'done', 'canceled'] as const\nexport type InteractionStatus = typeof interactionStatusValues[number]\n\nconst interactionParticipantSchema = z.object({\n userId: z.string().uuid(),\n name: z.string().trim().max(200).optional(),\n email: z.string().trim().max(320).optional(),\n status: z.string().trim().max(50).optional(),\n})\n\nconst interactionLinkedEntitySchema = z.object({\n id: z.string().uuid(),\n type: z.enum(['company', 'deal', 'offer']),\n label: z.string().trim().max(500),\n})\n\nconst interactionGuestPermissionsSchema = z\n .object({\n canInviteOthers: z.boolean().optional(),\n canModify: z.boolean().optional(),\n canSeeList: z.boolean().optional(),\n })\n .strict()\n\nconst interactionExtendedFields = {\n durationMinutes: z.number().int().min(0).optional().nullable(),\n location: z.string().trim().max(500).optional().nullable(),\n allDay: z.boolean().optional().nullable(),\n recurrenceRule: z.string().trim().max(500).optional().nullable(),\n recurrenceEnd: z.coerce.date().optional().nullable(),\n participants: z.array(interactionParticipantSchema).optional().nullable(),\n reminderMinutes: z.number().int().min(0).optional().nullable(),\n visibility: z.string().trim().max(50).optional().nullable(),\n linkedEntities: z.array(interactionLinkedEntitySchema).optional().nullable(),\n guestPermissions: interactionGuestPermissionsSchema.optional().nullable(),\n} as const\n\nconst interactionCreateBaseSchema = scopedSchema.extend({\n id: z.string().uuid().optional(),\n entityId: z.string().uuid(),\n interactionType: z.string().trim().min(1).max(100),\n title: z.string().trim().max(500).optional().nullable(),\n body: z.string().trim().max(10000).optional().nullable(),\n status: z.enum(interactionStatusValues).optional().default('planned'),\n date: z.string().trim().min(1, ACTIVITY_DATE_REQUIRED_MESSAGE_KEY).optional(),\n time: z.string().trim().min(1, ACTIVITY_TIME_REQUIRED_MESSAGE_KEY).optional(),\n phoneNumber: interactionPhoneNumberSchema,\n scheduledAt: z.coerce.date().optional().nullable(),\n occurredAt: z.coerce.date().optional().nullable(),\n priority: z.number().int().min(0).max(100).optional().nullable(),\n authorUserId: z.string().uuid().optional().nullable(),\n ownerUserId: z.string().uuid().optional().nullable(),\n dealId: z.string().uuid().optional().nullable(),\n appearanceIcon: z.string().trim().max(100).optional().nullable(),\n appearanceColor: z.string().trim().regex(/^#([0-9a-fA-F]{6})$/).optional().nullable(),\n source: z.string().trim().max(100).optional().nullable(),\n ...interactionExtendedFields,\n})\n\nfunction deriveScheduledAtFromDateTime(date?: string, time?: string): Date | null {\n if (!date || typeof date !== 'string') return null\n const trimmedDate = date.trim()\n if (!trimmedDate) return null\n const trimmedTime = typeof time === 'string' ? time.trim() : ''\n const iso = trimmedTime ? `${trimmedDate}T${trimmedTime}:00` : `${trimmedDate}T00:00:00`\n const parsed = new Date(iso)\n return Number.isNaN(parsed.getTime()) ? null : parsed\n}\n\nexport const interactionCreateSchema = interactionCreateBaseSchema\n .superRefine((value, ctx) => {\n if (value.interactionType === 'call' && value.phoneNumber !== undefined && value.phoneNumber !== null) {\n const phone = typeof value.phoneNumber === 'string' ? value.phoneNumber.trim() : ''\n if (!phone) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['phoneNumber'],\n message: ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY,\n })\n } else if (!isValidPhoneNumber(phone)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['phoneNumber'],\n message: ACTIVITY_PHONE_INVALID_MESSAGE_KEY,\n })\n }\n }\n })\n // Derive `scheduledAt` from `date+time` when only the latter are sent so\n // external API consumers don't silently persist `scheduled_at: null` after\n // the validator already enforced non-empty date/time. The form already\n // computes `scheduledAt` itself, so this branch is a no-op for the form path.\n .transform((value) => {\n if (value.scheduledAt) return value\n const derived = deriveScheduledAtFromDateTime(value.date, value.time)\n return derived ? { ...value, scheduledAt: derived } : value\n })\n\nexport type InteractionCreateInput = z.infer<typeof interactionCreateSchema>\n\nconst interactionUpdateBaseSchema = z\n .object({\n id: z.string().uuid(),\n })\n .merge(\n scopedSchema\n .extend({\n interactionType: z.string().trim().min(1).max(100).optional(),\n title: z.string().trim().max(500).optional().nullable(),\n body: z.string().trim().max(10000).optional().nullable(),\n status: z.enum(interactionStatusValues).optional(),\n date: z.string().trim().min(1, ACTIVITY_DATE_REQUIRED_MESSAGE_KEY).optional(),\n time: z.string().trim().min(1, ACTIVITY_TIME_REQUIRED_MESSAGE_KEY).optional(),\n phoneNumber: interactionPhoneNumberSchema,\n scheduledAt: z.coerce.date().optional().nullable(),\n occurredAt: z.coerce.date().optional().nullable(),\n priority: z.number().int().min(0).max(100).optional().nullable(),\n authorUserId: z.string().uuid().optional().nullable(),\n ownerUserId: z.string().uuid().optional().nullable(),\n dealId: z.string().uuid().optional().nullable(),\n appearanceIcon: z.string().trim().max(100).optional().nullable(),\n appearanceColor: z.string().trim().regex(/^#([0-9a-fA-F]{6})$/).optional().nullable(),\n pinned: z.boolean().optional(),\n ...interactionExtendedFields,\n })\n .partial(),\n )\n\nexport const interactionUpdateSchema = interactionUpdateBaseSchema\n .superRefine((value, ctx) => {\n if (value.interactionType === 'call' && value.phoneNumber !== undefined) {\n const phone = typeof value.phoneNumber === 'string' ? value.phoneNumber.trim() : ''\n if (!phone) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['phoneNumber'],\n message: ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY,\n })\n } else if (!isValidPhoneNumber(phone)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['phoneNumber'],\n message: ACTIVITY_PHONE_INVALID_MESSAGE_KEY,\n })\n }\n }\n })\n // Mirror the create-schema derivation for partial updates: when an external\n // caller supplies `date+time` without `scheduledAt`, derive the timestamp so\n // the update doesn't silently leave `scheduled_at` stale.\n .transform((value) => {\n if (value.scheduledAt !== undefined) return value\n if (!value.date && !value.time) return value\n const derived = deriveScheduledAtFromDateTime(value.date, value.time)\n return derived ? { ...value, scheduledAt: derived } : value\n })\n\nexport type InteractionUpdateInput = z.infer<typeof interactionUpdateSchema>\n\nexport const interactionCompleteSchema = z.object({\n id: z.string().uuid(),\n occurredAt: z.coerce.date().optional(),\n})\n\nexport const interactionCancelSchema = z.object({\n id: z.string().uuid(),\n})\n\nexport const customerAddressFormatSchema = z.enum(['line_first', 'street_first'])\n\nexport const customerSettingsUpsertSchema = scopedSchema.extend({\n addressFormat: customerAddressFormatSchema,\n})\n\nexport const customerStuckThresholdUpsertSchema = scopedSchema.extend({\n stuckThresholdDays: z.number().int().min(1).max(365),\n})\n\nexport const customerDictionarySortModesSchema = z.record(z.string(), dictionaryEntrySortModeSchema)\n\nexport const customerDictionarySortModesUpsertSchema = scopedSchema.extend({\n dictionarySortModes: customerDictionarySortModesSchema,\n})\n\nexport type PersonCreateInput = z.infer<typeof personCreateSchema>\nexport type PersonUpdateInput = z.infer<typeof personUpdateSchema>\nexport type CompanyCreateInput = z.infer<typeof companyCreateSchema>\nexport type CompanyUpdateInput = z.infer<typeof companyUpdateSchema>\nexport type DealCreateInput = z.infer<typeof dealCreateSchema>\nexport type DealUpdateInput = z.infer<typeof dealUpdateSchema>\nexport type ActivityCreateInput = z.infer<typeof activityCreateSchema>\nexport type ActivityUpdateInput = z.infer<typeof activityUpdateSchema>\nexport type CommentCreateInput = z.infer<typeof commentCreateSchema>\nexport type CommentUpdateInput = z.infer<typeof commentUpdateSchema>\nexport type AddressCreateInput = z.infer<typeof addressCreateSchema>\nexport type AddressUpdateInput = z.infer<typeof addressUpdateSchema>\nexport type TagCreateInput = z.infer<typeof tagCreateSchema>\nexport type TagUpdateInput = z.infer<typeof tagUpdateSchema>\nexport type TagAssignmentInput = z.infer<typeof tagAssignmentSchema>\nexport type TodoLinkCreateInput = z.infer<typeof todoLinkCreateSchema>\nexport type TodoLinkWithTodoCreateInput = z.infer<typeof todoLinkWithTodoCreateSchema>\nexport type CustomerSettingsUpsertInput = z.infer<typeof customerSettingsUpsertSchema>\nexport type CustomerStuckThresholdUpsertInput = z.infer<typeof customerStuckThresholdUpsertSchema>\nexport type CustomerDictionarySortModesUpsertInput = z.infer<typeof customerDictionarySortModesUpsertSchema>\nexport type CustomerAddressFormatInput = z.infer<typeof customerAddressFormatSchema>\nexport type InteractionCompleteInput = z.infer<typeof interactionCompleteSchema>\nexport type InteractionCancelInput = z.infer<typeof interactionCancelSchema>\n\n// --- Pipeline schemas ---\n\nexport const pipelineCreateSchema = scopedSchema.extend({\n name: z.string().trim().min(1).max(200),\n isDefault: z.boolean().optional(),\n})\n\nexport const pipelineUpdateSchema = z.object({\n id: uuid(),\n name: z.string().trim().min(1).max(200).optional(),\n isDefault: z.boolean().optional(),\n})\n\nexport const pipelineDeleteSchema = z.object({\n id: uuid(),\n})\n\nexport type PipelineCreateInput = z.infer<typeof pipelineCreateSchema>\nexport type PipelineUpdateInput = z.infer<typeof pipelineUpdateSchema>\nexport type PipelineDeleteInput = z.infer<typeof pipelineDeleteSchema>\n\n// --- Pipeline Stage schemas ---\n\nexport const pipelineStageCreateSchema = scopedSchema.extend({\n pipelineId: uuid(),\n label: z.string().trim().min(1).max(200),\n order: z.number().int().min(0).optional(),\n color: z.string().trim().max(20).nullish(),\n icon: z.string().trim().max(100).nullish(),\n})\n\nexport const pipelineStageUpdateSchema = z.object({\n id: uuid(),\n label: z.string().trim().min(1).max(200).optional(),\n order: z.number().int().min(0).optional(),\n color: z.string().trim().max(20).nullish(),\n icon: z.string().trim().max(100).nullish(),\n})\n\nexport const pipelineStageDeleteSchema = z.object({\n id: uuid(),\n})\n\nexport const pipelineStageReorderSchema = scopedSchema.extend({\n stages: z.array(z.object({\n id: uuid(),\n order: z.number().int().min(0),\n })).min(1),\n})\n\nexport type PipelineStageCreateInput = z.infer<typeof pipelineStageCreateSchema>\nexport type PipelineStageUpdateInput = z.infer<typeof pipelineStageUpdateSchema>\nexport type PipelineStageDeleteInput = z.infer<typeof pipelineStageDeleteSchema>\nexport type PipelineStageReorderInput = z.infer<typeof pipelineStageReorderSchema>\n\nexport const entityRoleCreateSchema = scopedSchema.extend({\n entityType: z.enum(['company', 'person']),\n entityId: uuid(),\n roleType: z.string().trim().min(1).max(100),\n userId: uuid(),\n})\n\nexport const entityRoleUpdateSchema = scopedSchema.extend({\n id: uuid(),\n userId: uuid(),\n})\n\nexport const entityRoleDeleteSchema = scopedSchema.extend({\n id: uuid(),\n})\n\nexport type EntityRoleCreateInput = z.infer<typeof entityRoleCreateSchema>\nexport type EntityRoleUpdateInput = z.infer<typeof entityRoleUpdateSchema>\nexport type EntityRoleDeleteInput = z.infer<typeof entityRoleDeleteSchema>\n\nexport const updateKindSettingSchema = z.object({\n kind: z.string().trim().min(1).max(100),\n selectionMode: z.enum(['single', 'multi']).optional(),\n visibleInTags: z.boolean().optional(),\n sortOrder: z.number().int().min(0).optional(),\n})\n\nexport type UpdateKindSettingInput = z.infer<typeof updateKindSettingSchema>\n\nexport const customerKindSettingsUpsertSchema = scopedSchema.extend({\n kind: z.string().trim().min(1).max(100),\n selectionMode: z.enum(['single', 'multi']).optional(),\n visibleInTags: z.boolean().optional(),\n sortOrder: z.number().int().min(0).optional(),\n})\n\nexport type CustomerKindSettingsUpsertInput = z.infer<typeof customerKindSettingsUpsertSchema>\n\nexport const labelCreateSchema = z.object({\n label: z.string().trim().min(1).max(120),\n slug: z.string().trim().min(1).max(80).regex(/^[a-z0-9_-]+$/).optional(),\n})\n\nexport type LabelCreateInput = z.infer<typeof labelCreateSchema>\n\nexport const labelCreateCommandSchema = scopedSchema.extend({\n label: z.string().trim().min(1).max(120),\n slug: z.string().trim().min(1).max(80).regex(/^[a-z0-9_-]+$/),\n userId: uuid(),\n})\n\nexport type LabelCreateCommandInput = z.infer<typeof labelCreateCommandSchema>\n\nexport const labelAssignmentSchema = z.object({\n labelId: z.string().uuid(),\n entityId: z.string().uuid(),\n})\n\nexport type LabelAssignmentInput = z.infer<typeof labelAssignmentSchema>\n\nexport const labelAssignCommandSchema = scopedSchema.extend({\n labelId: uuid(),\n entityId: uuid(),\n})\n\nexport const labelUnassignCommandSchema = scopedSchema.extend({\n labelId: uuid(),\n entityId: uuid(),\n})\n\nexport type LabelAssignCommandInput = z.infer<typeof labelAssignCommandSchema>\nexport type LabelUnassignCommandInput = z.infer<typeof labelUnassignCommandSchema>\n\nexport const personCompanyLinkCreateSchema = scopedSchema.extend({\n personEntityId: uuid(),\n companyEntityId: uuid(),\n isPrimary: z.boolean().optional(),\n})\n\nexport const personCompanyLinkUpdateSchema = scopedSchema.extend({\n linkId: uuid(),\n isPrimary: z.boolean(),\n})\n\nexport const personCompanyLinkDeleteSchema = scopedSchema.extend({\n linkId: uuid(),\n})\n\nexport type PersonCompanyLinkCreateInput = z.infer<typeof personCompanyLinkCreateSchema>\nexport type PersonCompanyLinkUpdateInput = z.infer<typeof personCompanyLinkUpdateSchema>\nexport type PersonCompanyLinkDeleteInput = z.infer<typeof personCompanyLinkDeleteSchema>\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,SAAS,qCAAqC;AAE9C,MAAM,OAAO,MAAM,EAAE,OAAO,EAAE,KAAK;AAE5B,MAAM,qCAAqC;AAC3C,MAAM,qCAAqC;AAC3C,MAAM,qCAAqC;AAC3C,MAAM,sCAAsC;AAC5C,MAAM,qCAAqC;AAElD,MAAM,oBAAoB,CAAC,UAA4B;AACrD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,UAAU;AACpC;AAEA,MAAM,cAAc,EAAE;AAAA,EACpB;AAAA,EACA,EACG,OAAO,EACP,KAAK,EACL,IAAI,EAAE,EACN,OAAO,CAAC,QAAQ,mBAAmB,GAAG,GAAG,EAAE,SAAS,mCAAmC,CAAC,EACxF,SAAS,EACT,SAAS;AACd;AAEA,MAAM,uBAAuB,EAAE;AAAA,EAC7B;AAAA,EACA,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAClD;AAEA,MAAM,qBAAqB,EAAE;AAAA,EAC3B;AAAA,EACA,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAChD;AAIA,MAAM,wBAAwB,EAAE;AAAA,EAC9B;AAAA,EACA,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AACjD;AAIA,MAAM,wBAAwB,CAAC,QAC7B,EAAE,WAAW,mBAAmB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,CAAC;AAIlF,MAAM,yBAAyB,EAAE;AAAA,EAC/B,CAAC,UAAU;AACT,QAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,WAAW,EAAG,QAAO;AACnE,WAAO;AAAA,EACT;AAAA,EACA,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS;AAC/C;AAEA,MAAM,+BAA+B,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,SAAS;AAEnF,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,gBAAgB,KAAK;AAAA,EACrB,UAAU,KAAK;AACjB,CAAC;AAED,MAAM,wBAAwB,EAC3B,OAAO;AAAA,EACN,IAAI,EAAE,OAAO,KAAK;AAAA,EAClB,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACtC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EACrD,OAAO,EACJ,OAAO,EACP,KAAK,EACL,MAAM,qBAAqB,EAC3B,SAAS,EACT,SAAS;AACd,CAAC,EACA,OAAO;AAEV,MAAM,oBAAoB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAE1D,MAAM,mBAAmB;AAAA,EACvB,aAAa;AAAA;AAAA,EAEb,aAAa,sBAAsB,GAAI;AAAA,EACvC,aAAa,KAAK,EAAE,SAAS;AAAA,EAC7B,cAAc;AAAA,EACd,cAAc;AAAA,EACd,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC5C,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACpD,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC5C,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACjD,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACpD,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC/B,iBAAiB,sBAAsB,SAAS,EAAE,SAAS;AAAA,EAC3D,MAAM,EAAE,MAAM,KAAK,CAAC,EAAE,SAAS;AACjC;AAEA,MAAM,sBAAsB;AAAA,EAC1B,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACnD,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC9C,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAChD,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC/C,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC9C,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,iBAAiB,KAAK,EAAE,SAAS,EAAE,SAAS;AAC9C;AAEA,MAAM,wBAAwB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAC9D,MAAM,uBAAuB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAE7D,MAAM,uBAAuB;AAAA;AAAA,EAE3B,WAAW,sBAAsB,GAAG;AAAA,EACpC,WAAW,sBAAsB,GAAG;AAAA,EACpC,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC9C,YAAY,sBAAsB,GAAG;AAAA,EACrC,eAAe;AACjB;AAEO,MAAM,qBAAqB,aAAa,OAAO;AAAA,EACpD,GAAG;AAAA,EACH,aAAa,kBAAkB,SAAS;AAAA,EACxC,WAAW;AAAA,EACX,UAAU;AAAA,EACV,GAAG;AACL,CAAC;AAEM,MAAM,qBAAqB,EAC/B,OAAO;AAAA,EACN,IAAI,KAAK;AACX,CAAC,EACA;AAAA,EACC,aAAa,OAAO;AAAA,IAClB,GAAG;AAAA,IACH,GAAG;AAAA,IACH,WAAW,sBAAsB,SAAS;AAAA,IAC1C,UAAU,qBAAqB,SAAS;AAAA,EAC1C,CAAC,EAAE,QAAQ;AACb;AAEK,MAAM,sBAAsB,aAAa,OAAO;AAAA,EACrD,GAAG;AAAA,EACH,aAAa;AAAA,EACb,GAAG;AACL,CAAC;AAEM,MAAM,sBAAsB,EAChC,OAAO;AAAA,EACN,IAAI,KAAK;AACX,CAAC,EACA,MAAM,oBAAoB,QAAQ,CAAC;AAE/B,MAAM,mBAAmB,aAAa,OAAO;AAAA,EAClD,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAChC,aAAa,EAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS;AAAA,EAC3C,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACpC,eAAe,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC5C,YAAY,KAAK,EAAE,SAAS;AAAA,EAC5B,iBAAiB,KAAK,EAAE,SAAS;AAAA,EACjC,aAAa,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EAC/C,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACjD,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACjD,iBAAiB,EAAE,OAAO,KAAK,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA,EAI1C,aAAa,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACxC,QAAQ,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACrC,gBAAgB,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAAA,EACjD,cAAc,KAAK,EAAE,SAAS;AAAA,EAC9B,WAAW,EAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS;AAAA,EACzC,YAAY,EAAE,MAAM,KAAK,CAAC,EAAE,SAAS;AAAA,EACrC,WAAW,EAAE,MAAM,KAAK,CAAC,EAAE,SAAS;AACtC,CAAC;AAEM,MAAM,mBAAmB,EAC7B,OAAO;AAAA,EACN,IAAI,KAAK;AACX,CAAC,EACA,MAAM,iBAAiB,QAAQ,CAAC;AAI5B,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,KAAK,EAAE,MAAM,KAAK,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,GAAK;AAAA,EACrC,aAAa,KAAK,EAAE,SAAS;AAC/B,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,KAAK,EAAE,MAAM,KAAK,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,GAAK;AAAA,EACrC,iBAAiB,KAAK;AACxB,CAAC;AAEM,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,IAAI,EAAE,QAAQ;AAAA,EACd,eAAe,KAAK,EAAE,SAAS;AAAA,EAC/B,SAAS,EAAE,OAAO;AACpB,CAAC;AAEM,MAAM,uBAAuB,aAAa,OAAO;AAAA,EACtD,UAAU,KAAK;AAAA,EACf,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACvC,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACtC,MAAM,EAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS;AAAA,EACpC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,kCAAkC,EAAE,SAAS;AAAA,EAC5E,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,kCAAkC,EAAE,SAAS;AAAA,EAC5E,aAAa;AAAA,EACb,YAAY,EAAE,OAAO,KAAK,EAAE,SAAS;AAAA,EACrC,QAAQ,KAAK,EAAE,SAAS;AAAA,EACxB,cAAc,KAAK,EAAE,SAAS;AAAA,EAC9B,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EAC/D,iBAAiB,EACd,OAAO,EACP,KAAK,EACL,MAAM,qBAAqB,EAC3B,SAAS,EACT,SAAS;AACd,CAAC;AAEM,MAAM,uBAAuB,EACjC,OAAO;AAAA,EACN,IAAI,KAAK;AACX,CAAC,EACA,MAAM,qBAAqB,QAAQ,CAAC;AAEhC,MAAM,sBAAsB,aAAa,OAAO;AAAA,EACrD,UAAU,KAAK;AAAA,EACf,QAAQ,KAAK,EAAE,SAAS;AAAA,EACxB,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI;AAAA,EAChC,cAAc,KAAK,EAAE,SAAS;AAAA,EAC9B,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EAC/D,iBAAiB,EACd,OAAO,EACP,KAAK,EACL,MAAM,qBAAqB,EAC3B,SAAS,EACT,SAAS;AACd,CAAC;AAEM,MAAM,sBAAsB,EAChC,OAAO;AAAA,EACN,IAAI,KAAK;AACX,CAAC,EACA,MAAM,oBAAoB,QAAQ,CAAC;AAE/B,MAAM,sBAAsB,aAAa,OAAO;AAAA,EACrD,UAAU,KAAK;AAAA,EACf,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACnC,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACtC,aAAa,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1C,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACvC,cAAc,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC3C,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAC5C,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACxC,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACnC,QAAQ,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACrC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACxC,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACtC,UAAU,EAAE,OACT,OAAO,EACP,IAAI,kBAAkB,SAAS,GAAG,EAClC,IAAI,kBAAkB,SAAS,GAAG,EAClC,SAAS,EACT,SAAS;AAAA,EACZ,WAAW,EAAE,OACV,OAAO,EACP,IAAI,kBAAkB,UAAU,GAAG,EACnC,IAAI,kBAAkB,UAAU,GAAG,EACnC,SAAS,EACT,SAAS;AAAA,EACZ,WAAW,EAAE,QAAQ,EAAE,SAAS;AAClC,CAAC;AAEM,MAAM,sBAAsB,EAChC,OAAO;AAAA,EACN,IAAI,KAAK;AACX,CAAC,EACA,MAAM,oBAAoB,QAAQ,CAAC;AAE/B,MAAM,kBAAkB,aAAa,OAAO;AAAA,EACjD,MAAM,EACH,OAAO,EACP,IAAI,CAAC,EACL,IAAI,EAAE,EACN,MAAM,iBAAiB,8DAA8D;AAAA,EACxF,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACnC,aAAa,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAC5C,CAAC;AAEM,MAAM,kBAAkB,EAC5B,OAAO;AAAA,EACN,IAAI,KAAK;AACX,CAAC,EACA,MAAM,gBAAgB,QAAQ,CAAC;AAElC,MAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AACA,MAAM,iCAAiC;AACvC,MAAM,qBAAqB,EAAE,OAAO,EAAE,KAAK,EAAE;AAAA,EAC3C,CAAC,UACE,uBAA6C,SAAS,KAAK,KAC5D,+BAA+B,KAAK,KAAK;AAAA,EAC3C,EAAE,SAAS,8BAA8B;AAC3C;AAEA,MAAM,wBAAwB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAC9D,MAAM,wBAAwB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG;AAKvD,MAAM,yBAAyB,CAAC,WAAW,WAAW,QAAQ,SAAS,WAAW,SAAS,MAAM;AACjG,MAAM,wBAAwB,EAC3B,OAAO,EACP,KAAK,EACL;AAAA,EACC,IAAI,OAAO,qBAAqB,uBAAuB,KAAK,GAAG,CAAC,IAAI;AAAA,EACpE;AACF;AACF,MAAM,uBAAuB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE;AAE9C,MAAM,sCAAsC,aAAa,OAAO;AAAA,EACrE,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO,sBAAsB,SAAS;AAAA,EACtC,OAAO,sBAAsB,SAAS,EAAE,SAAS;AAAA,EACjD,MAAM,qBAAqB,SAAS,EAAE,SAAS;AACjD,CAAC;AAIM,MAAM,sCAAsC,aAChD,OAAO;AAAA,EACN,IAAI,KAAK;AAAA,EACT,MAAM;AAAA,EACN,OAAO,sBAAsB,SAAS;AAAA,EACtC,OAAO,sBAAsB,SAAS;AAAA,EACtC,OAAO,sBAAsB,SAAS,EAAE,SAAS;AAAA,EACjD,MAAM,qBAAqB,SAAS,EAAE,SAAS;AACjD,CAAC,EACA;AAAA,EACC,CAAC,YACC,QAAQ,UAAU,UAClB,QAAQ,UAAU,UAClB,QAAQ,UAAU,UAClB,QAAQ,SAAS;AAAA,EACnB;AAAA,IACE,SAAS;AAAA,IACT,MAAM,CAAC,OAAO;AAAA,EAChB;AACF;AAIK,MAAM,sCAAsC,aAAa,OAAO;AAAA,EACrE,IAAI,KAAK;AAAA,EACT,MAAM;AACR,CAAC;AAIM,MAAM,sBAAsB,aAAa,OAAO;AAAA,EACrD,OAAO,KAAK;AAAA,EACZ,UAAU,KAAK;AACjB,CAAC;AAEM,MAAM,uBAAuB,aAAa,OAAO;AAAA,EACtD,UAAU,KAAK;AAAA,EACf,QAAQ,KAAK;AAAA,EACb,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,uBAAuB;AAAA,EACtE,iBAAiB,KAAK,EAAE,SAAS;AACnC,CAAC;AAEM,MAAM,+BAA+B,aAAa,OAAO;AAAA,EAC9D,UAAU,KAAK;AAAA,EACf,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAChC,QAAQ,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC7B,SAAS,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC9B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,uBAAuB;AAAA,EACtE,iBAAiB,KAAK,EAAE,SAAS;AAAA,EACjC,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACnD,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS;AACjD,CAAC;AAIM,MAAM,0BAA0B,CAAC,WAAW,QAAQ,UAAU;AAGrE,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC3C,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,SAAS;AAC7C,CAAC;AAED,MAAM,gCAAgC,EAAE,OAAO;AAAA,EAC7C,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,KAAK,CAAC,WAAW,QAAQ,OAAO,CAAC;AAAA,EACzC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG;AAClC,CAAC;AAED,MAAM,oCAAoC,EACvC,OAAO;AAAA,EACN,iBAAiB,EAAE,QAAQ,EAAE,SAAS;AAAA,EACtC,WAAW,EAAE,QAAQ,EAAE,SAAS;AAAA,EAChC,YAAY,EAAE,QAAQ,EAAE,SAAS;AACnC,CAAC,EACA,OAAO;AAEV,MAAM,4BAA4B;AAAA,EAChC,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EAC7D,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EACzD,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,EACxC,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EAC/D,eAAe,EAAE,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACnD,cAAc,EAAE,MAAM,4BAA4B,EAAE,SAAS,EAAE,SAAS;AAAA,EACxE,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EAC7D,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,SAAS;AAAA,EAC1D,gBAAgB,EAAE,MAAM,6BAA6B,EAAE,SAAS,EAAE,SAAS;AAAA,EAC3E,kBAAkB,kCAAkC,SAAS,EAAE,SAAS;AAC1E;AAEA,MAAM,8BAA8B,aAAa,OAAO;AAAA,EACtD,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC/B,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,EAC1B,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACjD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACvD,QAAQ,EAAE,KAAK,uBAAuB,EAAE,SAAS,EAAE,QAAQ,SAAS;AAAA,EACpE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,kCAAkC,EAAE,SAAS;AAAA,EAC5E,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,kCAAkC,EAAE,SAAS;AAAA,EAC5E,aAAa;AAAA,EACb,aAAa,EAAE,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACjD,YAAY,EAAE,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EAChD,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EAC/D,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACpD,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACnD,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EAC9C,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EAC/D,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,EAAE,SAAS,EAAE,SAAS;AAAA,EACpF,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EACvD,GAAG;AACL,CAAC;AAED,SAAS,8BAA8B,MAAe,MAA4B;AAChF,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,cAAc,KAAK,KAAK;AAC9B,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,cAAc,OAAO,SAAS,WAAW,KAAK,KAAK,IAAI;AAC7D,QAAM,MAAM,cAAc,GAAG,WAAW,IAAI,WAAW,QAAQ,GAAG,WAAW;AAC7E,QAAM,SAAS,IAAI,KAAK,GAAG;AAC3B,SAAO,OAAO,MAAM,OAAO,QAAQ,CAAC,IAAI,OAAO;AACjD;AAEO,MAAM,0BAA0B,4BACpC,YAAY,CAAC,OAAO,QAAQ;AAC3B,MAAI,MAAM,oBAAoB,UAAU,MAAM,gBAAgB,UAAa,MAAM,gBAAgB,MAAM;AACrG,UAAM,QAAQ,OAAO,MAAM,gBAAgB,WAAW,MAAM,YAAY,KAAK,IAAI;AACjF,QAAI,CAAC,OAAO;AACV,UAAI,SAAS;AAAA,QACX,MAAM,EAAE,aAAa;AAAA,QACrB,MAAM,CAAC,aAAa;AAAA,QACpB,SAAS;AAAA,MACX,CAAC;AAAA,IACH,WAAW,CAAC,mBAAmB,KAAK,GAAG;AACrC,UAAI,SAAS;AAAA,QACX,MAAM,EAAE,aAAa;AAAA,QACrB,MAAM,CAAC,aAAa;AAAA,QACpB,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC,EAKA,UAAU,CAAC,UAAU;AACpB,MAAI,MAAM,YAAa,QAAO;AAC9B,QAAM,UAAU,8BAA8B,MAAM,MAAM,MAAM,IAAI;AACpE,SAAO,UAAU,EAAE,GAAG,OAAO,aAAa,QAAQ,IAAI;AACxD,CAAC;AAIH,MAAM,8BAA8B,EACjC,OAAO;AAAA,EACN,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC,EACA;AAAA,EACC,aACG,OAAO;AAAA,IACN,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,IAC5D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,IACtD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IACvD,QAAQ,EAAE,KAAK,uBAAuB,EAAE,SAAS;AAAA,IACjD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,kCAAkC,EAAE,SAAS;AAAA,IAC5E,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,kCAAkC,EAAE,SAAS;AAAA,IAC5E,aAAa;AAAA,IACb,aAAa,EAAE,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IACjD,YAAY,EAAE,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IAChD,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,IAC/D,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IACpD,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IACnD,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,IAC9C,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,IAC/D,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,EAAE,SAAS,EAAE,SAAS;AAAA,IACpF,QAAQ,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC7B,GAAG;AAAA,EACL,CAAC,EACA,QAAQ;AACb;AAEK,MAAM,0BAA0B,4BACpC,YAAY,CAAC,OAAO,QAAQ;AAC3B,MAAI,MAAM,oBAAoB,UAAU,MAAM,gBAAgB,QAAW;AACvE,UAAM,QAAQ,OAAO,MAAM,gBAAgB,WAAW,MAAM,YAAY,KAAK,IAAI;AACjF,QAAI,CAAC,OAAO;AACV,UAAI,SAAS;AAAA,QACX,MAAM,EAAE,aAAa;AAAA,QACrB,MAAM,CAAC,aAAa;AAAA,QACpB,SAAS;AAAA,MACX,CAAC;AAAA,IACH,WAAW,CAAC,mBAAmB,KAAK,GAAG;AACrC,UAAI,SAAS;AAAA,QACX,MAAM,EAAE,aAAa;AAAA,QACrB,MAAM,CAAC,aAAa;AAAA,QACpB,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC,EAIA,UAAU,CAAC,UAAU;AACpB,MAAI,MAAM,gBAAgB,OAAW,QAAO;AAC5C,MAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,KAAM,QAAO;AACvC,QAAM,UAAU,8BAA8B,MAAM,MAAM,MAAM,IAAI;AACpE,SAAO,UAAU,EAAE,GAAG,OAAO,aAAa,QAAQ,IAAI;AACxD,CAAC;AAII,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,YAAY,EAAE,OAAO,KAAK,EAAE,SAAS;AACvC,CAAC;AAEM,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAEM,MAAM,8BAA8B,EAAE,KAAK,CAAC,cAAc,cAAc,CAAC;AAEzE,MAAM,+BAA+B,aAAa,OAAO;AAAA,EAC9D,eAAe;AACjB,CAAC;AAEM,MAAM,qCAAqC,aAAa,OAAO;AAAA,EACpE,oBAAoB,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AACrD,CAAC;AAEM,MAAM,oCAAoC,EAAE,OAAO,EAAE,OAAO,GAAG,6BAA6B;AAE5F,MAAM,0CAA0C,aAAa,OAAO;AAAA,EACzE,qBAAqB;AACvB,CAAC;AA4BM,MAAM,uBAAuB,aAAa,OAAO;AAAA,EACtD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACtC,WAAW,EAAE,QAAQ,EAAE,SAAS;AAClC,CAAC;AAEM,MAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,IAAI,KAAK;AAAA,EACT,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACjD,WAAW,EAAE,QAAQ,EAAE,SAAS;AAClC,CAAC;AAEM,MAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,IAAI,KAAK;AACX,CAAC;AAQM,MAAM,4BAA4B,aAAa,OAAO;AAAA,EAC3D,YAAY,KAAK;AAAA,EACjB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACxC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ;AAAA,EACzC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,QAAQ;AAC3C,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,IAAI,KAAK;AAAA,EACT,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACxC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ;AAAA,EACzC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,QAAQ;AAC3C,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,IAAI,KAAK;AACX,CAAC;AAEM,MAAM,6BAA6B,aAAa,OAAO;AAAA,EAC5D,QAAQ,EAAE,MAAM,EAAE,OAAO;AAAA,IACvB,IAAI,KAAK;AAAA,IACT,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,EAC/B,CAAC,CAAC,EAAE,IAAI,CAAC;AACX,CAAC;AAOM,MAAM,yBAAyB,aAAa,OAAO;AAAA,EACxD,YAAY,EAAE,KAAK,CAAC,WAAW,QAAQ,CAAC;AAAA,EACxC,UAAU,KAAK;AAAA,EACf,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC1C,QAAQ,KAAK;AACf,CAAC;AAEM,MAAM,yBAAyB,aAAa,OAAO;AAAA,EACxD,IAAI,KAAK;AAAA,EACT,QAAQ,KAAK;AACf,CAAC;AAEM,MAAM,yBAAyB,aAAa,OAAO;AAAA,EACxD,IAAI,KAAK;AACX,CAAC;AAMM,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACtC,eAAe,EAAE,KAAK,CAAC,UAAU,OAAO,CAAC,EAAE,SAAS;AAAA,EACpD,eAAe,EAAE,QAAQ,EAAE,SAAS;AAAA,EACpC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;AAC9C,CAAC;AAIM,MAAM,mCAAmC,aAAa,OAAO;AAAA,EAClE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACtC,eAAe,EAAE,KAAK,CAAC,UAAU,OAAO,CAAC,EAAE,SAAS;AAAA,EACpD,eAAe,EAAE,QAAQ,EAAE,SAAS;AAAA,EACpC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;AAC9C,CAAC;AAIM,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACxC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACvC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,MAAM,eAAe,EAAE,SAAS;AACzE,CAAC;AAIM,MAAM,2BAA2B,aAAa,OAAO;AAAA,EAC1D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACvC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,MAAM,eAAe;AAAA,EAC5D,QAAQ,KAAK;AACf,CAAC;AAIM,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,SAAS,EAAE,OAAO,EAAE,KAAK;AAAA,EACzB,UAAU,EAAE,OAAO,EAAE,KAAK;AAC5B,CAAC;AAIM,MAAM,2BAA2B,aAAa,OAAO;AAAA,EAC1D,SAAS,KAAK;AAAA,EACd,UAAU,KAAK;AACjB,CAAC;AAEM,MAAM,6BAA6B,aAAa,OAAO;AAAA,EAC5D,SAAS,KAAK;AAAA,EACd,UAAU,KAAK;AACjB,CAAC;AAKM,MAAM,gCAAgC,aAAa,OAAO;AAAA,EAC/D,gBAAgB,KAAK;AAAA,EACrB,iBAAiB,KAAK;AAAA,EACtB,WAAW,EAAE,QAAQ,EAAE,SAAS;AAClC,CAAC;AAEM,MAAM,gCAAgC,aAAa,OAAO;AAAA,EAC/D,QAAQ,KAAK;AAAA,EACb,WAAW,EAAE,QAAQ;AACvB,CAAC;AAEM,MAAM,gCAAgC,aAAa,OAAO;AAAA,EAC/D,QAAQ,KAAK;AACf,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { isValidPhoneNumber } from '@open-mercato/shared/lib/phone'\nimport { COORDINATE_RANGES } from '@open-mercato/shared/lib/location/coordinates'\nimport { dictionaryEntrySortModeSchema } from '@open-mercato/core/modules/dictionaries/lib/entrySort'\n\nconst uuid = () => z.string().uuid()\n\nexport const CUSTOMER_PHONE_INVALID_MESSAGE_KEY = 'customers.people.form.primaryPhone.invalid'\nexport const ACTIVITY_DATE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.dateRequired'\nexport const ACTIVITY_TIME_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.timeRequired'\nexport const ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.phoneRequired'\nexport const ACTIVITY_PHONE_INVALID_MESSAGE_KEY = 'customers.activities.errors.phoneInvalid'\n\nconst emptyStringToNull = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n}\n\nconst phoneSchema = z.preprocess(\n emptyStringToNull,\n z\n .string()\n .trim()\n .max(50)\n .refine((val) => isValidPhoneNumber(val), { message: CUSTOMER_PHONE_INVALID_MESSAGE_KEY })\n .nullable()\n .optional(),\n)\n\nconst clearableEmailSchema = z.preprocess(\n emptyStringToNull,\n z.string().email().max(320).nullable().optional(),\n)\n\nconst clearableUrlSchema = z.preprocess(\n emptyStringToNull,\n z.string().url().max(300).nullable().optional(),\n)\n\n// Domain is a plain (non-URL) string that maps to a nullable column, so blanking\n// a previously-set value on edit must transmit null to clear it. See #2529.\nconst clearableDomainSchema = z.preprocess(\n emptyStringToNull,\n z.string().trim().max(200).nullable().optional(),\n)\n\n// Plain optional string fields that map to nullable columns: blanking a previously-set\n// value on edit must transmit null to clear it, not be silently dropped. See #3050.\nconst clearableStringSchema = (max: number) =>\n z.preprocess(emptyStringToNull, z.string().trim().max(max).nullable().optional())\n\n// Annual revenue maps to a nullable numeric column. `''`/whitespace/null all clear it;\n// `.nullable()` short-circuits before coercion so null does not coerce to 0. See #3050.\nconst clearableRevenueSchema = z.preprocess(\n (value) => {\n if (typeof value === 'string' && value.trim().length === 0) return null\n return value\n },\n z.coerce.number().min(0).nullable().optional(),\n)\n\nconst interactionPhoneNumberSchema = z.string().trim().max(50).optional().nullable()\n\nconst scopedSchema = z.object({\n organizationId: uuid(),\n tenantId: uuid(),\n})\n\nconst nextInteractionSchema = z\n .object({\n at: z.coerce.date(),\n name: z.string().trim().min(1).max(200),\n refId: z.string().trim().max(191).optional().nullable(),\n icon: z.string().trim().max(100).optional().nullable(),\n color: z\n .string()\n .trim()\n .regex(/^#([0-9a-fA-F]{6})$/)\n .optional()\n .nullable(),\n })\n .strict()\n\nconst displayNameSchema = z.string().trim().min(1).max(200)\n\nconst baseEntitySchema = {\n displayName: displayNameSchema,\n // Nullable so a blanked description on edit clears the column instead of being dropped. See #3050.\n description: clearableStringSchema(4000),\n ownerUserId: uuid().optional(),\n primaryEmail: clearableEmailSchema,\n primaryPhone: phoneSchema,\n status: z.string().trim().max(100).optional(),\n lifecycleStage: z.string().trim().max(100).optional(),\n source: z.string().trim().max(150).optional(),\n temperature: z.string().trim().max(100).optional(),\n renewalQuarter: z.string().trim().max(100).optional(),\n isActive: z.boolean().optional(),\n nextInteraction: nextInteractionSchema.nullable().optional(),\n tags: z.array(uuid()).optional(),\n}\n\nconst personDetailsSchema = {\n preferredName: z.string().trim().max(120).optional(),\n jobTitle: z.string().trim().max(150).optional(),\n department: z.string().trim().max(150).optional(),\n seniority: z.string().trim().max(100).optional(),\n timezone: z.string().trim().max(120).optional(),\n linkedInUrl: clearableUrlSchema,\n twitterUrl: clearableUrlSchema,\n companyEntityId: uuid().nullable().optional(),\n}\n\nconst personFirstNameSchema = z.string().trim().min(1).max(120)\nconst personLastNameSchema = z.string().trim().min(1).max(120)\n\nconst companyDetailsSchema = {\n // Nullable so blanked values on edit clear the columns instead of being dropped. See #3050.\n legalName: clearableStringSchema(200),\n brandName: clearableStringSchema(200),\n domain: clearableDomainSchema,\n websiteUrl: clearableUrlSchema,\n industry: z.string().trim().max(150).optional(),\n sizeBucket: clearableStringSchema(100),\n annualRevenue: clearableRevenueSchema,\n}\n\nexport const personCreateSchema = scopedSchema.extend({\n ...baseEntitySchema,\n displayName: displayNameSchema.optional(),\n firstName: personFirstNameSchema,\n lastName: personLastNameSchema,\n ...personDetailsSchema,\n})\n\nexport const personUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(\n scopedSchema.extend({\n ...baseEntitySchema,\n ...personDetailsSchema,\n firstName: personFirstNameSchema.optional(),\n lastName: personLastNameSchema.optional(),\n }).partial()\n )\n\nexport const companyCreateSchema = scopedSchema.extend({\n ...baseEntitySchema,\n displayName: displayNameSchema,\n ...companyDetailsSchema,\n})\n\nexport const companyUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(companyCreateSchema.partial())\n\nexport const dealCreateSchema = scopedSchema.extend({\n title: z.string().min(1).max(200),\n description: z.string().max(4000).optional(),\n status: z.string().max(50).optional(),\n pipelineStage: z.string().max(100).optional(),\n pipelineId: uuid().optional(),\n pipelineStageId: uuid().optional(),\n valueAmount: z.coerce.number().min(0).optional(),\n valueCurrency: z.string().min(3).max(3).optional(),\n probability: z.number().min(0).max(100).optional(),\n expectedCloseAt: z.coerce.date().optional(),\n // Nullable: the bulk owner-update worker passes `null` to clear ownership.\n // Without `.nullable()`, dealUpdateSchema.parse({ ownerUserId: null }) throws\n // ZodError \"expected string, received null\" inside the queue worker (TC-CRM-069).\n ownerUserId: uuid().optional().nullable(),\n source: z.string().max(150).optional(),\n closureOutcome: z.enum(['won', 'lost']).optional(),\n lossReasonId: uuid().optional(),\n lossNotes: z.string().max(4000).optional(),\n companyIds: z.array(uuid()).optional(),\n personIds: z.array(uuid()).optional(),\n})\n\nexport const dealUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(dealCreateSchema.partial())\n\n// Bulk update schemas \u2014 used by `api/deals/bulk-update-{owner,stage}/route.ts`. Kept here\n// so all deal-write contracts live next to `dealCreateSchema` / `dealUpdateSchema`.\nexport const dealsBulkUpdateOwnerSchema = z.object({\n ids: z.array(uuid()).min(1).max(10000),\n ownerUserId: uuid().nullable(),\n})\n\nexport const dealsBulkUpdateStageSchema = z.object({\n ids: z.array(uuid()).min(1).max(10000),\n pipelineStageId: uuid(),\n})\n\nexport const dealsBulkUpdateResponseSchema = z.object({\n ok: z.boolean(),\n progressJobId: uuid().nullable(),\n message: z.string(),\n})\n\nexport const activityCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n activityType: z.string().min(1).max(100),\n subject: z.string().max(200).optional(),\n body: z.string().max(8000).optional(),\n date: z.string().trim().min(1, ACTIVITY_DATE_REQUIRED_MESSAGE_KEY).optional(),\n time: z.string().trim().min(1, ACTIVITY_TIME_REQUIRED_MESSAGE_KEY).optional(),\n phoneNumber: interactionPhoneNumberSchema,\n occurredAt: z.coerce.date().optional(),\n dealId: uuid().optional(),\n authorUserId: uuid().optional(),\n appearanceIcon: z.string().trim().max(100).optional().nullable(),\n appearanceColor: z\n .string()\n .trim()\n .regex(/^#([0-9a-fA-F]{6})$/)\n .optional()\n .nullable(),\n})\n\nexport const activityUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(activityCreateSchema.partial())\n\nexport const commentCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n dealId: uuid().optional(),\n body: z.string().min(1).max(8000),\n authorUserId: uuid().optional(),\n appearanceIcon: z.string().trim().max(100).optional().nullable(),\n appearanceColor: z\n .string()\n .trim()\n .regex(/^#([0-9a-fA-F]{6})$/)\n .optional()\n .nullable(),\n})\n\nexport const commentUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(commentCreateSchema.partial())\n\nexport const addressCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n name: z.string().max(150).optional(),\n purpose: z.string().max(150).optional(),\n companyName: z.string().max(200).optional(),\n addressLine1: z.string().min(1).max(300),\n addressLine2: z.string().max(300).optional(),\n buildingNumber: z.string().max(50).optional(),\n flatNumber: z.string().max(50).optional(),\n city: z.string().max(150).optional(),\n region: z.string().max(150).optional(),\n postalCode: z.string().max(30).optional(),\n country: z.string().max(150).optional(),\n latitude: z.coerce\n .number()\n .min(COORDINATE_RANGES.latitude.min)\n .max(COORDINATE_RANGES.latitude.max)\n .nullable()\n .optional(),\n longitude: z.coerce\n .number()\n .min(COORDINATE_RANGES.longitude.min)\n .max(COORDINATE_RANGES.longitude.max)\n .nullable()\n .optional(),\n isPrimary: z.boolean().optional(),\n})\n\nexport const addressUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(addressCreateSchema.partial())\n\nexport const tagCreateSchema = scopedSchema.extend({\n slug: z\n .string()\n .min(1)\n .max(80)\n .regex(/^[a-z0-9_-]+$/, 'Slug must be lowercase and may contain dashes or underscores'),\n label: z.string().min(1).max(120),\n color: z.string().max(30).optional(),\n description: z.string().max(400).optional(),\n})\n\nexport const tagUpdateSchema = z\n .object({\n id: uuid(),\n })\n .merge(tagCreateSchema.partial())\n\nconst KNOWN_DICTIONARY_KINDS = [\n 'status',\n 'source',\n 'lifecycle_stage',\n 'address_type',\n 'activity_type',\n 'deal_status',\n 'pipeline_stage',\n 'job_title',\n 'industry',\n 'temperature',\n 'renewal_quarter',\n 'person_company_role',\n] as const\nconst CUSTOM_DICTIONARY_KIND_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/\nconst dictionaryKindEnum = z.string().trim().refine(\n (value) =>\n (KNOWN_DICTIONARY_KINDS as readonly string[]).includes(value) ||\n CUSTOM_DICTIONARY_KIND_PATTERN.test(value),\n { message: 'Unsupported dictionary kind' },\n)\n\nconst dictionaryValueSchema = z.string().trim().min(1).max(150)\nconst dictionaryLabelSchema = z.string().trim().max(150)\n// Pipeline-stage rows migrated to semantic tone identifiers in\n// Migration20260519120000_pipeline_stage_color_tones; AddStageDialog now writes those\n// directly. Other dictionary kinds still store hex. Accept either format so round-tripping\n// a migrated pipeline-stage entry through the dictionary edit UI doesn't fail validation.\nconst DICTIONARY_COLOR_TONES = ['success', 'warning', 'info', 'error', 'neutral', 'brand', 'pink'] as const\nconst dictionaryColorSchema = z\n .string()\n .trim()\n .regex(\n new RegExp(`^(#[0-9a-fA-F]{6}|${DICTIONARY_COLOR_TONES.join('|')})$`),\n 'Color must be a six-digit hex code (e.g. #3366ff) or a semantic tone identifier',\n )\nconst dictionaryIconSchema = z.string().trim().max(48)\n\nexport const customerDictionaryEntryCreateSchema = scopedSchema.extend({\n kind: dictionaryKindEnum,\n value: dictionaryValueSchema,\n label: dictionaryLabelSchema.optional(),\n color: dictionaryColorSchema.nullable().optional(),\n icon: dictionaryIconSchema.nullable().optional(),\n})\n\nexport type CustomerDictionaryEntryCreateInput = z.infer<typeof customerDictionaryEntryCreateSchema>\n\nexport const customerDictionaryEntryUpdateSchema = scopedSchema\n .extend({\n id: uuid(),\n kind: dictionaryKindEnum,\n value: dictionaryValueSchema.optional(),\n label: dictionaryLabelSchema.optional(),\n color: dictionaryColorSchema.nullable().optional(),\n icon: dictionaryIconSchema.nullable().optional(),\n })\n .refine(\n (payload) =>\n payload.value !== undefined ||\n payload.label !== undefined ||\n payload.color !== undefined ||\n payload.icon !== undefined,\n {\n message: 'Provide at least one field to update.',\n path: ['value'],\n }\n )\n\nexport type CustomerDictionaryEntryUpdateInput = z.infer<typeof customerDictionaryEntryUpdateSchema>\n\nexport const customerDictionaryEntryDeleteSchema = scopedSchema.extend({\n id: uuid(),\n kind: dictionaryKindEnum,\n})\n\nexport type CustomerDictionaryEntryDeleteInput = z.infer<typeof customerDictionaryEntryDeleteSchema>\n\nexport const tagAssignmentSchema = scopedSchema.extend({\n tagId: uuid(),\n entityId: uuid(),\n})\n\nexport const todoLinkCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n todoId: uuid(),\n todoSource: z.string().min(1).max(120).default('customers:interaction'),\n createdByUserId: uuid().optional(),\n})\n\nexport const todoLinkWithTodoCreateSchema = scopedSchema.extend({\n entityId: uuid(),\n title: z.string().min(1).max(200),\n isDone: z.boolean().optional(),\n is_done: z.boolean().optional(),\n todoSource: z.string().min(1).max(120).default('customers:interaction'),\n createdByUserId: uuid().optional(),\n todoCustom: z.record(z.string(), z.any()).optional(),\n custom: z.record(z.string(), z.any()).optional(),\n})\n\n// --- Interaction schemas ---\n\n/**\n * @deprecated Interaction statuses are now dictionary-backed and tenant-configurable\n * (the `interaction-statuses` dictionary). This frozen 3-value list is kept only for\n * backward compatibility; it is no longer the validation source (the API accepts any\n * `z.string().max(50)`). For open/terminal semantics use `lib/interactionStatus.ts`\n * (`isOpenInteractionStatus` / `isTerminalInteractionStatus` / `INTERACTION_STATUS_*`);\n * for the seeded default set use `INTERACTION_STATUS_DEFAULTS` in `cli.ts`. Do not expand\n * this list \u2014 adding members would break exhaustive consumers.\n */\nexport const interactionStatusValues = ['planned', 'done', 'canceled'] as const\n/** @deprecated See {@link interactionStatusValues}. */\nexport type InteractionStatus = typeof interactionStatusValues[number]\n\nconst interactionParticipantSchema = z.object({\n userId: z.string().uuid(),\n name: z.string().trim().max(200).optional(),\n email: z.string().trim().max(320).optional(),\n status: z.string().trim().max(50).optional(),\n})\n\nconst interactionLinkedEntitySchema = z.object({\n id: z.string().uuid(),\n type: z.enum(['company', 'deal', 'offer']),\n label: z.string().trim().max(500),\n})\n\nconst interactionGuestPermissionsSchema = z\n .object({\n canInviteOthers: z.boolean().optional(),\n canModify: z.boolean().optional(),\n canSeeList: z.boolean().optional(),\n })\n .strict()\n\nconst interactionExtendedFields = {\n durationMinutes: z.number().int().min(0).optional().nullable(),\n location: z.string().trim().max(500).optional().nullable(),\n allDay: z.boolean().optional().nullable(),\n recurrenceRule: z.string().trim().max(500).optional().nullable(),\n recurrenceEnd: z.coerce.date().optional().nullable(),\n participants: z.array(interactionParticipantSchema).optional().nullable(),\n reminderMinutes: z.number().int().min(0).optional().nullable(),\n visibility: z.string().trim().max(50).optional().nullable(),\n linkedEntities: z.array(interactionLinkedEntitySchema).optional().nullable(),\n guestPermissions: interactionGuestPermissionsSchema.optional().nullable(),\n} as const\n\nconst interactionCreateBaseSchema = scopedSchema.extend({\n id: z.string().uuid().optional(),\n entityId: z.string().uuid(),\n interactionType: z.string().trim().min(1).max(100),\n title: z.string().trim().max(500).optional().nullable(),\n body: z.string().trim().max(10000).optional().nullable(),\n // Lenient like `deal_status` (status: z.string().max(50)). The `interaction-statuses`\n // dictionary drives the UI dropdown; the API accepts any string <=50 chars so existing\n // rows, external writers, and the dispatch-crm MCP keep working. Open/terminal semantics\n // live in lib/interactionStatus.ts, not in this validator.\n status: z.string().max(50).optional().default('planned'),\n date: z.string().trim().min(1, ACTIVITY_DATE_REQUIRED_MESSAGE_KEY).optional(),\n time: z.string().trim().min(1, ACTIVITY_TIME_REQUIRED_MESSAGE_KEY).optional(),\n phoneNumber: interactionPhoneNumberSchema,\n scheduledAt: z.coerce.date().optional().nullable(),\n occurredAt: z.coerce.date().optional().nullable(),\n priority: z.number().int().min(0).max(100).optional().nullable(),\n authorUserId: z.string().uuid().optional().nullable(),\n ownerUserId: z.string().uuid().optional().nullable(),\n dealId: z.string().uuid().optional().nullable(),\n appearanceIcon: z.string().trim().max(100).optional().nullable(),\n appearanceColor: z.string().trim().regex(/^#([0-9a-fA-F]{6})$/).optional().nullable(),\n source: z.string().trim().max(100).optional().nullable(),\n ...interactionExtendedFields,\n})\n\nfunction deriveScheduledAtFromDateTime(date?: string, time?: string): Date | null {\n if (!date || typeof date !== 'string') return null\n const trimmedDate = date.trim()\n if (!trimmedDate) return null\n const trimmedTime = typeof time === 'string' ? time.trim() : ''\n const iso = trimmedTime ? `${trimmedDate}T${trimmedTime}:00` : `${trimmedDate}T00:00:00`\n const parsed = new Date(iso)\n return Number.isNaN(parsed.getTime()) ? null : parsed\n}\n\nexport const interactionCreateSchema = interactionCreateBaseSchema\n .superRefine((value, ctx) => {\n if (value.interactionType === 'call' && value.phoneNumber !== undefined && value.phoneNumber !== null) {\n const phone = typeof value.phoneNumber === 'string' ? value.phoneNumber.trim() : ''\n if (!phone) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['phoneNumber'],\n message: ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY,\n })\n } else if (!isValidPhoneNumber(phone)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['phoneNumber'],\n message: ACTIVITY_PHONE_INVALID_MESSAGE_KEY,\n })\n }\n }\n })\n // Derive `scheduledAt` from `date+time` when only the latter are sent so\n // external API consumers don't silently persist `scheduled_at: null` after\n // the validator already enforced non-empty date/time. The form already\n // computes `scheduledAt` itself, so this branch is a no-op for the form path.\n .transform((value) => {\n if (value.scheduledAt) return value\n const derived = deriveScheduledAtFromDateTime(value.date, value.time)\n return derived ? { ...value, scheduledAt: derived } : value\n })\n\nexport type InteractionCreateInput = z.infer<typeof interactionCreateSchema>\n\nconst interactionUpdateBaseSchema = z\n .object({\n id: z.string().uuid(),\n })\n .merge(\n scopedSchema\n .extend({\n interactionType: z.string().trim().min(1).max(100).optional(),\n title: z.string().trim().max(500).optional().nullable(),\n body: z.string().trim().max(10000).optional().nullable(),\n status: z.string().max(50).optional(),\n date: z.string().trim().min(1, ACTIVITY_DATE_REQUIRED_MESSAGE_KEY).optional(),\n time: z.string().trim().min(1, ACTIVITY_TIME_REQUIRED_MESSAGE_KEY).optional(),\n phoneNumber: interactionPhoneNumberSchema,\n scheduledAt: z.coerce.date().optional().nullable(),\n occurredAt: z.coerce.date().optional().nullable(),\n priority: z.number().int().min(0).max(100).optional().nullable(),\n authorUserId: z.string().uuid().optional().nullable(),\n ownerUserId: z.string().uuid().optional().nullable(),\n dealId: z.string().uuid().optional().nullable(),\n appearanceIcon: z.string().trim().max(100).optional().nullable(),\n appearanceColor: z.string().trim().regex(/^#([0-9a-fA-F]{6})$/).optional().nullable(),\n pinned: z.boolean().optional(),\n ...interactionExtendedFields,\n })\n .partial(),\n )\n\nexport const interactionUpdateSchema = interactionUpdateBaseSchema\n .superRefine((value, ctx) => {\n if (value.interactionType === 'call' && value.phoneNumber !== undefined) {\n const phone = typeof value.phoneNumber === 'string' ? value.phoneNumber.trim() : ''\n if (!phone) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['phoneNumber'],\n message: ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY,\n })\n } else if (!isValidPhoneNumber(phone)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['phoneNumber'],\n message: ACTIVITY_PHONE_INVALID_MESSAGE_KEY,\n })\n }\n }\n })\n // Mirror the create-schema derivation for partial updates: when an external\n // caller supplies `date+time` without `scheduledAt`, derive the timestamp so\n // the update doesn't silently leave `scheduled_at` stale.\n .transform((value) => {\n if (value.scheduledAt !== undefined) return value\n if (!value.date && !value.time) return value\n const derived = deriveScheduledAtFromDateTime(value.date, value.time)\n return derived ? { ...value, scheduledAt: derived } : value\n })\n\nexport type InteractionUpdateInput = z.infer<typeof interactionUpdateSchema>\n\nexport const interactionCompleteSchema = z.object({\n id: z.string().uuid(),\n occurredAt: z.coerce.date().optional(),\n})\n\nexport const interactionCancelSchema = z.object({\n id: z.string().uuid(),\n})\n\nexport const customerAddressFormatSchema = z.enum(['line_first', 'street_first'])\n\nexport const customerSettingsUpsertSchema = scopedSchema.extend({\n addressFormat: customerAddressFormatSchema,\n})\n\nexport const customerStuckThresholdUpsertSchema = scopedSchema.extend({\n stuckThresholdDays: z.number().int().min(1).max(365),\n})\n\nexport const customerDictionarySortModesSchema = z.record(z.string(), dictionaryEntrySortModeSchema)\n\nexport const customerDictionarySortModesUpsertSchema = scopedSchema.extend({\n dictionarySortModes: customerDictionarySortModesSchema,\n})\n\nexport type PersonCreateInput = z.infer<typeof personCreateSchema>\nexport type PersonUpdateInput = z.infer<typeof personUpdateSchema>\nexport type CompanyCreateInput = z.infer<typeof companyCreateSchema>\nexport type CompanyUpdateInput = z.infer<typeof companyUpdateSchema>\nexport type DealCreateInput = z.infer<typeof dealCreateSchema>\nexport type DealUpdateInput = z.infer<typeof dealUpdateSchema>\nexport type ActivityCreateInput = z.infer<typeof activityCreateSchema>\nexport type ActivityUpdateInput = z.infer<typeof activityUpdateSchema>\nexport type CommentCreateInput = z.infer<typeof commentCreateSchema>\nexport type CommentUpdateInput = z.infer<typeof commentUpdateSchema>\nexport type AddressCreateInput = z.infer<typeof addressCreateSchema>\nexport type AddressUpdateInput = z.infer<typeof addressUpdateSchema>\nexport type TagCreateInput = z.infer<typeof tagCreateSchema>\nexport type TagUpdateInput = z.infer<typeof tagUpdateSchema>\nexport type TagAssignmentInput = z.infer<typeof tagAssignmentSchema>\nexport type TodoLinkCreateInput = z.infer<typeof todoLinkCreateSchema>\nexport type TodoLinkWithTodoCreateInput = z.infer<typeof todoLinkWithTodoCreateSchema>\nexport type CustomerSettingsUpsertInput = z.infer<typeof customerSettingsUpsertSchema>\nexport type CustomerStuckThresholdUpsertInput = z.infer<typeof customerStuckThresholdUpsertSchema>\nexport type CustomerDictionarySortModesUpsertInput = z.infer<typeof customerDictionarySortModesUpsertSchema>\nexport type CustomerAddressFormatInput = z.infer<typeof customerAddressFormatSchema>\nexport type InteractionCompleteInput = z.infer<typeof interactionCompleteSchema>\nexport type InteractionCancelInput = z.infer<typeof interactionCancelSchema>\n\n// --- Pipeline schemas ---\n\nexport const pipelineCreateSchema = scopedSchema.extend({\n name: z.string().trim().min(1).max(200),\n isDefault: z.boolean().optional(),\n})\n\nexport const pipelineUpdateSchema = z.object({\n id: uuid(),\n name: z.string().trim().min(1).max(200).optional(),\n isDefault: z.boolean().optional(),\n})\n\nexport const pipelineDeleteSchema = z.object({\n id: uuid(),\n})\n\nexport type PipelineCreateInput = z.infer<typeof pipelineCreateSchema>\nexport type PipelineUpdateInput = z.infer<typeof pipelineUpdateSchema>\nexport type PipelineDeleteInput = z.infer<typeof pipelineDeleteSchema>\n\n// --- Pipeline Stage schemas ---\n\nexport const pipelineStageCreateSchema = scopedSchema.extend({\n pipelineId: uuid(),\n label: z.string().trim().min(1).max(200),\n order: z.number().int().min(0).optional(),\n color: z.string().trim().max(20).nullish(),\n icon: z.string().trim().max(100).nullish(),\n})\n\nexport const pipelineStageUpdateSchema = z.object({\n id: uuid(),\n label: z.string().trim().min(1).max(200).optional(),\n order: z.number().int().min(0).optional(),\n color: z.string().trim().max(20).nullish(),\n icon: z.string().trim().max(100).nullish(),\n})\n\nexport const pipelineStageDeleteSchema = z.object({\n id: uuid(),\n})\n\nexport const pipelineStageReorderSchema = scopedSchema.extend({\n stages: z.array(z.object({\n id: uuid(),\n order: z.number().int().min(0),\n })).min(1),\n})\n\nexport type PipelineStageCreateInput = z.infer<typeof pipelineStageCreateSchema>\nexport type PipelineStageUpdateInput = z.infer<typeof pipelineStageUpdateSchema>\nexport type PipelineStageDeleteInput = z.infer<typeof pipelineStageDeleteSchema>\nexport type PipelineStageReorderInput = z.infer<typeof pipelineStageReorderSchema>\n\nexport const entityRoleCreateSchema = scopedSchema.extend({\n entityType: z.enum(['company', 'person']),\n entityId: uuid(),\n roleType: z.string().trim().min(1).max(100),\n userId: uuid(),\n})\n\nexport const entityRoleUpdateSchema = scopedSchema.extend({\n id: uuid(),\n userId: uuid(),\n})\n\nexport const entityRoleDeleteSchema = scopedSchema.extend({\n id: uuid(),\n})\n\nexport type EntityRoleCreateInput = z.infer<typeof entityRoleCreateSchema>\nexport type EntityRoleUpdateInput = z.infer<typeof entityRoleUpdateSchema>\nexport type EntityRoleDeleteInput = z.infer<typeof entityRoleDeleteSchema>\n\nexport const updateKindSettingSchema = z.object({\n kind: z.string().trim().min(1).max(100),\n selectionMode: z.enum(['single', 'multi']).optional(),\n visibleInTags: z.boolean().optional(),\n sortOrder: z.number().int().min(0).optional(),\n})\n\nexport type UpdateKindSettingInput = z.infer<typeof updateKindSettingSchema>\n\nexport const customerKindSettingsUpsertSchema = scopedSchema.extend({\n kind: z.string().trim().min(1).max(100),\n selectionMode: z.enum(['single', 'multi']).optional(),\n visibleInTags: z.boolean().optional(),\n sortOrder: z.number().int().min(0).optional(),\n})\n\nexport type CustomerKindSettingsUpsertInput = z.infer<typeof customerKindSettingsUpsertSchema>\n\nexport const labelCreateSchema = z.object({\n label: z.string().trim().min(1).max(120),\n slug: z.string().trim().min(1).max(80).regex(/^[a-z0-9_-]+$/).optional(),\n})\n\nexport type LabelCreateInput = z.infer<typeof labelCreateSchema>\n\nexport const labelCreateCommandSchema = scopedSchema.extend({\n label: z.string().trim().min(1).max(120),\n slug: z.string().trim().min(1).max(80).regex(/^[a-z0-9_-]+$/),\n userId: uuid(),\n})\n\nexport type LabelCreateCommandInput = z.infer<typeof labelCreateCommandSchema>\n\nexport const labelAssignmentSchema = z.object({\n labelId: z.string().uuid(),\n entityId: z.string().uuid(),\n})\n\nexport type LabelAssignmentInput = z.infer<typeof labelAssignmentSchema>\n\nexport const labelAssignCommandSchema = scopedSchema.extend({\n labelId: uuid(),\n entityId: uuid(),\n})\n\nexport const labelUnassignCommandSchema = scopedSchema.extend({\n labelId: uuid(),\n entityId: uuid(),\n})\n\nexport type LabelAssignCommandInput = z.infer<typeof labelAssignCommandSchema>\nexport type LabelUnassignCommandInput = z.infer<typeof labelUnassignCommandSchema>\n\nexport const personCompanyLinkCreateSchema = scopedSchema.extend({\n personEntityId: uuid(),\n companyEntityId: uuid(),\n isPrimary: z.boolean().optional(),\n})\n\nexport const personCompanyLinkUpdateSchema = scopedSchema.extend({\n linkId: uuid(),\n isPrimary: z.boolean(),\n})\n\nexport const personCompanyLinkDeleteSchema = scopedSchema.extend({\n linkId: uuid(),\n})\n\nexport type PersonCompanyLinkCreateInput = z.infer<typeof personCompanyLinkCreateSchema>\nexport type PersonCompanyLinkUpdateInput = z.infer<typeof personCompanyLinkUpdateSchema>\nexport type PersonCompanyLinkDeleteInput = z.infer<typeof personCompanyLinkDeleteSchema>\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -19,7 +19,8 @@ const CUSTOMER_DICTIONARY_KINDS = [
19
19
  "industries",
20
20
  "temperature",
21
21
  "renewal-quarters",
22
- "person-company-roles"
22
+ "person-company-roles",
23
+ "interaction-statuses"
23
24
  ];
24
25
  const CUSTOMER_DICTIONARIES_MANAGE_HREF = "/backend/config/customers";
25
26
  function getCustomerDictionarySettingsSectionId(kind) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/lib/dictionaries.ts"],
4
- "sourcesContent": ["import {\n ICON_LIBRARY,\n ICON_SUGGESTIONS,\n type IconOption,\n type DictionaryDisplayEntry,\n type DictionaryMap,\n createDictionaryMap,\n normalizeDictionaryEntries,\n DictionaryValue,\n renderDictionaryColor,\n renderDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\n\nexport const CUSTOMER_DICTIONARY_KINDS = [\n 'statuses',\n 'sources',\n 'lifecycle-stages',\n 'address-types',\n 'activity-types',\n 'deal-statuses',\n 'pipeline-stages',\n 'job-titles',\n 'industries',\n 'temperature',\n 'renewal-quarters',\n 'person-company-roles',\n] as const\n\nexport type CustomerDictionaryKind = typeof CUSTOMER_DICTIONARY_KINDS[number]\nexport type CustomerDictionaryDisplayEntry = DictionaryDisplayEntry\nexport type CustomerDictionaryMap = DictionaryMap\n\nexport const CUSTOMER_DICTIONARIES_MANAGE_HREF = '/backend/config/customers'\n\nexport function getCustomerDictionarySettingsSectionId(kind: CustomerDictionaryKind) {\n return `customer-dictionary-${kind}`\n}\n\nexport function getCustomerDictionaryManageHref(kind: CustomerDictionaryKind) {\n return `${CUSTOMER_DICTIONARIES_MANAGE_HREF}#${getCustomerDictionarySettingsSectionId(kind)}`\n}\n\nexport function createEmptyCustomerDictionaryMaps(): Record<CustomerDictionaryKind, CustomerDictionaryMap> {\n return CUSTOMER_DICTIONARY_KINDS.reduce<Record<CustomerDictionaryKind, CustomerDictionaryMap>>(\n (acc, kind) => {\n acc[kind] = {}\n return acc\n },\n {} as Record<CustomerDictionaryKind, CustomerDictionaryMap>,\n )\n}\n\nexport {\n ICON_LIBRARY,\n ICON_SUGGESTIONS,\n type IconOption,\n DictionaryValue,\n createDictionaryMap,\n normalizeDictionaryEntries,\n renderDictionaryColor,\n renderDictionaryIcon,\n}\n"],
5
- "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,4BAA4B;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,MAAM,oCAAoC;AAE1C,SAAS,uCAAuC,MAA8B;AACnF,SAAO,uBAAuB,IAAI;AACpC;AAEO,SAAS,gCAAgC,MAA8B;AAC5E,SAAO,GAAG,iCAAiC,IAAI,uCAAuC,IAAI,CAAC;AAC7F;AAEO,SAAS,oCAA2F;AACzG,SAAO,0BAA0B;AAAA,IAC/B,CAAC,KAAK,SAAS;AACb,UAAI,IAAI,IAAI,CAAC;AACb,aAAO;AAAA,IACT;AAAA,IACA,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import {\n ICON_LIBRARY,\n ICON_SUGGESTIONS,\n type IconOption,\n type DictionaryDisplayEntry,\n type DictionaryMap,\n createDictionaryMap,\n normalizeDictionaryEntries,\n DictionaryValue,\n renderDictionaryColor,\n renderDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\n\nexport const CUSTOMER_DICTIONARY_KINDS = [\n 'statuses',\n 'sources',\n 'lifecycle-stages',\n 'address-types',\n 'activity-types',\n 'deal-statuses',\n 'pipeline-stages',\n 'job-titles',\n 'industries',\n 'temperature',\n 'renewal-quarters',\n 'person-company-roles',\n 'interaction-statuses',\n] as const\n\nexport type CustomerDictionaryKind = typeof CUSTOMER_DICTIONARY_KINDS[number]\nexport type CustomerDictionaryDisplayEntry = DictionaryDisplayEntry\nexport type CustomerDictionaryMap = DictionaryMap\n\nexport const CUSTOMER_DICTIONARIES_MANAGE_HREF = '/backend/config/customers'\n\nexport function getCustomerDictionarySettingsSectionId(kind: CustomerDictionaryKind) {\n return `customer-dictionary-${kind}`\n}\n\nexport function getCustomerDictionaryManageHref(kind: CustomerDictionaryKind) {\n return `${CUSTOMER_DICTIONARIES_MANAGE_HREF}#${getCustomerDictionarySettingsSectionId(kind)}`\n}\n\nexport function createEmptyCustomerDictionaryMaps(): Record<CustomerDictionaryKind, CustomerDictionaryMap> {\n return CUSTOMER_DICTIONARY_KINDS.reduce<Record<CustomerDictionaryKind, CustomerDictionaryMap>>(\n (acc, kind) => {\n acc[kind] = {}\n return acc\n },\n {} as Record<CustomerDictionaryKind, CustomerDictionaryMap>,\n )\n}\n\nexport {\n ICON_LIBRARY,\n ICON_SUGGESTIONS,\n type IconOption,\n DictionaryValue,\n createDictionaryMap,\n normalizeDictionaryEntries,\n renderDictionaryColor,\n renderDictionaryIcon,\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,4BAA4B;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,MAAM,oCAAoC;AAE1C,SAAS,uCAAuC,MAA8B;AACnF,SAAO,uBAAuB,IAAI;AACpC;AAEO,SAAS,gCAAgC,MAA8B;AAC5E,SAAO,GAAG,iCAAiC,IAAI,uCAAuC,IAAI,CAAC;AAC7F;AAEO,SAAS,oCAA2F;AACzG,SAAO,0BAA0B;AAAA,IAC/B,CAAC,KAAK,SAAS;AACb,UAAI,IAAI,IAAI,CAAC;AACb,aAAO;AAAA,IACT;AAAA,IACA,CAAC;AAAA,EACH;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { sql } from "kysely";
2
+ import { TERMINAL_INTERACTION_STATUS_LIST } from "./interactionStatus.js";
2
3
  async function recomputeNextInteraction(em, entityId, organizationId) {
3
4
  const db = em.getKysely();
4
5
  let query = db.selectFrom("customer_interactions").select([
@@ -8,7 +9,7 @@ async function recomputeNextInteraction(em, entityId, organizationId) {
8
9
  "interaction_type",
9
10
  "appearance_icon",
10
11
  "appearance_color"
11
- ]).where("entity_id", "=", entityId).where("status", "=", "planned").where("scheduled_at", "is not", null).where("deleted_at", "is", null);
12
+ ]).where("entity_id", "=", entityId).where("status", "not in", [...TERMINAL_INTERACTION_STATUS_LIST]).where("scheduled_at", "is not", null).where("deleted_at", "is", null);
12
13
  if (organizationId) {
13
14
  query = query.where("organization_id", "=", organizationId);
14
15
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/lib/interactionProjection.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\n\nexport type NextInteractionProjectionResult = {\n nextInteractionId: string | null\n}\n\n/**\n * Recomputes the next-interaction projection fields on a CustomerEntity.\n *\n * Algorithm:\n * 1. Find all interactions for entityId where status = 'planned', scheduled_at IS NOT NULL, deleted_at IS NULL\n * 2. Sort by scheduled_at ASC, priority DESC NULLS LAST, created_at ASC, id ASC\n * 3. Take the first row and project its fields onto the CustomerEntity\n * 4. If no candidates, set all projection fields to NULL\n */\nexport async function recomputeNextInteraction(\n em: EntityManager,\n entityId: string,\n organizationId?: string | null,\n): Promise<NextInteractionProjectionResult> {\n const db = em.getKysely<any>() as any\n\n let query = db\n .selectFrom('customer_interactions')\n .select([\n 'id',\n 'scheduled_at',\n 'title',\n 'interaction_type',\n 'appearance_icon',\n 'appearance_color',\n ])\n .where('entity_id', '=', entityId)\n .where('status', '=', 'planned')\n .where('scheduled_at', 'is not', null)\n .where('deleted_at', 'is', null)\n if (organizationId) {\n query = query.where('organization_id', '=', organizationId)\n }\n\n const candidate = await query\n .orderBy('scheduled_at', 'asc')\n .orderBy(sql`priority desc nulls last`)\n .orderBy('created_at', 'asc')\n .orderBy('id', 'asc')\n .executeTakeFirst() as Record<string, any> | undefined\n\n if (candidate) {\n const interactionName = candidate.title && candidate.title.trim() !== ''\n ? candidate.title\n : candidate.interaction_type\n\n await db\n .updateTable('customer_entities')\n .set({\n next_interaction_at: candidate.scheduled_at,\n next_interaction_name: interactionName,\n next_interaction_ref_id: candidate.id,\n next_interaction_icon: candidate.appearance_icon,\n next_interaction_color: candidate.appearance_color,\n updated_at: sql`now()`,\n } as any)\n .where('id', '=', entityId)\n .execute()\n return { nextInteractionId: String(candidate.id) }\n } else {\n await db\n .updateTable('customer_entities')\n .set({\n next_interaction_at: null,\n next_interaction_name: null,\n next_interaction_ref_id: null,\n next_interaction_icon: null,\n next_interaction_color: null,\n updated_at: sql`now()`,\n } as any)\n .where('id', '=', entityId)\n .execute()\n return { nextInteractionId: null }\n }\n}\n"],
5
- "mappings": "AACA,SAAS,WAAW;AAepB,eAAsB,yBACpB,IACA,UACA,gBAC0C;AAC1C,QAAM,KAAK,GAAG,UAAe;AAE7B,MAAI,QAAQ,GACT,WAAW,uBAAuB,EAClC,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA,MAAM,aAAa,KAAK,QAAQ,EAChC,MAAM,UAAU,KAAK,SAAS,EAC9B,MAAM,gBAAgB,UAAU,IAAI,EACpC,MAAM,cAAc,MAAM,IAAI;AACjC,MAAI,gBAAgB;AAClB,YAAQ,MAAM,MAAM,mBAAmB,KAAK,cAAc;AAAA,EAC5D;AAEA,QAAM,YAAY,MAAM,MACrB,QAAQ,gBAAgB,KAAK,EAC7B,QAAQ,6BAA6B,EACrC,QAAQ,cAAc,KAAK,EAC3B,QAAQ,MAAM,KAAK,EACnB,iBAAiB;AAEpB,MAAI,WAAW;AACb,UAAM,kBAAkB,UAAU,SAAS,UAAU,MAAM,KAAK,MAAM,KAClE,UAAU,QACV,UAAU;AAEd,UAAM,GACH,YAAY,mBAAmB,EAC/B,IAAI;AAAA,MACH,qBAAqB,UAAU;AAAA,MAC/B,uBAAuB;AAAA,MACvB,yBAAyB,UAAU;AAAA,MACnC,uBAAuB,UAAU;AAAA,MACjC,wBAAwB,UAAU;AAAA,MAClC,YAAY;AAAA,IACd,CAAQ,EACP,MAAM,MAAM,KAAK,QAAQ,EACzB,QAAQ;AACX,WAAO,EAAE,mBAAmB,OAAO,UAAU,EAAE,EAAE;AAAA,EACnD,OAAO;AACL,UAAM,GACH,YAAY,mBAAmB,EAC/B,IAAI;AAAA,MACH,qBAAqB;AAAA,MACrB,uBAAuB;AAAA,MACvB,yBAAyB;AAAA,MACzB,uBAAuB;AAAA,MACvB,wBAAwB;AAAA,MACxB,YAAY;AAAA,IACd,CAAQ,EACP,MAAM,MAAM,KAAK,QAAQ,EACzB,QAAQ;AACX,WAAO,EAAE,mBAAmB,KAAK;AAAA,EACnC;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { TERMINAL_INTERACTION_STATUS_LIST } from './interactionStatus'\n\nexport type NextInteractionProjectionResult = {\n nextInteractionId: string | null\n}\n\n/**\n * Recomputes the next-interaction projection fields on a CustomerEntity.\n *\n * Algorithm:\n * 1. Find all OPEN interactions for entityId (status NOT IN terminal \u2014 see lib/interactionStatus.ts),\n * scheduled_at IS NOT NULL, deleted_at IS NULL. A scheduled task that moves planned -> in_progress\n * stays the next step; this projection only tracks scheduled next-steps, so undated open tasks are\n * deliberately excluded (next_interaction_at must stay a real date for grid sort / search index).\n * 2. Sort by scheduled_at ASC, priority DESC NULLS LAST, created_at ASC, id ASC\n * 3. Take the first row and project its fields onto the CustomerEntity\n * 4. If no candidates, set all projection fields to NULL\n */\nexport async function recomputeNextInteraction(\n em: EntityManager,\n entityId: string,\n organizationId?: string | null,\n): Promise<NextInteractionProjectionResult> {\n const db = em.getKysely<any>() as any\n\n let query = db\n .selectFrom('customer_interactions')\n .select([\n 'id',\n 'scheduled_at',\n 'title',\n 'interaction_type',\n 'appearance_icon',\n 'appearance_color',\n ])\n .where('entity_id', '=', entityId)\n .where('status', 'not in', [...TERMINAL_INTERACTION_STATUS_LIST])\n .where('scheduled_at', 'is not', null)\n .where('deleted_at', 'is', null)\n if (organizationId) {\n query = query.where('organization_id', '=', organizationId)\n }\n\n const candidate = await query\n .orderBy('scheduled_at', 'asc')\n .orderBy(sql`priority desc nulls last`)\n .orderBy('created_at', 'asc')\n .orderBy('id', 'asc')\n .executeTakeFirst() as Record<string, any> | undefined\n\n if (candidate) {\n const interactionName = candidate.title && candidate.title.trim() !== ''\n ? candidate.title\n : candidate.interaction_type\n\n await db\n .updateTable('customer_entities')\n .set({\n next_interaction_at: candidate.scheduled_at,\n next_interaction_name: interactionName,\n next_interaction_ref_id: candidate.id,\n next_interaction_icon: candidate.appearance_icon,\n next_interaction_color: candidate.appearance_color,\n updated_at: sql`now()`,\n } as any)\n .where('id', '=', entityId)\n .execute()\n return { nextInteractionId: String(candidate.id) }\n } else {\n await db\n .updateTable('customer_entities')\n .set({\n next_interaction_at: null,\n next_interaction_name: null,\n next_interaction_ref_id: null,\n next_interaction_icon: null,\n next_interaction_color: null,\n updated_at: sql`now()`,\n } as any)\n .where('id', '=', entityId)\n .execute()\n return { nextInteractionId: null }\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,WAAW;AACpB,SAAS,wCAAwC;AAkBjD,eAAsB,yBACpB,IACA,UACA,gBAC0C;AAC1C,QAAM,KAAK,GAAG,UAAe;AAE7B,MAAI,QAAQ,GACT,WAAW,uBAAuB,EAClC,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA,MAAM,aAAa,KAAK,QAAQ,EAChC,MAAM,UAAU,UAAU,CAAC,GAAG,gCAAgC,CAAC,EAC/D,MAAM,gBAAgB,UAAU,IAAI,EACpC,MAAM,cAAc,MAAM,IAAI;AACjC,MAAI,gBAAgB;AAClB,YAAQ,MAAM,MAAM,mBAAmB,KAAK,cAAc;AAAA,EAC5D;AAEA,QAAM,YAAY,MAAM,MACrB,QAAQ,gBAAgB,KAAK,EAC7B,QAAQ,6BAA6B,EACrC,QAAQ,cAAc,KAAK,EAC3B,QAAQ,MAAM,KAAK,EACnB,iBAAiB;AAEpB,MAAI,WAAW;AACb,UAAM,kBAAkB,UAAU,SAAS,UAAU,MAAM,KAAK,MAAM,KAClE,UAAU,QACV,UAAU;AAEd,UAAM,GACH,YAAY,mBAAmB,EAC/B,IAAI;AAAA,MACH,qBAAqB,UAAU;AAAA,MAC/B,uBAAuB;AAAA,MACvB,yBAAyB,UAAU;AAAA,MACnC,uBAAuB,UAAU;AAAA,MACjC,wBAAwB,UAAU;AAAA,MAClC,YAAY;AAAA,IACd,CAAQ,EACP,MAAM,MAAM,KAAK,QAAQ,EACzB,QAAQ;AACX,WAAO,EAAE,mBAAmB,OAAO,UAAU,EAAE,EAAE;AAAA,EACnD,OAAO;AACL,UAAM,GACH,YAAY,mBAAmB,EAC/B,IAAI;AAAA,MACH,qBAAqB;AAAA,MACrB,uBAAuB;AAAA,MACvB,yBAAyB;AAAA,MACzB,uBAAuB;AAAA,MACvB,wBAAwB;AAAA,MACxB,YAAY;AAAA,IACd,CAAQ,EACP,MAAM,MAAM,KAAK,QAAQ,EACzB,QAAQ;AACX,WAAO,EAAE,mBAAmB,KAAK;AAAA,EACnC;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,24 @@
1
+ const INTERACTION_STATUS_COMPLETED = "done";
2
+ const INTERACTION_STATUS_CANCELED = "canceled";
3
+ const INTERACTION_STATUS_PLANNED = "planned";
4
+ const TERMINAL_INTERACTION_STATUS_LIST = [
5
+ INTERACTION_STATUS_COMPLETED,
6
+ INTERACTION_STATUS_CANCELED,
7
+ "completed"
8
+ ];
9
+ const TERMINAL_INTERACTION_STATUSES = new Set(TERMINAL_INTERACTION_STATUS_LIST);
10
+ function isTerminalInteractionStatus(value) {
11
+ return value != null && TERMINAL_INTERACTION_STATUSES.has(value);
12
+ }
13
+ function isOpenInteractionStatus(value) {
14
+ return !isTerminalInteractionStatus(value);
15
+ }
16
+ export {
17
+ INTERACTION_STATUS_CANCELED,
18
+ INTERACTION_STATUS_COMPLETED,
19
+ INTERACTION_STATUS_PLANNED,
20
+ TERMINAL_INTERACTION_STATUS_LIST,
21
+ isOpenInteractionStatus,
22
+ isTerminalInteractionStatus
23
+ };
24
+ //# sourceMappingURL=interactionStatus.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customers/lib/interactionStatus.ts"],
4
+ "sourcesContent": ["// Single source of truth for interaction (CRM task) open/terminal semantics.\n//\n// The editable list of statuses lives in the `interaction-statuses` dictionary and\n// is configurable per tenant. The behaviorally-significant question \u2014 does a status\n// mean the task is still open, or is it closed/terminal \u2014 stays in code here so every\n// call site agrees. A status not known to code counts as OPEN (non-terminal): it shows\n// in the open-activities badge and is never auto-treated as completed. That is the safe\n// default and matches the deal open-activities enricher's `status NOT IN (terminal)` filter.\n\nexport const INTERACTION_STATUS_COMPLETED = 'done' as const\nexport const INTERACTION_STATUS_CANCELED = 'canceled' as const\nexport const INTERACTION_STATUS_PLANNED = 'planned' as const\n\n// `completed` is a legacy spelling accepted defensively; `done` is the canonical\n// terminal-success value persisted by the complete action and the email logger.\nexport const TERMINAL_INTERACTION_STATUS_LIST: readonly string[] = [\n INTERACTION_STATUS_COMPLETED,\n INTERACTION_STATUS_CANCELED,\n 'completed',\n]\n\nconst TERMINAL_INTERACTION_STATUSES = new Set<string>(TERMINAL_INTERACTION_STATUS_LIST)\n\nexport function isTerminalInteractionStatus(value: string | null | undefined): boolean {\n return value != null && TERMINAL_INTERACTION_STATUSES.has(value)\n}\n\nexport function isOpenInteractionStatus(value: string | null | undefined): boolean {\n return !isTerminalInteractionStatus(value)\n}\n"],
5
+ "mappings": "AASO,MAAM,+BAA+B;AACrC,MAAM,8BAA8B;AACpC,MAAM,6BAA6B;AAInC,MAAM,mCAAsD;AAAA,EACjE;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,gCAAgC,IAAI,IAAY,gCAAgC;AAE/E,SAAS,4BAA4B,OAA2C;AACrF,SAAO,SAAS,QAAQ,8BAA8B,IAAI,KAAK;AACjE;AAEO,SAAS,wBAAwB,OAA2C;AACjF,SAAO,CAAC,4BAA4B,KAAK;AAC3C;",
6
+ "names": []
7
+ }
@@ -2,6 +2,7 @@ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/
2
2
  import { CustomerEntity, CustomerInteraction } from "../data/entities.js";
3
3
  import { findPeopleByAddresses, normalizeAddresses } from "./findPeopleByAddresses.js";
4
4
  import { emitCustomersEvent } from "../events.js";
5
+ import { INTERACTION_STATUS_COMPLETED } from "./interactionStatus.js";
5
6
  const POSTGRES_UNIQUE_VIOLATION = "23505";
6
7
  async function handler(payload, ctx) {
7
8
  const linkId = payload?.channelLinkId ?? payload?.messageChannelLinkId;
@@ -236,11 +237,9 @@ async function persistInteractions(em, personIdSet, data) {
236
237
  // scheduled work. Without an explicit status the entity default
237
238
  // ('planned') combined with a past `occurredAt` makes the activity
238
239
  // timeline render the email as "overdue", which is the wrong UX.
239
- // The canonical "completed" value is `'done'` per
240
- // `validators.ts:interactionStatusValues = ['planned', 'done', 'canceled']`
241
- // — `'completed'` is a legacy spelling that the enricher accepts
242
- // defensively but the activity timeline `isOverdue` predicate does NOT.
243
- status: "done",
240
+ // The canonical terminal-success value is `INTERACTION_STATUS_COMPLETED`
241
+ // ('done'); see `lib/interactionStatus.ts` for the open/terminal semantics.
242
+ status: INTERACTION_STATUS_COMPLETED,
244
243
  externalMessageId: data.linkId,
245
244
  visibility: data.visibility,
246
245
  channelProviderKey: data.channelProviderKey
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/lib/link-channel-message-handler.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CustomerEntity, CustomerInteraction } from '../data/entities'\nimport { findPeopleByAddresses, normalizeAddresses } from './findPeopleByAddresses'\nimport { emitCustomersEvent } from '../events'\n\n/**\n * Shared implementation for the link-channel-message subscribers.\n *\n * The auto-discovery scanner requires a single string `event` value on each\n * subscriber file. Because we must handle both `communication_channels.message.received`\n * AND `.sent`, we use TWO thin subscriber files (link-channel-message-received.ts\n * and link-channel-message-sent.ts) that both delegate here.\n */\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Payload emitted by `communication_channels.message.received` and\n * `communication_channels.message.sent`.\n *\n * The canonical field is `channelLinkId` (as emitted by the hub). The alias\n * `messageChannelLinkId` is kept for test stubs and legacy compatibility.\n */\ntype LinkChannelMessagePayload = {\n eventType?: string\n /** UUID of the MessageChannelLink row (canonical hub field name). */\n channelLinkId?: string\n /** Alias used in some older stubs / test payloads. Prefer channelLinkId. */\n messageChannelLinkId?: string\n channelId?: string | null\n tenantId?: string\n organizationId?: string | null\n providerKey?: string | null\n direction?: 'inbound' | 'outbound' | null\n}\n\ntype SubscriberContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst POSTGRES_UNIQUE_VIOLATION = '23505'\n\n// \u2500\u2500 Main handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport default async function handler(\n payload: LinkChannelMessagePayload,\n ctx: SubscriberContext,\n): Promise<void> {\n // Resolve the link ID from either field name.\n const linkId = payload?.channelLinkId ?? payload?.messageChannelLinkId\n if (typeof linkId !== 'string' || !linkId) return\n\n // Fail-closed when tenantId is missing \u2014 unscoped queries are unsafe.\n if (typeof payload.tenantId !== 'string' || !payload.tenantId) return\n\n const tenantId = payload.tenantId\n const organizationId = payload.organizationId ?? null\n // CustomerEntity and CustomerInteraction are organization-scoped. Without an\n // organization id, fail closed instead of linking tenant-wide by email/thread.\n if (!organizationId) return\n const dscope = { tenantId, organizationId }\n\n // em.fork() gives us an isolated identity map for this event.\n const em = (ctx.resolve('em') as EntityManager).fork()\n\n // \u2500\u2500 (1) Load the MessageChannelLink row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The customers module MUST NOT import MessageChannelLink from the\n // communication_channels module (cross-module ORM boundary rule in AGENTS.md).\n // We use the entity class name as a string so MikroORM's identity map resolves\n // it at runtime \u2014 the generated entity registry includes the hub's entities.\n // Read through findOneWithDecryption so any encrypted columns on the hub\n // entity are transparently decrypted (per the Encryption section in AGENTS.md).\n const link = (await findOneWithDecryption(\n em,\n 'MessageChannelLink' as any,\n { id: linkId, tenantId, organizationId } as any,\n undefined,\n dscope,\n )) as Record<string, unknown> | null\n\n if (!link) return\n\n const metaJson = (link.channelMetadata ?? null) as Record<string, unknown> | null\n const payloadJson = (link.channelPayload ?? null) as Record<string, unknown> | null\n\n // \u2500\u2500 (2) Resolve the channel to get its owner userId \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The channel.userId is needed for two purposes:\n // - authorUserId on the CustomerInteraction row\n // - default visibility ('private' for user-scoped, 'shared' for tenant-scoped)\n //\n // We look up the channel only when channelId is provided in the event payload.\n let channelUserId: string | null = null\n if (typeof payload.channelId === 'string' && payload.channelId) {\n const channel = (await findOneWithDecryption(\n em,\n 'CommunicationChannel' as any,\n { id: payload.channelId, tenantId, organizationId } as any,\n undefined,\n dscope,\n )) as { userId?: string | null } | null\n channelUserId = channel?.userId ?? null\n }\n\n // \u2500\u2500 (3) Collect recipient addresses \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The channel metadata shape differs by provider and direction:\n // - Outbound (Gmail): subject/to/cc/bcc/from are in channelMetadata\n // (merged from GmailEmailNativeMetadata by deliver-outbound-message.ts)\n // - Inbound (Gmail): from/to/cc/bcc/subject are in channelPayload\n // (from NormalizedInboundMessage.channelPayload)\n //\n // We check channelMetadata first (works for outbound + any provider that\n // writes addresses there), then fall back to channelPayload for inbound.\n const rawAddresses: unknown[] = []\n\n // For inbound IMAP messages we ALWAYS read addresses from `channelPayload`,\n // not `channelMetadata`. The IMAP adapter stores raw provider headers in\n // `channelMetadata` (where `from` is a JSON-stringified string that's\n // useless for address matching), and the structured normalized addresses\n // in `channelPayload.from` / `.to` / `.cc` / `.bcc`. Reading metadata\n // first poisoned `rawAddresses` with stringified-JSON garbage so the\n // payload-fallback never ran.\n //\n // Priority: payloadJson (canonical normalized shape) \u2192 metaJson fallback\n // (for legacy/outbound providers that still write addresses there).\n if (payloadJson) {\n collectAddressField(rawAddresses, payloadJson.from)\n collectAddressField(rawAddresses, payloadJson.to)\n collectAddressField(rawAddresses, payloadJson.cc)\n collectAddressField(rawAddresses, payloadJson.bcc)\n }\n\n if (rawAddresses.length === 0 && metaJson) {\n collectAddressField(rawAddresses, metaJson.from)\n collectAddressField(rawAddresses, metaJson.to)\n collectAddressField(rawAddresses, metaJson.cc)\n collectAddressField(rawAddresses, metaJson.bcc)\n }\n\n const normalized = normalizeAddresses(rawAddresses as string[])\n\n // \u2500\u2500 (4) Optional explicit crmPersonId hint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The outbound compose route stores `crmPersonId` in channelMetadata so the\n // subscriber can link to the intended Person even if their address isn't in\n // the recipient list (e.g. typo in To: field).\n const crmPersonIdHint =\n typeof metaJson?.crmPersonId === 'string' ? (metaJson!.crmPersonId as string) : null\n\n // Defense-in-depth: the crmPersonId hint is written by the compose route\n // (which already verifies tenant ownership), but the subscriber MUST re-verify\n // the hinted Person belongs to THIS tenant before linking. Otherwise a stale or\n // forged hint could attach an interaction to a Person in another tenant.\n let crmPersonId: string | null = null\n if (crmPersonIdHint) {\n const hintedPerson = await findOneWithDecryption(\n em,\n CustomerEntity,\n { id: crmPersonIdHint, kind: 'person', tenantId, organizationId, deletedAt: null } as any,\n undefined,\n dscope,\n )\n if (hintedPerson) crmPersonId = crmPersonIdHint\n }\n\n // Early exit: no addresses AND no hint \u2192 nothing to link.\n if (normalized.length === 0 && !crmPersonId) {\n // Before giving up, try threading-inheritance (TC-CRM-EMAIL-005).\n await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson)\n return\n }\n\n // \u2500\u2500 (5) Resolve People by address \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const matched = await findPeopleByAddresses(em, normalized, tenantId, organizationId)\n const personIdSet = new Set<string>(matched.map((m) => m.id))\n if (crmPersonId) personIdSet.add(crmPersonId)\n\n if (personIdSet.size === 0) {\n // Try threading-inheritance before giving up.\n await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson)\n return\n }\n\n // \u2500\u2500 (6) Determine visibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // Priority order:\n // 1. Explicit `crmVisibility` in channelMetadata (set by compose route)\n // 2. Channel owner: 'private' if user-scoped, 'shared' if tenant-scoped\n const linkDirection =\n link.direction === 'inbound' || link.direction === 'outbound' ? link.direction : null\n const visibility: 'private' | 'shared' = resolveVisibility(metaJson, channelUserId, linkDirection)\n\n // \u2500\u2500 (7) Extract subject / body / timestamps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const subject =\n typeof metaJson?.subject === 'string'\n ? (metaJson!.subject as string)\n : typeof payloadJson?.subject === 'string'\n ? (payloadJson!.subject as string)\n : null\n\n const bodyText =\n typeof metaJson?.bodyText === 'string'\n ? (metaJson!.bodyText as string)\n : typeof payloadJson?.text === 'string'\n ? (payloadJson!.text as string)\n : null\n\n const occurredAt = link.createdAt instanceof Date ? link.createdAt : new Date()\n const providerKey =\n typeof link.providerKey === 'string' ? (link.providerKey as string) : null\n\n // \u2500\u2500 (8) INSERT one CustomerInteraction per matched Person \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n await persistInteractions(\n em,\n personIdSet,\n {\n linkId,\n tenantId,\n organizationId,\n interactionType: 'email',\n title: subject,\n body: bodyText,\n authorUserId: channelUserId,\n occurredAt,\n visibility,\n channelProviderKey: providerKey,\n },\n )\n}\n\n// \u2500\u2500 Threading-inheritance fallback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * When direct address matching finds 0 people AND crmPersonId is absent, look\n * up parent message references from channelMetadata (`inReplyTo` + `references`).\n *\n * For each reference (an RFC2822 Message-ID), find a prior MessageChannelLink\n * in this tenant whose `channelMetadata.messageId` matches. For each such\n * parent link, find any existing email CustomerInteraction rows where\n * `externalMessageId = parent.id` \u2014 and link THIS message to the same Persons.\n *\n * This enables TC-CRM-EMAIL-005: a reply from an unknown address is still\n * attached to alice's timeline because the original thread was.\n */\nasync function handleThreadingInheritance(\n em: EntityManager,\n _link: Record<string, unknown>,\n linkId: string,\n tenantId: string,\n organizationId: string | null,\n channelUserId: string | null,\n metaJson: Record<string, unknown> | null,\n payloadJson: Record<string, unknown> | null,\n): Promise<void> {\n // \u2500\u2500 Primary: inherit Person(s) from the hub's authoritative thread \u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The hub threads an inbound reply into the same `messages.message.thread_id`\n // as the outbound that started the conversation (via the thread token /\n // subject+participants matcher). That outbound is already linked to the CRM\n // Person (through the `crmPersonId` hint set by the compose route). So a reply\n // inherits the Person of any existing email interaction in the same thread.\n //\n // This is the dependable join where the alternatives are not:\n // - Address matching (`findPeopleByAddresses`) filters the *encrypted*\n // `primary_email` column by a plaintext value, which never matches when\n // tenant data encryption is on (ciphertext != plaintext).\n // - RFC Message-IDs are rewritten by some providers (e.g. Gmail) on send,\n // so the legacy In-Reply-To/References inheritance below also misses.\n // The hub thread id survives both, so we resolve by it first.\n const inboundMessageId = typeof _link.messageId === 'string' ? _link.messageId : null\n if (inboundMessageId && organizationId) {\n const threadPersonRows = (await em.getConnection().execute(\n `SELECT DISTINCT ci.entity_id AS entity_id\n FROM messages inbound_m\n JOIN messages thread_m ON thread_m.thread_id = inbound_m.thread_id\n JOIN message_channel_links mcl ON mcl.message_id = thread_m.id\n JOIN customer_interactions ci ON ci.external_message_id = mcl.id\n WHERE inbound_m.id = ?\n AND inbound_m.tenant_id = ?\n AND inbound_m.organization_id = ?\n AND inbound_m.deleted_at IS NULL\n AND inbound_m.thread_id IS NOT NULL\n AND thread_m.tenant_id = ?\n AND thread_m.organization_id = ?\n AND thread_m.deleted_at IS NULL\n AND mcl.tenant_id = ?\n AND mcl.organization_id = ?\n AND ci.tenant_id = ?\n AND ci.organization_id = ?\n AND ci.interaction_type = 'email'\n AND ci.deleted_at IS NULL\n AND ci.entity_id IS NOT NULL\n LIMIT 200`,\n [\n inboundMessageId,\n tenantId,\n organizationId,\n tenantId,\n organizationId,\n tenantId,\n organizationId,\n tenantId,\n organizationId,\n ],\n )) as Array<{ entity_id: string }>\n const threadPersonIds = new Set<string>(\n threadPersonRows.map((row) => row.entity_id).filter((id): id is string => !!id),\n )\n if (threadPersonIds.size > 0) {\n const subject =\n typeof metaJson?.subject === 'string'\n ? (metaJson.subject as string)\n : typeof payloadJson?.subject === 'string'\n ? (payloadJson.subject as string)\n : null\n const bodyText =\n typeof payloadJson?.text === 'string'\n ? (payloadJson.text as string)\n : typeof metaJson?.bodyText === 'string'\n ? (metaJson.bodyText as string)\n : null\n const occurredAt = _link.createdAt instanceof Date ? (_link.createdAt as Date) : new Date()\n const providerKey = typeof _link.providerKey === 'string' ? (_link.providerKey as string) : null\n await persistInteractions(em, threadPersonIds, {\n linkId,\n tenantId,\n organizationId,\n interactionType: 'email',\n title: subject,\n body: bodyText,\n authorUserId: channelUserId,\n occurredAt,\n visibility: channelUserId ? 'private' : 'shared',\n channelProviderKey: providerKey,\n })\n return\n }\n }\n\n // \u2500\u2500 Fallback: legacy In-Reply-To / References Message-ID inheritance \u2500\u2500\u2500\u2500\u2500\n // Collect reference message-ids from inReplyTo + references\n const refIds: string[] = []\n const inReplyTo =\n typeof metaJson?.inReplyTo === 'string'\n ? stripBrackets(metaJson!.inReplyTo as string)\n : typeof payloadJson?.inReplyTo === 'string'\n ? stripBrackets(payloadJson!.inReplyTo as string)\n : null\n if (inReplyTo) refIds.push(inReplyTo)\n\n const refs =\n Array.isArray(metaJson?.references)\n ? (metaJson!.references as unknown[])\n : Array.isArray(payloadJson?.references)\n ? (payloadJson!.references as unknown[])\n : []\n for (const r of refs) {\n if (typeof r === 'string') {\n const stripped = stripBrackets(r)\n if (stripped && !refIds.includes(stripped)) refIds.push(stripped)\n }\n }\n\n if (refIds.length === 0) return\n\n // Find parent MessageChannelLinks whose channelMetadata.messageId is in refIds.\n // Bounded lookup: narrow to the candidate Message-IDs directly in SQL instead\n // of loading every link in the tenant and filtering in JS (which does not scale\n // on a busy mailbox \u2014 this path runs for every inbound reply that didn't match a\n // Person by address). channel_metadata is NOT an encrypted column, so a\n // parameterized raw query is safe; we match both bracketed (`<id>`) and\n // unbracketed (`id`) Message-ID storage forms, capped to a sane page.\n const dscope = { tenantId, organizationId }\n const messageIdCandidates = Array.from(\n new Set(refIds.flatMap((ref) => [ref, `<${ref}>`])),\n )\n const placeholders = messageIdCandidates.map(() => '?').join(', ')\n const parentLinkRows = (await em.getConnection().execute(\n `SELECT id FROM message_channel_links\n WHERE tenant_id = ?\n AND organization_id = ?\n AND channel_metadata->>'messageId' IN (${placeholders})\n LIMIT 200`,\n [tenantId, organizationId, ...messageIdCandidates],\n )) as Array<{ id: string }>\n\n const matchedParentIds = parentLinkRows.map((parentLink) => parentLink.id)\n\n if (matchedParentIds.length === 0) return\n\n // Find existing email CustomerInteraction rows for those parent links.\n const parentInteractions = (await findWithDecryption(\n em,\n CustomerInteraction,\n {\n externalMessageId: { $in: matchedParentIds },\n tenantId,\n organizationId,\n interactionType: 'email',\n deletedAt: null,\n } as any,\n undefined,\n dscope,\n )) as Array<{ entity: { id: string } }>\n\n const inheritedPersonIdSet = new Set<string>(\n parentInteractions.map((pi) => pi.entity?.id).filter(Boolean) as string[],\n )\n\n if (inheritedPersonIdSet.size === 0) return\n\n const visibility: 'private' | 'shared' = channelUserId ? 'private' : 'shared'\n const link = _link\n const inheritedMeta = (link.channelMetadata ?? null) as Record<string, unknown> | null\n const subject =\n typeof inheritedMeta?.subject === 'string' ? (inheritedMeta.subject as string) : null\n const occurredAt = link.createdAt instanceof Date ? (link.createdAt as Date) : new Date()\n const providerKey =\n typeof link.providerKey === 'string' ? (link.providerKey as string) : null\n\n await persistInteractions(\n em,\n inheritedPersonIdSet,\n {\n linkId,\n tenantId,\n organizationId,\n interactionType: 'email',\n title: subject,\n body: null,\n authorUserId: channelUserId,\n occurredAt,\n visibility,\n channelProviderKey: providerKey,\n },\n )\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface InteractionData {\n linkId: string\n tenantId: string\n organizationId: string | null\n interactionType: string\n title: string | null\n body: string | null\n authorUserId: string | null\n occurredAt: Date\n visibility: 'private' | 'shared'\n channelProviderKey: string | null\n}\n\nasync function persistInteractions(\n em: EntityManager,\n personIdSet: Set<string>,\n data: InteractionData,\n): Promise<void> {\n for (const personId of personIdSet) {\n // Fork per row so a unique-violation on one row doesn't poison the identity\n // map for subsequent rows. The parent em's connection pool is reused.\n const rowEm = em.fork()\n // Use em.getReference so we satisfy the ManyToOne relation without a\n // redundant SELECT \u2014 MikroORM will flush the FK column directly.\n const entityRef = rowEm.getReference(CustomerEntity, personId)\n const interaction = rowEm.create(CustomerInteraction, {\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n entity: entityRef,\n interactionType: data.interactionType,\n title: data.title,\n body: data.body,\n authorUserId: data.authorUserId,\n occurredAt: data.occurredAt,\n // Emails are logged AFTER they're sent/received \u2014 they are not\n // scheduled work. Without an explicit status the entity default\n // ('planned') combined with a past `occurredAt` makes the activity\n // timeline render the email as \"overdue\", which is the wrong UX.\n // The canonical \"completed\" value is `'done'` per\n // `validators.ts:interactionStatusValues = ['planned', 'done', 'canceled']`\n // \u2014 `'completed'` is a legacy spelling that the enricher accepts\n // defensively but the activity timeline `isOverdue` predicate does NOT.\n status: 'done',\n externalMessageId: data.linkId,\n visibility: data.visibility,\n channelProviderKey: data.channelProviderKey,\n } as any)\n try {\n await rowEm.flush()\n } catch (err) {\n // Idempotency: swallow unique-violation on (entity_id, external_message_id).\n // The partial unique index `customer_interactions_email_dedupe_uq` guarantees\n // at-most-once semantics across retries.\n const code = (err as { code?: string }).code\n if (code !== POSTGRES_UNIQUE_VIOLATION) throw err\n // Duplicate (retried delivery) \u2014 already linked, skip the refresh signal.\n continue\n }\n // Live-refresh signal for the CRM Person page (clientBroadcast \u2192 SSE). The\n // interaction is already persisted, so a failed emit must not abort linking\n // or fail this persistent (retried) subscriber.\n try {\n await emitCustomersEvent('customers.email.linked', {\n personId,\n interactionId: interaction.id,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n })\n } catch {\n /* swallow \u2014 UI refresh signal is non-critical */\n }\n }\n}\n\nfunction resolveVisibility(\n metaJson: Record<string, unknown> | null,\n channelUserId: string | null,\n direction: 'inbound' | 'outbound' | null,\n): 'private' | 'shared' {\n // The explicit `crmVisibility` override is written ONLY by the outbound compose\n // route. Inbound `channelMetadata` is provider-derived (and therefore attacker-\n // influenceable), so it MUST NOT be able to downgrade a user-owned channel's\n // inbound mail from private \u2192 shared. Honor the override on outbound only.\n if (direction === 'outbound') {\n if (metaJson?.crmVisibility === 'shared') return 'shared'\n if (metaJson?.crmVisibility === 'private') return 'private'\n }\n // Tenant-scoped channels (no userId) \u2192 shared; user-scoped \u2192 private.\n return channelUserId ? 'private' : 'shared'\n}\n\n/**\n * Collect email address strings from a field that may be:\n * - a plain string: 'alice@example.com'\n * - an array of strings: ['alice@example.com', 'bob@example.com']\n * - an object with an `address` property: { address: 'alice@example.com', name: 'Alice' }\n * - an array of such objects\n */\nfunction collectAddressField(out: unknown[], value: unknown): void {\n if (!value) return\n if (typeof value === 'string') {\n out.push(value)\n return\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === 'string') {\n out.push(item)\n } else if (item && typeof item === 'object' && typeof (item as Record<string, unknown>).address === 'string') {\n out.push((item as Record<string, unknown>).address as string)\n }\n }\n return\n }\n if (typeof value === 'object' && typeof (value as Record<string, unknown>).address === 'string') {\n out.push((value as Record<string, unknown>).address as string)\n }\n}\n\nfunction stripBrackets(value: string): string {\n const trimmed = value.trim()\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) {\n return trimmed.slice(1, -1)\n }\n return trimmed\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CustomerEntity, CustomerInteraction } from '../data/entities'\nimport { findPeopleByAddresses, normalizeAddresses } from './findPeopleByAddresses'\nimport { emitCustomersEvent } from '../events'\nimport { INTERACTION_STATUS_COMPLETED } from './interactionStatus'\n\n/**\n * Shared implementation for the link-channel-message subscribers.\n *\n * The auto-discovery scanner requires a single string `event` value on each\n * subscriber file. Because we must handle both `communication_channels.message.received`\n * AND `.sent`, we use TWO thin subscriber files (link-channel-message-received.ts\n * and link-channel-message-sent.ts) that both delegate here.\n */\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Payload emitted by `communication_channels.message.received` and\n * `communication_channels.message.sent`.\n *\n * The canonical field is `channelLinkId` (as emitted by the hub). The alias\n * `messageChannelLinkId` is kept for test stubs and legacy compatibility.\n */\ntype LinkChannelMessagePayload = {\n eventType?: string\n /** UUID of the MessageChannelLink row (canonical hub field name). */\n channelLinkId?: string\n /** Alias used in some older stubs / test payloads. Prefer channelLinkId. */\n messageChannelLinkId?: string\n channelId?: string | null\n tenantId?: string\n organizationId?: string | null\n providerKey?: string | null\n direction?: 'inbound' | 'outbound' | null\n}\n\ntype SubscriberContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst POSTGRES_UNIQUE_VIOLATION = '23505'\n\n// \u2500\u2500 Main handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport default async function handler(\n payload: LinkChannelMessagePayload,\n ctx: SubscriberContext,\n): Promise<void> {\n // Resolve the link ID from either field name.\n const linkId = payload?.channelLinkId ?? payload?.messageChannelLinkId\n if (typeof linkId !== 'string' || !linkId) return\n\n // Fail-closed when tenantId is missing \u2014 unscoped queries are unsafe.\n if (typeof payload.tenantId !== 'string' || !payload.tenantId) return\n\n const tenantId = payload.tenantId\n const organizationId = payload.organizationId ?? null\n // CustomerEntity and CustomerInteraction are organization-scoped. Without an\n // organization id, fail closed instead of linking tenant-wide by email/thread.\n if (!organizationId) return\n const dscope = { tenantId, organizationId }\n\n // em.fork() gives us an isolated identity map for this event.\n const em = (ctx.resolve('em') as EntityManager).fork()\n\n // \u2500\u2500 (1) Load the MessageChannelLink row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The customers module MUST NOT import MessageChannelLink from the\n // communication_channels module (cross-module ORM boundary rule in AGENTS.md).\n // We use the entity class name as a string so MikroORM's identity map resolves\n // it at runtime \u2014 the generated entity registry includes the hub's entities.\n // Read through findOneWithDecryption so any encrypted columns on the hub\n // entity are transparently decrypted (per the Encryption section in AGENTS.md).\n const link = (await findOneWithDecryption(\n em,\n 'MessageChannelLink' as any,\n { id: linkId, tenantId, organizationId } as any,\n undefined,\n dscope,\n )) as Record<string, unknown> | null\n\n if (!link) return\n\n const metaJson = (link.channelMetadata ?? null) as Record<string, unknown> | null\n const payloadJson = (link.channelPayload ?? null) as Record<string, unknown> | null\n\n // \u2500\u2500 (2) Resolve the channel to get its owner userId \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The channel.userId is needed for two purposes:\n // - authorUserId on the CustomerInteraction row\n // - default visibility ('private' for user-scoped, 'shared' for tenant-scoped)\n //\n // We look up the channel only when channelId is provided in the event payload.\n let channelUserId: string | null = null\n if (typeof payload.channelId === 'string' && payload.channelId) {\n const channel = (await findOneWithDecryption(\n em,\n 'CommunicationChannel' as any,\n { id: payload.channelId, tenantId, organizationId } as any,\n undefined,\n dscope,\n )) as { userId?: string | null } | null\n channelUserId = channel?.userId ?? null\n }\n\n // \u2500\u2500 (3) Collect recipient addresses \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The channel metadata shape differs by provider and direction:\n // - Outbound (Gmail): subject/to/cc/bcc/from are in channelMetadata\n // (merged from GmailEmailNativeMetadata by deliver-outbound-message.ts)\n // - Inbound (Gmail): from/to/cc/bcc/subject are in channelPayload\n // (from NormalizedInboundMessage.channelPayload)\n //\n // We check channelMetadata first (works for outbound + any provider that\n // writes addresses there), then fall back to channelPayload for inbound.\n const rawAddresses: unknown[] = []\n\n // For inbound IMAP messages we ALWAYS read addresses from `channelPayload`,\n // not `channelMetadata`. The IMAP adapter stores raw provider headers in\n // `channelMetadata` (where `from` is a JSON-stringified string that's\n // useless for address matching), and the structured normalized addresses\n // in `channelPayload.from` / `.to` / `.cc` / `.bcc`. Reading metadata\n // first poisoned `rawAddresses` with stringified-JSON garbage so the\n // payload-fallback never ran.\n //\n // Priority: payloadJson (canonical normalized shape) \u2192 metaJson fallback\n // (for legacy/outbound providers that still write addresses there).\n if (payloadJson) {\n collectAddressField(rawAddresses, payloadJson.from)\n collectAddressField(rawAddresses, payloadJson.to)\n collectAddressField(rawAddresses, payloadJson.cc)\n collectAddressField(rawAddresses, payloadJson.bcc)\n }\n\n if (rawAddresses.length === 0 && metaJson) {\n collectAddressField(rawAddresses, metaJson.from)\n collectAddressField(rawAddresses, metaJson.to)\n collectAddressField(rawAddresses, metaJson.cc)\n collectAddressField(rawAddresses, metaJson.bcc)\n }\n\n const normalized = normalizeAddresses(rawAddresses as string[])\n\n // \u2500\u2500 (4) Optional explicit crmPersonId hint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The outbound compose route stores `crmPersonId` in channelMetadata so the\n // subscriber can link to the intended Person even if their address isn't in\n // the recipient list (e.g. typo in To: field).\n const crmPersonIdHint =\n typeof metaJson?.crmPersonId === 'string' ? (metaJson!.crmPersonId as string) : null\n\n // Defense-in-depth: the crmPersonId hint is written by the compose route\n // (which already verifies tenant ownership), but the subscriber MUST re-verify\n // the hinted Person belongs to THIS tenant before linking. Otherwise a stale or\n // forged hint could attach an interaction to a Person in another tenant.\n let crmPersonId: string | null = null\n if (crmPersonIdHint) {\n const hintedPerson = await findOneWithDecryption(\n em,\n CustomerEntity,\n { id: crmPersonIdHint, kind: 'person', tenantId, organizationId, deletedAt: null } as any,\n undefined,\n dscope,\n )\n if (hintedPerson) crmPersonId = crmPersonIdHint\n }\n\n // Early exit: no addresses AND no hint \u2192 nothing to link.\n if (normalized.length === 0 && !crmPersonId) {\n // Before giving up, try threading-inheritance (TC-CRM-EMAIL-005).\n await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson)\n return\n }\n\n // \u2500\u2500 (5) Resolve People by address \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const matched = await findPeopleByAddresses(em, normalized, tenantId, organizationId)\n const personIdSet = new Set<string>(matched.map((m) => m.id))\n if (crmPersonId) personIdSet.add(crmPersonId)\n\n if (personIdSet.size === 0) {\n // Try threading-inheritance before giving up.\n await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson)\n return\n }\n\n // \u2500\u2500 (6) Determine visibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n //\n // Priority order:\n // 1. Explicit `crmVisibility` in channelMetadata (set by compose route)\n // 2. Channel owner: 'private' if user-scoped, 'shared' if tenant-scoped\n const linkDirection =\n link.direction === 'inbound' || link.direction === 'outbound' ? link.direction : null\n const visibility: 'private' | 'shared' = resolveVisibility(metaJson, channelUserId, linkDirection)\n\n // \u2500\u2500 (7) Extract subject / body / timestamps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const subject =\n typeof metaJson?.subject === 'string'\n ? (metaJson!.subject as string)\n : typeof payloadJson?.subject === 'string'\n ? (payloadJson!.subject as string)\n : null\n\n const bodyText =\n typeof metaJson?.bodyText === 'string'\n ? (metaJson!.bodyText as string)\n : typeof payloadJson?.text === 'string'\n ? (payloadJson!.text as string)\n : null\n\n const occurredAt = link.createdAt instanceof Date ? link.createdAt : new Date()\n const providerKey =\n typeof link.providerKey === 'string' ? (link.providerKey as string) : null\n\n // \u2500\u2500 (8) INSERT one CustomerInteraction per matched Person \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n await persistInteractions(\n em,\n personIdSet,\n {\n linkId,\n tenantId,\n organizationId,\n interactionType: 'email',\n title: subject,\n body: bodyText,\n authorUserId: channelUserId,\n occurredAt,\n visibility,\n channelProviderKey: providerKey,\n },\n )\n}\n\n// \u2500\u2500 Threading-inheritance fallback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * When direct address matching finds 0 people AND crmPersonId is absent, look\n * up parent message references from channelMetadata (`inReplyTo` + `references`).\n *\n * For each reference (an RFC2822 Message-ID), find a prior MessageChannelLink\n * in this tenant whose `channelMetadata.messageId` matches. For each such\n * parent link, find any existing email CustomerInteraction rows where\n * `externalMessageId = parent.id` \u2014 and link THIS message to the same Persons.\n *\n * This enables TC-CRM-EMAIL-005: a reply from an unknown address is still\n * attached to alice's timeline because the original thread was.\n */\nasync function handleThreadingInheritance(\n em: EntityManager,\n _link: Record<string, unknown>,\n linkId: string,\n tenantId: string,\n organizationId: string | null,\n channelUserId: string | null,\n metaJson: Record<string, unknown> | null,\n payloadJson: Record<string, unknown> | null,\n): Promise<void> {\n // \u2500\u2500 Primary: inherit Person(s) from the hub's authoritative thread \u2500\u2500\u2500\u2500\u2500\u2500\n //\n // The hub threads an inbound reply into the same `messages.message.thread_id`\n // as the outbound that started the conversation (via the thread token /\n // subject+participants matcher). That outbound is already linked to the CRM\n // Person (through the `crmPersonId` hint set by the compose route). So a reply\n // inherits the Person of any existing email interaction in the same thread.\n //\n // This is the dependable join where the alternatives are not:\n // - Address matching (`findPeopleByAddresses`) filters the *encrypted*\n // `primary_email` column by a plaintext value, which never matches when\n // tenant data encryption is on (ciphertext != plaintext).\n // - RFC Message-IDs are rewritten by some providers (e.g. Gmail) on send,\n // so the legacy In-Reply-To/References inheritance below also misses.\n // The hub thread id survives both, so we resolve by it first.\n const inboundMessageId = typeof _link.messageId === 'string' ? _link.messageId : null\n if (inboundMessageId && organizationId) {\n const threadPersonRows = (await em.getConnection().execute(\n `SELECT DISTINCT ci.entity_id AS entity_id\n FROM messages inbound_m\n JOIN messages thread_m ON thread_m.thread_id = inbound_m.thread_id\n JOIN message_channel_links mcl ON mcl.message_id = thread_m.id\n JOIN customer_interactions ci ON ci.external_message_id = mcl.id\n WHERE inbound_m.id = ?\n AND inbound_m.tenant_id = ?\n AND inbound_m.organization_id = ?\n AND inbound_m.deleted_at IS NULL\n AND inbound_m.thread_id IS NOT NULL\n AND thread_m.tenant_id = ?\n AND thread_m.organization_id = ?\n AND thread_m.deleted_at IS NULL\n AND mcl.tenant_id = ?\n AND mcl.organization_id = ?\n AND ci.tenant_id = ?\n AND ci.organization_id = ?\n AND ci.interaction_type = 'email'\n AND ci.deleted_at IS NULL\n AND ci.entity_id IS NOT NULL\n LIMIT 200`,\n [\n inboundMessageId,\n tenantId,\n organizationId,\n tenantId,\n organizationId,\n tenantId,\n organizationId,\n tenantId,\n organizationId,\n ],\n )) as Array<{ entity_id: string }>\n const threadPersonIds = new Set<string>(\n threadPersonRows.map((row) => row.entity_id).filter((id): id is string => !!id),\n )\n if (threadPersonIds.size > 0) {\n const subject =\n typeof metaJson?.subject === 'string'\n ? (metaJson.subject as string)\n : typeof payloadJson?.subject === 'string'\n ? (payloadJson.subject as string)\n : null\n const bodyText =\n typeof payloadJson?.text === 'string'\n ? (payloadJson.text as string)\n : typeof metaJson?.bodyText === 'string'\n ? (metaJson.bodyText as string)\n : null\n const occurredAt = _link.createdAt instanceof Date ? (_link.createdAt as Date) : new Date()\n const providerKey = typeof _link.providerKey === 'string' ? (_link.providerKey as string) : null\n await persistInteractions(em, threadPersonIds, {\n linkId,\n tenantId,\n organizationId,\n interactionType: 'email',\n title: subject,\n body: bodyText,\n authorUserId: channelUserId,\n occurredAt,\n visibility: channelUserId ? 'private' : 'shared',\n channelProviderKey: providerKey,\n })\n return\n }\n }\n\n // \u2500\u2500 Fallback: legacy In-Reply-To / References Message-ID inheritance \u2500\u2500\u2500\u2500\u2500\n // Collect reference message-ids from inReplyTo + references\n const refIds: string[] = []\n const inReplyTo =\n typeof metaJson?.inReplyTo === 'string'\n ? stripBrackets(metaJson!.inReplyTo as string)\n : typeof payloadJson?.inReplyTo === 'string'\n ? stripBrackets(payloadJson!.inReplyTo as string)\n : null\n if (inReplyTo) refIds.push(inReplyTo)\n\n const refs =\n Array.isArray(metaJson?.references)\n ? (metaJson!.references as unknown[])\n : Array.isArray(payloadJson?.references)\n ? (payloadJson!.references as unknown[])\n : []\n for (const r of refs) {\n if (typeof r === 'string') {\n const stripped = stripBrackets(r)\n if (stripped && !refIds.includes(stripped)) refIds.push(stripped)\n }\n }\n\n if (refIds.length === 0) return\n\n // Find parent MessageChannelLinks whose channelMetadata.messageId is in refIds.\n // Bounded lookup: narrow to the candidate Message-IDs directly in SQL instead\n // of loading every link in the tenant and filtering in JS (which does not scale\n // on a busy mailbox \u2014 this path runs for every inbound reply that didn't match a\n // Person by address). channel_metadata is NOT an encrypted column, so a\n // parameterized raw query is safe; we match both bracketed (`<id>`) and\n // unbracketed (`id`) Message-ID storage forms, capped to a sane page.\n const dscope = { tenantId, organizationId }\n const messageIdCandidates = Array.from(\n new Set(refIds.flatMap((ref) => [ref, `<${ref}>`])),\n )\n const placeholders = messageIdCandidates.map(() => '?').join(', ')\n const parentLinkRows = (await em.getConnection().execute(\n `SELECT id FROM message_channel_links\n WHERE tenant_id = ?\n AND organization_id = ?\n AND channel_metadata->>'messageId' IN (${placeholders})\n LIMIT 200`,\n [tenantId, organizationId, ...messageIdCandidates],\n )) as Array<{ id: string }>\n\n const matchedParentIds = parentLinkRows.map((parentLink) => parentLink.id)\n\n if (matchedParentIds.length === 0) return\n\n // Find existing email CustomerInteraction rows for those parent links.\n const parentInteractions = (await findWithDecryption(\n em,\n CustomerInteraction,\n {\n externalMessageId: { $in: matchedParentIds },\n tenantId,\n organizationId,\n interactionType: 'email',\n deletedAt: null,\n } as any,\n undefined,\n dscope,\n )) as Array<{ entity: { id: string } }>\n\n const inheritedPersonIdSet = new Set<string>(\n parentInteractions.map((pi) => pi.entity?.id).filter(Boolean) as string[],\n )\n\n if (inheritedPersonIdSet.size === 0) return\n\n const visibility: 'private' | 'shared' = channelUserId ? 'private' : 'shared'\n const link = _link\n const inheritedMeta = (link.channelMetadata ?? null) as Record<string, unknown> | null\n const subject =\n typeof inheritedMeta?.subject === 'string' ? (inheritedMeta.subject as string) : null\n const occurredAt = link.createdAt instanceof Date ? (link.createdAt as Date) : new Date()\n const providerKey =\n typeof link.providerKey === 'string' ? (link.providerKey as string) : null\n\n await persistInteractions(\n em,\n inheritedPersonIdSet,\n {\n linkId,\n tenantId,\n organizationId,\n interactionType: 'email',\n title: subject,\n body: null,\n authorUserId: channelUserId,\n occurredAt,\n visibility,\n channelProviderKey: providerKey,\n },\n )\n}\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface InteractionData {\n linkId: string\n tenantId: string\n organizationId: string | null\n interactionType: string\n title: string | null\n body: string | null\n authorUserId: string | null\n occurredAt: Date\n visibility: 'private' | 'shared'\n channelProviderKey: string | null\n}\n\nasync function persistInteractions(\n em: EntityManager,\n personIdSet: Set<string>,\n data: InteractionData,\n): Promise<void> {\n for (const personId of personIdSet) {\n // Fork per row so a unique-violation on one row doesn't poison the identity\n // map for subsequent rows. The parent em's connection pool is reused.\n const rowEm = em.fork()\n // Use em.getReference so we satisfy the ManyToOne relation without a\n // redundant SELECT \u2014 MikroORM will flush the FK column directly.\n const entityRef = rowEm.getReference(CustomerEntity, personId)\n const interaction = rowEm.create(CustomerInteraction, {\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n entity: entityRef,\n interactionType: data.interactionType,\n title: data.title,\n body: data.body,\n authorUserId: data.authorUserId,\n occurredAt: data.occurredAt,\n // Emails are logged AFTER they're sent/received \u2014 they are not\n // scheduled work. Without an explicit status the entity default\n // ('planned') combined with a past `occurredAt` makes the activity\n // timeline render the email as \"overdue\", which is the wrong UX.\n // The canonical terminal-success value is `INTERACTION_STATUS_COMPLETED`\n // ('done'); see `lib/interactionStatus.ts` for the open/terminal semantics.\n status: INTERACTION_STATUS_COMPLETED,\n externalMessageId: data.linkId,\n visibility: data.visibility,\n channelProviderKey: data.channelProviderKey,\n } as any)\n try {\n await rowEm.flush()\n } catch (err) {\n // Idempotency: swallow unique-violation on (entity_id, external_message_id).\n // The partial unique index `customer_interactions_email_dedupe_uq` guarantees\n // at-most-once semantics across retries.\n const code = (err as { code?: string }).code\n if (code !== POSTGRES_UNIQUE_VIOLATION) throw err\n // Duplicate (retried delivery) \u2014 already linked, skip the refresh signal.\n continue\n }\n // Live-refresh signal for the CRM Person page (clientBroadcast \u2192 SSE). The\n // interaction is already persisted, so a failed emit must not abort linking\n // or fail this persistent (retried) subscriber.\n try {\n await emitCustomersEvent('customers.email.linked', {\n personId,\n interactionId: interaction.id,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n })\n } catch {\n /* swallow \u2014 UI refresh signal is non-critical */\n }\n }\n}\n\nfunction resolveVisibility(\n metaJson: Record<string, unknown> | null,\n channelUserId: string | null,\n direction: 'inbound' | 'outbound' | null,\n): 'private' | 'shared' {\n // The explicit `crmVisibility` override is written ONLY by the outbound compose\n // route. Inbound `channelMetadata` is provider-derived (and therefore attacker-\n // influenceable), so it MUST NOT be able to downgrade a user-owned channel's\n // inbound mail from private \u2192 shared. Honor the override on outbound only.\n if (direction === 'outbound') {\n if (metaJson?.crmVisibility === 'shared') return 'shared'\n if (metaJson?.crmVisibility === 'private') return 'private'\n }\n // Tenant-scoped channels (no userId) \u2192 shared; user-scoped \u2192 private.\n return channelUserId ? 'private' : 'shared'\n}\n\n/**\n * Collect email address strings from a field that may be:\n * - a plain string: 'alice@example.com'\n * - an array of strings: ['alice@example.com', 'bob@example.com']\n * - an object with an `address` property: { address: 'alice@example.com', name: 'Alice' }\n * - an array of such objects\n */\nfunction collectAddressField(out: unknown[], value: unknown): void {\n if (!value) return\n if (typeof value === 'string') {\n out.push(value)\n return\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === 'string') {\n out.push(item)\n } else if (item && typeof item === 'object' && typeof (item as Record<string, unknown>).address === 'string') {\n out.push((item as Record<string, unknown>).address as string)\n }\n }\n return\n }\n if (typeof value === 'object' && typeof (value as Record<string, unknown>).address === 'string') {\n out.push((value as Record<string, unknown>).address as string)\n }\n}\n\nfunction stripBrackets(value: string): string {\n const trimmed = value.trim()\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) {\n return trimmed.slice(1, -1)\n }\n return trimmed\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["subject", "occurredAt", "providerKey"]
7
7
  }
@@ -16,11 +16,14 @@ function AppearanceSelector({
16
16
  disabled = false,
17
17
  iconSuggestions = ICON_SUGGESTIONS,
18
18
  iconLibrary,
19
- className
19
+ className,
20
+ previewLabel
20
21
  }) {
21
22
  const normalizedIcon = icon ?? "";
22
23
  const normalizedColor = color ?? "#000000";
23
24
  const hasAppearance = Boolean(icon) || Boolean(color);
25
+ const trimmedPreviewLabel = typeof previewLabel === "string" ? previewLabel.trim() : "";
26
+ const hasPreviewContent = hasAppearance || trimmedPreviewLabel.length > 0;
24
27
  const iconOptions = React.useMemo(() => iconLibrary && iconLibrary.length ? iconLibrary : ICON_LIBRARY, [iconLibrary]);
25
28
  const [pickerOpen, setPickerOpen] = React.useState(false);
26
29
  const [iconSearch, setIconSearch] = React.useState("");
@@ -194,9 +197,10 @@ function AppearanceSelector({
194
197
  ] }),
195
198
  /* @__PURE__ */ jsxs("div", { children: [
196
199
  /* @__PURE__ */ jsx("label", { className: "text-sm font-medium", children: "Preview" }),
197
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3 rounded border border-dashed border-border px-3 py-2", children: hasAppearance ? /* @__PURE__ */ jsxs(Fragment, { children: [
200
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3 rounded border border-dashed border-border px-3 py-2", children: hasPreviewContent ? /* @__PURE__ */ jsxs(Fragment, { children: [
198
201
  renderDictionaryIcon(icon, "h-5 w-5"),
199
- renderDictionaryColor(color, "h-4 w-4 rounded-full")
202
+ renderDictionaryColor(color, "h-4 w-4 rounded-full"),
203
+ trimmedPreviewLabel ? /* @__PURE__ */ jsx("span", { className: "text-sm", children: trimmedPreviewLabel }) : null
200
204
  ] }) : /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: labels.previewEmptyLabel }) })
201
205
  ] })
202
206
  ] });