@open-mercato/core 0.5.1-develop.2912.8d7b1fef24 → 0.5.1-develop.2924.d13908516e

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 (59) hide show
  1. package/dist/modules/customers/api/companies/[id]/people/route.js +12 -7
  2. package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
  3. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +2 -1
  4. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  5. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -2
  6. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  7. package/dist/modules/customers/commands/companies.js +93 -19
  8. package/dist/modules/customers/commands/companies.js.map +2 -2
  9. package/dist/modules/customers/commands/people.js +9 -1
  10. package/dist/modules/customers/commands/people.js.map +2 -2
  11. package/dist/modules/customers/commands/personCompanyLinks.js +2 -2
  12. package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
  13. package/dist/modules/customers/components/detail/CompanyCard.js +32 -3
  14. package/dist/modules/customers/components/detail/CompanyCard.js.map +2 -2
  15. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +37 -19
  16. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  17. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +7 -4
  18. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  19. package/dist/modules/customers/components/detail/PersonCompaniesSection.js +63 -2
  20. package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +2 -2
  21. package/dist/modules/customers/components/detail/PersonDetailTabs.js +37 -19
  22. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  23. package/dist/modules/customers/components/detail/TasksSection.js +1 -11
  24. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  25. package/dist/modules/customers/components/formConfig.js +50 -39
  26. package/dist/modules/customers/components/formConfig.js.map +2 -2
  27. package/dist/modules/customers/events.js +3 -3
  28. package/dist/modules/customers/events.js.map +2 -2
  29. package/dist/modules/customers/lib/displayName.js +13 -1
  30. package/dist/modules/customers/lib/displayName.js.map +2 -2
  31. package/dist/modules/customers/lib/personCompanies.js +12 -7
  32. package/dist/modules/customers/lib/personCompanies.js.map +2 -2
  33. package/dist/modules/customers/lib/personCompanyLinkTable.js +5 -0
  34. package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
  35. package/dist/modules/workflows/lib/activity-executor.js +21 -17
  36. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  37. package/package.json +3 -3
  38. package/src/modules/customers/api/companies/[id]/people/route.ts +12 -7
  39. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +2 -1
  40. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +12 -2
  41. package/src/modules/customers/commands/companies.ts +107 -19
  42. package/src/modules/customers/commands/people.ts +16 -1
  43. package/src/modules/customers/commands/personCompanyLinks.ts +3 -2
  44. package/src/modules/customers/components/detail/CompanyCard.tsx +28 -4
  45. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +18 -2
  46. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +8 -4
  47. package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +66 -0
  48. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +18 -2
  49. package/src/modules/customers/components/detail/TasksSection.tsx +1 -8
  50. package/src/modules/customers/components/formConfig.tsx +59 -40
  51. package/src/modules/customers/events.ts +3 -3
  52. package/src/modules/customers/i18n/de.json +10 -0
  53. package/src/modules/customers/i18n/en.json +10 -0
  54. package/src/modules/customers/i18n/es.json +10 -0
  55. package/src/modules/customers/i18n/pl.json +10 -0
  56. package/src/modules/customers/lib/displayName.ts +19 -0
  57. package/src/modules/customers/lib/personCompanies.ts +12 -7
  58. package/src/modules/customers/lib/personCompanyLinkTable.ts +14 -0
  59. package/src/modules/workflows/lib/activity-executor.ts +21 -18
@@ -7,6 +7,7 @@ import { Check, Pencil, Plus, Settings } from 'lucide-react'
7
7
  import { useT } from '@open-mercato/shared/lib/i18n/context'
8
8
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
9
9
  import { Button } from '@open-mercato/ui/primitives/button'
10
+ import { deriveDisplayName, isDerivedDisplayName } from '../lib/displayName'
10
11
  import {
11
12
  Dialog,
12
13
  DialogContent,
@@ -703,16 +704,18 @@ export const createDisplayNameSection = (t: Translator) =>
703
704
  function DisplayNameSection({ values, setValue, errors }: CrudFormGroupComponentProps) {
704
705
  const [editing, setEditing] = React.useState(false)
705
706
  const [manualOverride, setManualOverride] = React.useState(() => {
706
- const current = typeof values.displayName === 'string' ? values.displayName.trim() : ''
707
- return current.length > 0
707
+ const current = typeof values.displayName === 'string' ? values.displayName : ''
708
+ const firstInit = typeof values.firstName === 'string' ? values.firstName : ''
709
+ const lastInit = typeof values.lastName === 'string' ? values.lastName : ''
710
+ // Sticky-manual: treat as user-customized only when the persisted display name
711
+ // doesn't match the first+last derivation (matches the server-side rule in
712
+ // updatePersonCommand). Empty values are considered derived.
713
+ return !isDerivedDisplayName(current, firstInit, lastInit)
708
714
  })
709
715
 
710
716
  const first = typeof values.firstName === 'string' ? values.firstName.trim() : ''
711
717
  const last = typeof values.lastName === 'string' ? values.lastName.trim() : ''
712
- const derived = React.useMemo(() => {
713
- const parts = [first, last].filter((part) => !!part)
714
- return parts.join(' ').trim()
715
- }, [first, last])
718
+ const derived = React.useMemo(() => deriveDisplayName(first, last), [first, last])
716
719
 
717
720
  React.useEffect(() => {
718
721
  if (!manualOverride) {
@@ -1684,40 +1687,56 @@ export const createPersonEditGroups = (t: Translator): CrudFormGroup[] => [
1684
1687
  * Groups for the Person v2 "Dane osobowe" Figma layout (SPEC-048 mockup).
1685
1688
  * All groups in column 1 (Zone 1). Notes handled separately in Zone 2 tabs.
1686
1689
  */
1687
- export const createPersonPersonalDataGroups = (t: Translator): CrudFormGroup[] => [
1688
- {
1689
- id: 'personalData',
1690
- title: t('customers.people.form.groups.personalData', 'Personal data'),
1691
- column: 1,
1692
- fields: ['firstName', 'lastName', 'jobTitle', 'primaryEmail', 'primaryPhone'],
1693
- },
1694
- {
1695
- id: 'companyRole',
1696
- title: t('customers.people.form.groups.companyRole', 'Company & role'),
1697
- column: 1,
1698
- fields: ['companyEntityId', 'status', 'lifecycleStage', 'source'],
1699
- },
1700
- {
1701
- id: 'customFields',
1702
- title: t('customers.people.form.groups.customAttributes', 'Custom attributes'),
1703
- column: 1,
1704
- kind: 'customFields',
1705
- },
1706
- {
1707
- id: 'roles',
1708
- title: t('customers.people.form.groups.roles', 'My roles'),
1709
- column: 1,
1710
- component: ({ values }: CrudFormGroupComponentProps) => (
1711
- values.id ? (
1712
- <RolesSection
1713
- entityType="person"
1714
- entityId={values.id as string}
1715
- entityName={typeof values.displayName === 'string' ? values.displayName : null}
1716
- />
1717
- ) : null
1718
- ),
1719
- },
1720
- ]
1690
+ export const createPersonPersonalDataGroups = (
1691
+ t: Translator,
1692
+ options?: { entityName?: string | null },
1693
+ ): CrudFormGroup[] => {
1694
+ const entityName = options?.entityName?.trim() || null
1695
+ const rolesTitle = entityName
1696
+ ? t('customers.roles.groupTitle.person', 'My roles with {{name}}', { name: entityName })
1697
+ : t('customers.people.form.groups.roles', 'My roles')
1698
+ return [
1699
+ {
1700
+ id: 'personalDataDisplay',
1701
+ title: t('customers.people.form.groups.displayName', 'Display name'),
1702
+ column: 1,
1703
+ bare: true,
1704
+ component: createDisplayNameSection(t),
1705
+ },
1706
+ {
1707
+ id: 'personalData',
1708
+ title: t('customers.people.form.groups.personalData', 'Personal data'),
1709
+ column: 1,
1710
+ fields: ['firstName', 'lastName', 'jobTitle', 'primaryEmail', 'primaryPhone'],
1711
+ },
1712
+ {
1713
+ id: 'companyRole',
1714
+ title: t('customers.people.form.groups.companyRole', 'Company & role'),
1715
+ column: 1,
1716
+ fields: ['companyEntityId', 'status', 'lifecycleStage', 'source'],
1717
+ },
1718
+ {
1719
+ id: 'customFields',
1720
+ title: t('customers.people.form.groups.customAttributes', 'Custom attributes'),
1721
+ column: 1,
1722
+ kind: 'customFields',
1723
+ },
1724
+ {
1725
+ id: 'roles',
1726
+ title: rolesTitle,
1727
+ column: 1,
1728
+ component: ({ values }: CrudFormGroupComponentProps) => (
1729
+ values.id ? (
1730
+ <RolesSection
1731
+ entityType="person"
1732
+ entityId={values.id as string}
1733
+ entityName={typeof values.displayName === 'string' ? values.displayName : null}
1734
+ />
1735
+ ) : null
1736
+ ),
1737
+ },
1738
+ ]
1739
+ }
1721
1740
 
1722
1741
  // ---------------------------------------------------------------------------
1723
1742
  // Edit-mode payload builders
@@ -75,9 +75,9 @@ const events = [
75
75
  { id: 'customers.label_assignment.deleted', label: 'Label Unassigned', entity: 'label_assignment', category: 'crud' },
76
76
 
77
77
  // Person-Company Links
78
- { id: 'customers.person_company_link.created', label: 'Person Linked To Company', entity: 'person_company_link', category: 'crud' },
79
- { id: 'customers.person_company_link.updated', label: 'Person-Company Link Updated', entity: 'person_company_link', category: 'crud' },
80
- { id: 'customers.person_company_link.deleted', label: 'Person Unlinked From Company', entity: 'person_company_link', category: 'crud' },
78
+ { id: 'customers.person_company_link.created', label: 'Person Linked To Company', entity: 'person_company_link', category: 'crud', clientBroadcast: true },
79
+ { id: 'customers.person_company_link.updated', label: 'Person-Company Link Updated', entity: 'person_company_link', category: 'crud', clientBroadcast: true },
80
+ { id: 'customers.person_company_link.deleted', label: 'Person Unlinked From Company', entity: 'person_company_link', category: 'crud', clientBroadcast: true },
81
81
  ] as const
82
82
 
83
83
  export const eventsConfig = createModuleEvents({
@@ -195,6 +195,10 @@
195
195
  "customers.companies.dashboard.seeAllActivity": "Alle {{count}} Aktivitäten anzeigen",
196
196
  "customers.companies.dashboard.showAll": "Show all",
197
197
  "customers.companies.dashboard.upcomingMeetings": "Anstehende Meetings",
198
+ "customers.companies.delete.blocked": "Unternehmen kann nicht gelöscht werden: {{blockers}}. Bitte zuerst Verknüpfungen lösen oder neu zuweisen.",
199
+ "customers.companies.delete.blockers.deals": "verknüpfte Deals ({{count}})",
200
+ "customers.companies.delete.blockers.directPeople": "Personen, deren primäres Unternehmen dies ist ({{count}})",
201
+ "customers.companies.delete.blockers.persons": "verknüpfte Personen ({{count}})",
198
202
  "customers.companies.detail.actions.addDeal": "Deal hinzufügen",
199
203
  "customers.companies.detail.actions.backToList": "Zurück zu Unternehmen",
200
204
  "customers.companies.detail.actions.cancel": "Abbrechen",
@@ -1189,6 +1193,11 @@
1189
1193
  "customers.people.detail.companies.sortRecent": "Sort: Recently active",
1190
1194
  "customers.people.detail.companies.subtitle": "Link one or more companies and choose the primary relationship for this person.",
1191
1195
  "customers.people.detail.companies.summary": "{{count}} verknüpfte Firmen",
1196
+ "customers.people.detail.companies.unlinkAction": "Verknüpfung lösen",
1197
+ "customers.people.detail.companies.unlinkConfirm": "Verknüpfung von {{company}} mit {{person}} lösen?",
1198
+ "customers.people.detail.companies.unlinkConfirmTitle": "Firma entkoppeln",
1199
+ "customers.people.detail.companies.unlinkError": "Verknüpfung der Firma konnte nicht gelöst werden.",
1200
+ "customers.people.detail.companies.unlinkSuccess": "Firma entkoppelt.",
1192
1201
  "customers.people.detail.company.current": "{{company}}",
1193
1202
  "customers.people.detail.company.empty": "Kein Unternehmen zugewiesen",
1194
1203
  "customers.people.detail.company.label": "Unternehmen",
@@ -1530,6 +1539,7 @@
1530
1539
  "customers.people.form.groups.custom": "Individuelle Felder",
1531
1540
  "customers.people.form.groups.customAttributes": "Custom attributes",
1532
1541
  "customers.people.form.groups.details": "Details",
1542
+ "customers.people.form.groups.displayName": "Anzeigename",
1533
1543
  "customers.people.form.groups.notes": "Notizen",
1534
1544
  "customers.people.form.groups.personalData": "Personal data",
1535
1545
  "customers.people.form.groups.roles": "My roles",
@@ -195,6 +195,10 @@
195
195
  "customers.companies.dashboard.seeAllActivity": "See all {{count}} activities",
196
196
  "customers.companies.dashboard.showAll": "Show all",
197
197
  "customers.companies.dashboard.upcomingMeetings": "Upcoming meetings",
198
+ "customers.companies.delete.blocked": "Cannot delete company: {{blockers}}. Please unlink or reassign first.",
199
+ "customers.companies.delete.blockers.deals": "linked deals ({{count}})",
200
+ "customers.companies.delete.blockers.directPeople": "persons whose primary company is this one ({{count}})",
201
+ "customers.companies.delete.blockers.persons": "linked persons ({{count}})",
198
202
  "customers.companies.detail.actions.addDeal": "Add deal",
199
203
  "customers.companies.detail.actions.backToList": "Back to companies",
200
204
  "customers.companies.detail.actions.cancel": "Cancel",
@@ -1189,6 +1193,11 @@
1189
1193
  "customers.people.detail.companies.sortRecent": "Sort: Recently active",
1190
1194
  "customers.people.detail.companies.subtitle": "Link one or more companies and choose the primary relationship for this person.",
1191
1195
  "customers.people.detail.companies.summary": "{{count}} linked companies",
1196
+ "customers.people.detail.companies.unlinkAction": "Unlink",
1197
+ "customers.people.detail.companies.unlinkConfirm": "Unlink {{company}} from {{person}}?",
1198
+ "customers.people.detail.companies.unlinkConfirmTitle": "Unlink company",
1199
+ "customers.people.detail.companies.unlinkError": "Failed to unlink company.",
1200
+ "customers.people.detail.companies.unlinkSuccess": "Company unlinked.",
1192
1201
  "customers.people.detail.company.current": "{{company}}",
1193
1202
  "customers.people.detail.company.empty": "No company assigned",
1194
1203
  "customers.people.detail.company.label": "Company",
@@ -1530,6 +1539,7 @@
1530
1539
  "customers.people.form.groups.custom": "Custom fields",
1531
1540
  "customers.people.form.groups.customAttributes": "Custom attributes",
1532
1541
  "customers.people.form.groups.details": "Details",
1542
+ "customers.people.form.groups.displayName": "Display name",
1533
1543
  "customers.people.form.groups.notes": "Notes",
1534
1544
  "customers.people.form.groups.personalData": "Personal data",
1535
1545
  "customers.people.form.groups.roles": "My roles",
@@ -195,6 +195,10 @@
195
195
  "customers.companies.dashboard.seeAllActivity": "Ver todas las {{count}} actividades",
196
196
  "customers.companies.dashboard.showAll": "Show all",
197
197
  "customers.companies.dashboard.upcomingMeetings": "Próximas reuniones",
198
+ "customers.companies.delete.blocked": "No se puede eliminar la empresa: {{blockers}}. Por favor, desvincula o reasigna primero.",
199
+ "customers.companies.delete.blockers.deals": "oportunidades vinculadas ({{count}})",
200
+ "customers.companies.delete.blockers.directPeople": "personas cuya empresa principal es esta ({{count}})",
201
+ "customers.companies.delete.blockers.persons": "personas vinculadas ({{count}})",
198
202
  "customers.companies.detail.actions.addDeal": "Agregar oportunidad",
199
203
  "customers.companies.detail.actions.backToList": "Volver a empresas",
200
204
  "customers.companies.detail.actions.cancel": "Cancelar",
@@ -1189,6 +1193,11 @@
1189
1193
  "customers.people.detail.companies.sortRecent": "Sort: Recently active",
1190
1194
  "customers.people.detail.companies.subtitle": "Link one or more companies and choose the primary relationship for this person.",
1191
1195
  "customers.people.detail.companies.summary": "{{count}} empresas vinculadas",
1196
+ "customers.people.detail.companies.unlinkAction": "Desvincular",
1197
+ "customers.people.detail.companies.unlinkConfirm": "¿Desvincular {{company}} de {{person}}?",
1198
+ "customers.people.detail.companies.unlinkConfirmTitle": "Desvincular empresa",
1199
+ "customers.people.detail.companies.unlinkError": "No se pudo desvincular la empresa.",
1200
+ "customers.people.detail.companies.unlinkSuccess": "Empresa desvinculada.",
1192
1201
  "customers.people.detail.company.current": "{{company}}",
1193
1202
  "customers.people.detail.company.empty": "Ninguna empresa asignada",
1194
1203
  "customers.people.detail.company.label": "Empresa",
@@ -1530,6 +1539,7 @@
1530
1539
  "customers.people.form.groups.custom": "Campos personalizados",
1531
1540
  "customers.people.form.groups.customAttributes": "Custom attributes",
1532
1541
  "customers.people.form.groups.details": "Detalles",
1542
+ "customers.people.form.groups.displayName": "Nombre para mostrar",
1533
1543
  "customers.people.form.groups.notes": "Notas",
1534
1544
  "customers.people.form.groups.personalData": "Personal data",
1535
1545
  "customers.people.form.groups.roles": "My roles",
@@ -195,6 +195,10 @@
195
195
  "customers.companies.dashboard.seeAllActivity": "Zobacz wszystkie {{count}} aktywności",
196
196
  "customers.companies.dashboard.showAll": "Show all",
197
197
  "customers.companies.dashboard.upcomingMeetings": "Nadchodzące spotkania",
198
+ "customers.companies.delete.blocked": "Nie można usunąć firmy: {{blockers}}. Najpierw odłącz lub przepisz powiązania.",
199
+ "customers.companies.delete.blockers.deals": "powiązane szanse ({{count}})",
200
+ "customers.companies.delete.blockers.directPeople": "osoby, dla których ta firma jest podstawowa ({{count}})",
201
+ "customers.companies.delete.blockers.persons": "powiązane osoby ({{count}})",
198
202
  "customers.companies.detail.actions.addDeal": "Dodaj szansę",
199
203
  "customers.companies.detail.actions.backToList": "Powrót do listy firm",
200
204
  "customers.companies.detail.actions.cancel": "Anuluj",
@@ -1189,6 +1193,11 @@
1189
1193
  "customers.people.detail.companies.sortRecent": "Sort: Recently active",
1190
1194
  "customers.people.detail.companies.subtitle": "Link one or more companies and choose the primary relationship for this person.",
1191
1195
  "customers.people.detail.companies.summary": "{{count}} powiązanych firm",
1196
+ "customers.people.detail.companies.unlinkAction": "Odłącz",
1197
+ "customers.people.detail.companies.unlinkConfirm": "Odłączyć firmę {{company}} od {{person}}?",
1198
+ "customers.people.detail.companies.unlinkConfirmTitle": "Odłącz firmę",
1199
+ "customers.people.detail.companies.unlinkError": "Nie udało się odłączyć firmy.",
1200
+ "customers.people.detail.companies.unlinkSuccess": "Firma odłączona.",
1192
1201
  "customers.people.detail.company.current": "{{company}}",
1193
1202
  "customers.people.detail.company.empty": "Brak przypisanej firmy",
1194
1203
  "customers.people.detail.company.label": "Firma",
@@ -1530,6 +1539,7 @@
1530
1539
  "customers.people.form.groups.custom": "Pola niestandardowe",
1531
1540
  "customers.people.form.groups.customAttributes": "Atrybuty niestandardowe",
1532
1541
  "customers.people.form.groups.details": "Szczegóły",
1542
+ "customers.people.form.groups.displayName": "Nazwa wyświetlana",
1533
1543
  "customers.people.form.groups.notes": "Notatki",
1534
1544
  "customers.people.form.groups.personalData": "Dane osobowe",
1535
1545
  "customers.people.form.groups.roles": "Moje role",
@@ -1,3 +1,22 @@
1
+ export function deriveDisplayName(
2
+ firstName: string | null | undefined,
3
+ lastName: string | null | undefined,
4
+ ): string {
5
+ const first = (firstName ?? '').trim()
6
+ const last = (lastName ?? '').trim()
7
+ return [first, last].filter((part) => part.length > 0).join(' ').trim()
8
+ }
9
+
10
+ export function isDerivedDisplayName(
11
+ current: string | null | undefined,
12
+ firstName: string | null | undefined,
13
+ lastName: string | null | undefined,
14
+ ): boolean {
15
+ const trimmed = (current ?? '').trim()
16
+ if (trimmed.length === 0) return true
17
+ return trimmed === deriveDisplayName(firstName, lastName)
18
+ }
19
+
1
20
  export function deriveDisplayNameFromEmail(email: string | null | undefined): string | null {
2
21
  if (typeof email !== 'string') return null
3
22
  const trimmed = email.trim()
@@ -6,7 +6,10 @@ import {
6
6
  CustomerPersonCompanyLink,
7
7
  CustomerPersonProfile,
8
8
  } from '../data/entities'
9
- import { withActiveCustomerPersonCompanyLinkFilter } from './personCompanyLinkTable'
9
+ import {
10
+ filterActivePersonCompanyLinks,
11
+ withActiveCustomerPersonCompanyLinkFilter,
12
+ } from './personCompanyLinkTable'
10
13
 
11
14
  export type PersonCompanySummary = {
12
15
  linkId: string | null
@@ -62,12 +65,14 @@ export async function loadPersonCompanyLinks(
62
65
  { person, organizationId: person.organizationId, tenantId: person.tenantId },
63
66
  'customers.personCompanies.loadPersonCompanyLinks',
64
67
  )
65
- return findWithDecryption(
66
- em,
67
- CustomerPersonCompanyLink,
68
- where,
69
- { populate: ['company'], orderBy: { isPrimary: 'desc', createdAt: 'asc' } },
70
- { tenantId: person.tenantId, organizationId: person.organizationId },
68
+ return filterActivePersonCompanyLinks(
69
+ await findWithDecryption(
70
+ em,
71
+ CustomerPersonCompanyLink,
72
+ where,
73
+ { populate: ['company'], orderBy: { isPrimary: 'desc', createdAt: 'asc' } },
74
+ { tenantId: person.tenantId, organizationId: person.organizationId },
75
+ ),
71
76
  )
72
77
  }
73
78
 
@@ -51,3 +51,17 @@ export async function withActiveCustomerPersonCompanyLinkFilter<T extends Record
51
51
  }
52
52
  return { ...where, deletedAt: null }
53
53
  }
54
+
55
+ /**
56
+ * Drop soft-deleted link rows from a result set as a defense-in-depth fallback.
57
+ * MikroORM has historically dropped `deletedAt: null` from the WHERE clause for
58
+ * nullable date columns under certain configurations, so callers SHOULD apply this
59
+ * after `findWithDecryption(...)` until the upstream query filter is verified to
60
+ * fully cover all callers.
61
+ */
62
+ export function filterActivePersonCompanyLinks<T extends { deletedAt?: Date | string | null | undefined }>(
63
+ links: T[] | null | undefined,
64
+ ): T[] {
65
+ if (!Array.isArray(links)) return []
66
+ return links.filter((entry) => entry?.deletedAt == null)
67
+ }
@@ -16,7 +16,7 @@ import { WorkflowInstance } from '../data/entities'
16
16
  import { createModuleQueue, Queue } from '@open-mercato/queue'
17
17
  import { getRedisUrl } from '@open-mercato/shared/lib/redis/connection'
18
18
  import {
19
- assertSafeOutboundUrl,
19
+ safeOutboundFetch,
20
20
  UnsafeOutboundUrlError,
21
21
  type HostLookup,
22
22
  } from '@open-mercato/shared/lib/url-safety'
@@ -646,12 +646,27 @@ export async function executeCallWebhook(
646
646
 
647
647
  const allowPrivate = deps.allowPrivate ?? isAllowPrivateWorkflowWebhookUrlsEnabled()
648
648
 
649
+ let response: Response
649
650
  try {
650
- await assertSafeOutboundUrl(url, {
651
- subject: 'Workflow webhook URL',
652
- allowPrivate,
653
- lookupHost: deps.lookupHost,
654
- })
651
+ response = await safeOutboundFetch(
652
+ url,
653
+ {
654
+ method,
655
+ headers: {
656
+ 'Content-Type': 'application/json',
657
+ ...headers,
658
+ },
659
+ body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
660
+ redirect: 'manual',
661
+ signal: deps.signal,
662
+ },
663
+ {
664
+ subject: 'Workflow webhook URL',
665
+ allowPrivate,
666
+ lookupHost: deps.lookupHost,
667
+ fetchImpl: deps.fetchImpl,
668
+ },
669
+ )
655
670
  } catch (error) {
656
671
  if (error instanceof UnsafeOutboundUrlError) {
657
672
  throw new Error(
@@ -661,18 +676,6 @@ export async function executeCallWebhook(
661
676
  throw error
662
677
  }
663
678
 
664
- const fetchImpl = deps.fetchImpl ?? fetch
665
- const response = await fetchImpl(url, {
666
- method,
667
- headers: {
668
- 'Content-Type': 'application/json',
669
- ...headers,
670
- },
671
- body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
672
- redirect: 'manual',
673
- signal: deps.signal,
674
- })
675
-
676
679
  if (response.status >= 300 && response.status < 400) {
677
680
  const location = response.headers.get('location')
678
681
  throw new Error(