@open-mercato/core 0.7.1-develop.7113.1.9d83dca10c → 0.7.1-develop.7121.1.734c263bda

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 (95) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/crmFixtures.js.map +1 -1
  3. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +9 -2
  4. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
  5. package/dist/modules/communication_channels/lib/outbound-recipient.js +5 -1
  6. package/dist/modules/communication_channels/lib/outbound-recipient.js.map +2 -2
  7. package/dist/modules/communication_channels/lib/test-seed.js +5 -0
  8. package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
  9. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +1 -1
  10. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +1 -1
  11. package/dist/modules/customers/backend/customers/deals/page.js +4 -3
  12. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  13. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +2 -2
  14. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
  15. package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +1 -1
  16. package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +1 -1
  17. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
  18. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  19. package/dist/modules/customers/cli.js +5 -5
  20. package/dist/modules/customers/cli.js.map +2 -2
  21. package/dist/modules/customers/commands/deals.js.map +2 -2
  22. package/dist/modules/customers/components/detail/pipelineStageUtils.js +1 -0
  23. package/dist/modules/customers/components/detail/pipelineStageUtils.js.map +2 -2
  24. package/dist/modules/customers/components/linking/adapters/dealAdapter.js +2 -2
  25. package/dist/modules/customers/components/linking/adapters/dealAdapter.js.map +2 -2
  26. package/dist/modules/customers/lib/closureStage.js.map +2 -2
  27. package/dist/modules/customers/lib/dealStatus.js +6 -4
  28. package/dist/modules/customers/lib/dealStatus.js.map +2 -2
  29. package/dist/modules/customers/lib/dealsSummaryQueries.js +3 -3
  30. package/dist/modules/customers/lib/dealsSummaryQueries.js.map +1 -1
  31. package/dist/modules/customers/migrations/Migration20260824180000_deal_status_lost.js +30 -0
  32. package/dist/modules/customers/migrations/Migration20260824180000_deal_status_lost.js.map +7 -0
  33. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +1 -1
  34. package/dist/modules/eudr/notifications.js +5 -0
  35. package/dist/modules/eudr/notifications.js.map +2 -2
  36. package/dist/modules/query_index/lib/search-tokens.js +59 -28
  37. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  38. package/dist/modules/sales/commands/documents.js +146 -99
  39. package/dist/modules/sales/commands/documents.js.map +2 -2
  40. package/dist/modules/sales/commands/returns.js +1 -30
  41. package/dist/modules/sales/commands/returns.js.map +2 -2
  42. package/dist/modules/sales/commands/shared.js +1 -4
  43. package/dist/modules/sales/commands/shared.js.map +2 -2
  44. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +2 -1
  45. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
  46. package/dist/modules/sales/data/validators.js +3 -0
  47. package/dist/modules/sales/data/validators.js.map +2 -2
  48. package/dist/modules/sales/lib/calculations.js +14 -2
  49. package/dist/modules/sales/lib/calculations.js.map +2 -2
  50. package/dist/modules/sales/lib/json.js +8 -0
  51. package/dist/modules/sales/lib/json.js.map +7 -0
  52. package/dist/modules/sales/lib/lineSnapshots.js +66 -0
  53. package/dist/modules/sales/lib/lineSnapshots.js.map +7 -0
  54. package/dist/modules/warranty_claims/notifications.js +5 -0
  55. package/dist/modules/warranty_claims/notifications.js.map +2 -2
  56. package/dist/modules/wms/notifications.js +2 -0
  57. package/dist/modules/wms/notifications.js.map +2 -2
  58. package/package.json +7 -7
  59. package/src/helpers/integration/crmFixtures.ts +1 -1
  60. package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +21 -9
  61. package/src/modules/communication_channels/lib/adapter.ts +11 -0
  62. package/src/modules/communication_channels/lib/outbound-recipient.ts +14 -1
  63. package/src/modules/communication_channels/lib/test-seed.ts +19 -0
  64. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +1 -1
  65. package/src/modules/customers/backend/customers/deals/page.tsx +4 -3
  66. package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +5 -5
  67. package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +3 -3
  68. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2 -2
  69. package/src/modules/customers/cli.ts +5 -5
  70. package/src/modules/customers/commands/deals.ts +4 -3
  71. package/src/modules/customers/components/detail/pipelineStageUtils.ts +1 -0
  72. package/src/modules/customers/components/linking/adapters/dealAdapter.tsx +2 -2
  73. package/src/modules/customers/i18n/de.json +1 -1
  74. package/src/modules/customers/i18n/en.json +1 -1
  75. package/src/modules/customers/i18n/es.json +1 -1
  76. package/src/modules/customers/i18n/ko.json +1 -1
  77. package/src/modules/customers/i18n/pl.json +1 -1
  78. package/src/modules/customers/lib/closureStage.ts +2 -1
  79. package/src/modules/customers/lib/dealStatus.ts +23 -12
  80. package/src/modules/customers/lib/dealsSummaryQueries.ts +3 -3
  81. package/src/modules/customers/migrations/Migration20260824180000_deal_status_lost.ts +55 -0
  82. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +1 -1
  83. package/src/modules/eudr/notifications.ts +5 -0
  84. package/src/modules/query_index/lib/search-tokens.ts +100 -43
  85. package/src/modules/sales/commands/documents.ts +185 -105
  86. package/src/modules/sales/commands/returns.ts +1 -31
  87. package/src/modules/sales/commands/shared.ts +1 -4
  88. package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +12 -1
  89. package/src/modules/sales/data/validators.ts +3 -0
  90. package/src/modules/sales/lib/calculations.ts +33 -7
  91. package/src/modules/sales/lib/json.ts +4 -0
  92. package/src/modules/sales/lib/lineSnapshots.ts +98 -0
  93. package/src/modules/sales/lib/types.ts +20 -0
  94. package/src/modules/warranty_claims/notifications.ts +5 -0
  95. package/src/modules/wms/notifications.ts +2 -0
@@ -2,6 +2,7 @@ const notificationTypes = [
2
2
  {
3
3
  type: "warranty_claims.claim.submitted",
4
4
  module: "warranty_claims",
5
+ channels: ["in_app", "email"],
5
6
  titleKey: "warranty_claims.notifications.submitted.title",
6
7
  bodyKey: "warranty_claims.notifications.submitted.body",
7
8
  icon: "clipboard-check",
@@ -21,6 +22,7 @@ const notificationTypes = [
21
22
  {
22
23
  type: "warranty_claims.claim.assigned",
23
24
  module: "warranty_claims",
25
+ channels: ["in_app", "email"],
24
26
  titleKey: "warranty_claims.notifications.assigned.title",
25
27
  bodyKey: "warranty_claims.notifications.assigned.body",
26
28
  icon: "user-check",
@@ -40,6 +42,7 @@ const notificationTypes = [
40
42
  {
41
43
  type: "warranty_claims.claim.status_changed",
42
44
  module: "warranty_claims",
45
+ channels: ["in_app", "email"],
43
46
  titleKey: "warranty_claims.notifications.statusChanged.title",
44
47
  bodyKey: "warranty_claims.notifications.statusChanged.body",
45
48
  icon: "refresh-cw",
@@ -59,6 +62,7 @@ const notificationTypes = [
59
62
  {
60
63
  type: "warranty_claims.claim.escalated",
61
64
  module: "warranty_claims",
65
+ channels: ["in_app", "email"],
62
66
  titleKey: "warranty_claims.notifications.escalated.title",
63
67
  bodyKey: "warranty_claims.notifications.escalated.body",
64
68
  icon: "alarm-clock",
@@ -78,6 +82,7 @@ const notificationTypes = [
78
82
  {
79
83
  type: "warranty_claims.claim.customer_replied",
80
84
  module: "warranty_claims",
85
+ channels: ["in_app", "email"],
81
86
  titleKey: "warranty_claims.notifications.customerReplied.title",
82
87
  bodyKey: "warranty_claims.notifications.customerReplied.body",
83
88
  icon: "message-square-reply",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/warranty_claims/notifications.ts"],
4
- "sourcesContent": ["import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'\n\nexport const notificationTypes: NotificationTypeDefinition[] = [\n {\n type: 'warranty_claims.claim.submitted',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.submitted.title',\n bodyKey: 'warranty_claims.notifications.submitted.body',\n icon: 'clipboard-check',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.assigned',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.assigned.title',\n bodyKey: 'warranty_claims.notifications.assigned.body',\n icon: 'user-check',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.status_changed',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.statusChanged.title',\n bodyKey: 'warranty_claims.notifications.statusChanged.body',\n icon: 'refresh-cw',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.escalated',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.escalated.title',\n bodyKey: 'warranty_claims.notifications.escalated.body',\n icon: 'alarm-clock',\n severity: 'warning',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.customer_replied',\n module: 'warranty_claims',\n titleKey: 'warranty_claims.notifications.customerReplied.title',\n bodyKey: 'warranty_claims.notifications.customerReplied.body',\n icon: 'message-square-reply',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n]\n\nexport default notificationTypes\n"],
5
- "mappings": "AAEO,MAAM,oBAAkD;AAAA,EAC7D;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AACF;AAEA,IAAO,wBAAQ;",
4
+ "sourcesContent": ["import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'\n\nexport const notificationTypes: NotificationTypeDefinition[] = [\n {\n type: 'warranty_claims.claim.submitted',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.submitted.title',\n bodyKey: 'warranty_claims.notifications.submitted.body',\n icon: 'clipboard-check',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.assigned',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.assigned.title',\n bodyKey: 'warranty_claims.notifications.assigned.body',\n icon: 'user-check',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.status_changed',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.statusChanged.title',\n bodyKey: 'warranty_claims.notifications.statusChanged.body',\n icon: 'refresh-cw',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.escalated',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.escalated.title',\n bodyKey: 'warranty_claims.notifications.escalated.body',\n icon: 'alarm-clock',\n severity: 'warning',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n {\n type: 'warranty_claims.claim.customer_replied',\n module: 'warranty_claims',\n channels: ['in_app', 'email'],\n titleKey: 'warranty_claims.notifications.customerReplied.title',\n bodyKey: 'warranty_claims.notifications.customerReplied.body',\n icon: 'message-square-reply',\n severity: 'info',\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/warranty_claims/{sourceEntityId}',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/warranty_claims/{sourceEntityId}',\n expiresAfterHours: 168,\n },\n]\n\nexport default notificationTypes\n"],
5
+ "mappings": "AAEO,MAAM,oBAAkD;AAAA,EAC7D;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AACF;AAEA,IAAO,wBAAQ;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ const notificationTypes = [
2
2
  {
3
3
  type: "wms.inventory.low_stock",
4
4
  module: "wms",
5
+ channels: ["in_app", "email"],
5
6
  titleKey: "wms.notifications.lowStock.title",
6
7
  bodyKey: "wms.notifications.lowStock.body",
7
8
  icon: "alert-triangle",
@@ -21,6 +22,7 @@ const notificationTypes = [
21
22
  {
22
23
  type: "wms.inventory.reservation_shortfall",
23
24
  module: "wms",
25
+ channels: ["in_app", "email"],
24
26
  titleKey: "wms.notifications.reservationShortfall.title",
25
27
  bodyKey: "wms.notifications.reservationShortfall.body",
26
28
  icon: "alert-triangle",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/wms/notifications.ts"],
4
- "sourcesContent": ["import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'\n\nexport const notificationTypes: NotificationTypeDefinition[] = [\n {\n type: 'wms.inventory.low_stock',\n module: 'wms',\n titleKey: 'wms.notifications.lowStock.title',\n bodyKey: 'wms.notifications.lowStock.body',\n icon: 'alert-triangle',\n severity: 'warning',\n actions: [\n {\n id: 'view',\n labelKey: 'wms.notifications.lowStock.renderer.viewInventory',\n variant: 'outline',\n href: '/backend/wms/inventory',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/wms/inventory',\n expiresAfterHours: 72,\n },\n {\n type: 'wms.inventory.reservation_shortfall',\n module: 'wms',\n titleKey: 'wms.notifications.reservationShortfall.title',\n bodyKey: 'wms.notifications.reservationShortfall.body',\n icon: 'alert-triangle',\n severity: 'warning',\n actions: [\n {\n id: 'view-order',\n labelKey: 'wms.notifications.reservationShortfall.renderer.viewOrder',\n variant: 'outline',\n href: '/backend/sales/orders/{sourceEntityId}',\n icon: 'external-link',\n },\n {\n id: 'view-inventory',\n labelKey: 'wms.notifications.reservationShortfall.renderer.viewInventory',\n variant: 'outline',\n href: '/backend/wms/inventory',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/sales/orders/{sourceEntityId}',\n expiresAfterHours: 72,\n },\n]\n\nexport default notificationTypes\n"],
5
- "mappings": "AAEO,MAAM,oBAAkD;AAAA,EAC7D;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AACF;AAEA,IAAO,wBAAQ;",
4
+ "sourcesContent": ["import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'\n\nexport const notificationTypes: NotificationTypeDefinition[] = [\n {\n type: 'wms.inventory.low_stock',\n module: 'wms',\n channels: ['in_app', 'email'],\n titleKey: 'wms.notifications.lowStock.title',\n bodyKey: 'wms.notifications.lowStock.body',\n icon: 'alert-triangle',\n severity: 'warning',\n actions: [\n {\n id: 'view',\n labelKey: 'wms.notifications.lowStock.renderer.viewInventory',\n variant: 'outline',\n href: '/backend/wms/inventory',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/wms/inventory',\n expiresAfterHours: 72,\n },\n {\n type: 'wms.inventory.reservation_shortfall',\n module: 'wms',\n channels: ['in_app', 'email'],\n titleKey: 'wms.notifications.reservationShortfall.title',\n bodyKey: 'wms.notifications.reservationShortfall.body',\n icon: 'alert-triangle',\n severity: 'warning',\n actions: [\n {\n id: 'view-order',\n labelKey: 'wms.notifications.reservationShortfall.renderer.viewOrder',\n variant: 'outline',\n href: '/backend/sales/orders/{sourceEntityId}',\n icon: 'external-link',\n },\n {\n id: 'view-inventory',\n labelKey: 'wms.notifications.reservationShortfall.renderer.viewInventory',\n variant: 'outline',\n href: '/backend/wms/inventory',\n icon: 'external-link',\n },\n ],\n linkHref: '/backend/sales/orders/{sourceEntityId}',\n expiresAfterHours: 72,\n },\n]\n\nexport default notificationTypes\n"],
5
+ "mappings": "AAEO,MAAM,oBAAkD;AAAA,EAC7D;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,UAAU,OAAO;AAAA,IAC5B,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB;AACF;AAEA,IAAO,wBAAQ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.7.1-develop.7113.1.9d83dca10c",
3
+ "version": "0.7.1-develop.7121.1.734c263bda",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -252,16 +252,16 @@
252
252
  "zod": "^4.4.3"
253
253
  },
254
254
  "peerDependencies": {
255
- "@open-mercato/ai-assistant": "0.7.1-develop.7113.1.9d83dca10c",
256
- "@open-mercato/shared": "0.7.1-develop.7113.1.9d83dca10c",
257
- "@open-mercato/ui": "0.7.1-develop.7113.1.9d83dca10c",
255
+ "@open-mercato/ai-assistant": "0.7.1-develop.7121.1.734c263bda",
256
+ "@open-mercato/shared": "0.7.1-develop.7121.1.734c263bda",
257
+ "@open-mercato/ui": "0.7.1-develop.7121.1.734c263bda",
258
258
  "react": "^19.0.0",
259
259
  "react-dom": "^19.0.0"
260
260
  },
261
261
  "devDependencies": {
262
- "@open-mercato/ai-assistant": "0.7.1-develop.7113.1.9d83dca10c",
263
- "@open-mercato/shared": "0.7.1-develop.7113.1.9d83dca10c",
264
- "@open-mercato/ui": "0.7.1-develop.7113.1.9d83dca10c",
262
+ "@open-mercato/ai-assistant": "0.7.1-develop.7121.1.734c263bda",
263
+ "@open-mercato/shared": "0.7.1-develop.7121.1.734c263bda",
264
+ "@open-mercato/ui": "0.7.1-develop.7121.1.734c263bda",
265
265
  "@testing-library/dom": "^10.4.1",
266
266
  "@testing-library/jest-dom": "^7.0.0",
267
267
  "@testing-library/react": "^16.3.1",
@@ -81,7 +81,7 @@ export async function createDealFixture(
81
81
  valueCurrency?: string;
82
82
  // Optional deal lifecycle fields — all valid on `dealCreateSchema`. Forwarded so KPI/summary
83
83
  // tests can seed won/lost/overdue/owned deals across quarters (see TC-CRM-082). `status` is a
84
- // free-form dictionary value (e.g. 'open', 'in_progress', 'win', 'loose'); `expectedCloseAt`
84
+ // free-form dictionary value (e.g. 'open', 'in_progress', 'win', 'lost'); `expectedCloseAt`
85
85
  // accepts an ISO string (the schema coerces it to a Date); `closureOutcome` is 'won' | 'lost'.
86
86
  status?: string;
87
87
  expectedCloseAt?: string;
@@ -37,15 +37,20 @@ export const metadata = {
37
37
  },
38
38
  }
39
39
 
40
- // `to` is deliberately NOT typed as an email here. The recipient shape depends
41
- // on the provider (an email address for Gmail/IMAP, a channel snowflake for
42
- // Discord), and the adapter is only known once the channel is loaded — so the
43
- // schema accepts any non-empty single-line string and `validateOutboundRecipient`
44
- // applies the provider-appropriate rules below, defaulting to email. The CR/LF
45
- // rejection stays at the schema level so a header-injection attempt is refused
46
- // before the request reaches the channel lookup or the mutation guard.
40
+ // `to` is deliberately NOT typed as an email here, and deliberately optional.
41
+ // The recipient shape depends on the provider (an email address for Gmail/IMAP,
42
+ // a channel snowflake for Discord), and the adapter is only known once the
43
+ // channel is loaded — so the schema accepts any non-empty single-line string and
44
+ // `validateOutboundRecipient` applies the provider-appropriate rules below,
45
+ // defaulting to email. Whether the recipient may be left out is provider-derived
46
+ // too: an adapter that carries its own outbound target (Discord's
47
+ // `defaultChannelId`) accepts a body with no `to` and posts there, which is the
48
+ // smoke test the Discord spec documents; an email provider has no such default
49
+ // and `validateOutboundRecipient` still refuses the request. The CR/LF rejection
50
+ // stays at the schema level so a header-injection attempt is refused before the
51
+ // request reaches the channel lookup or the mutation guard.
47
52
  const bodySchema = z.object({
48
- to: z.string().min(1).max(MAX_OUTBOUND_RECIPIENT_LENGTH).regex(/^[^\r\n]*$/),
53
+ to: z.string().min(1).max(MAX_OUTBOUND_RECIPIENT_LENGTH).regex(/^[^\r\n]*$/).optional(),
49
54
  subject: z.string().min(1).max(500).optional(),
50
55
  body: z.string().max(50_000).optional(),
51
56
  })
@@ -232,7 +237,14 @@ export async function POST(req: Request, context: RouteContext): Promise<Respons
232
237
  tenantId: auth.tenantId as string,
233
238
  organizationId: channelCredentialsOrg,
234
239
  },
235
- metadata: { to: body.to, subject: body.subject ?? 'Test send', testSend: true },
240
+ // `to` is omitted from the metadata rather than passed as `undefined` when
241
+ // the caller left it out, so an adapter reading `metadata.to` sees no key
242
+ // at all and falls through to its own configured target.
243
+ metadata: {
244
+ ...(body.to !== undefined ? { to: body.to } : {}),
245
+ subject: body.subject ?? 'Test send',
246
+ testSend: true,
247
+ },
236
248
  })
237
249
  await guard.afterSuccess()
238
250
  return NextResponse.json({
@@ -60,6 +60,17 @@ export interface ChannelCapabilities {
60
60
  * applies transport-safety checks only (see `validateOutboundRecipient`) and
61
61
  * the adapter owns the provider-specific format — it MUST treat the value as
62
62
  * untrusted input.
63
+ *
64
+ * **`'provider-native'` also opts the provider into presence-optionality, not
65
+ * only format.** The hub accepts an outbound request that names no recipient
66
+ * at all and forwards it with no `metadata.to` key, on the understanding that
67
+ * the adapter resolves its own configured target (Discord: `defaultChannelId`).
68
+ * An adapter declaring this therefore MUST resolve such a target, and MUST
69
+ * fail legibly — an operator-readable `SendMessageResult.error`, not a throw —
70
+ * when it has none, because that message is surfaced verbatim to whoever ran
71
+ * the send. Declare `'email'` (or omit the field) if your provider has no
72
+ * default destination: the hub then keeps answering `Recipient is required`,
73
+ * which is the correct outcome for a request with nowhere to go.
63
74
  */
64
75
  recipientFormat?: 'email' | 'provider-native'
65
76
  }
@@ -37,11 +37,24 @@ export type OutboundRecipientCheck = { ok: true } | { ok: false; error: string }
37
37
  * path to send at all (#4976). Validation now follows the adapter's
38
38
  * `capabilities.recipientFormat`, defaulting to `'email'` so every existing
39
39
  * provider keeps byte-identical behavior.
40
+ *
41
+ * A `'provider-native'` provider may also be addressed by **omitting** the
42
+ * recipient entirely: those adapters are configured with their own outbound
43
+ * target (Discord's `defaultChannelId`) and fall back to it when the message
44
+ * names no channel, which is the smoke test the Discord spec documents in
45
+ * § 6 "Test it". An email provider has no such default, so `'email'` — the
46
+ * format every existing provider resolves to — still requires a recipient.
47
+ * Omission means `undefined` only: `null` or `''` is a caller that meant to
48
+ * supply an address and got it wrong, and stays a 422 on every provider.
40
49
  */
41
50
  export function validateOutboundRecipient(
42
51
  recipient: unknown,
43
52
  capabilities: Pick<ChannelCapabilities, 'recipientFormat'> | null | undefined,
44
53
  ): OutboundRecipientCheck {
54
+ const isProviderNative = capabilities?.recipientFormat === 'provider-native'
55
+ if (recipient === undefined && isProviderNative) {
56
+ return { ok: true }
57
+ }
45
58
  if (typeof recipient !== 'string' || recipient.length === 0) {
46
59
  return { ok: false, error: 'Recipient is required' }
47
60
  }
@@ -51,7 +64,7 @@ export function validateOutboundRecipient(
51
64
  error: `Recipient must be at most ${MAX_OUTBOUND_RECIPIENT_LENGTH} characters`,
52
65
  }
53
66
  }
54
- if (capabilities?.recipientFormat !== 'provider-native') {
67
+ if (!isProviderNative) {
55
68
  return emailRecipientSchema.safeParse(recipient).success
56
69
  ? { ok: true }
57
70
  : { ok: false, error: 'Recipient must be a valid email address' }
@@ -136,6 +136,24 @@ class TestSeedChannelAdapter implements ChannelAdapter {
136
136
  }
137
137
  }
138
138
 
139
+ /**
140
+ * Capabilities for the chat stub. Identical to the email stub's except for the
141
+ * recipient shape: a chat channel is addressed by a provider-issued identifier
142
+ * (a Discord snowflake), not an email address.
143
+ *
144
+ * Without this override the chat stub inherited `baseEmailCapabilities`, so it
145
+ * claimed `channelType: 'discord'` while validating recipients as email
146
+ * addresses — a channel that could not be addressed the way the provider it
147
+ * imitates actually is. That left the hub's `'provider-native'` branch
148
+ * (#4976: per-provider recipient validation, and the omitted recipient that
149
+ * falls back to the adapter's own target) reachable only with a live Discord
150
+ * bot, so no integration test could cover it and CI never exercised it.
151
+ */
152
+ const testSeedChatCapabilities: ChannelCapabilities = {
153
+ ...testSeedCapabilities,
154
+ recipientFormat: 'provider-native',
155
+ }
156
+
139
157
  /**
140
158
  * Chat-flavoured twin of {@link TestSeedChannelAdapter}: same network-free
141
159
  * behaviour, but it declares a non-email `channelType`, so a channel connected
@@ -145,6 +163,7 @@ class TestSeedChannelAdapter implements ChannelAdapter {
145
163
  class TestSeedChatChannelAdapter extends TestSeedChannelAdapter {
146
164
  readonly providerKey: string = TEST_SEED_CHAT_PROVIDER_KEY
147
165
  readonly channelType: string = 'discord'
166
+ readonly capabilities = testSeedChatCapabilities
148
167
 
149
168
  async normalizeInbound(raw: InboundMessage): Promise<NormalizedInboundMessage> {
150
169
  // Unlike the email stub, this one is reachable: the test-seed ingest action
@@ -88,7 +88,7 @@ export function useDealClosure({
88
88
  const lossPayload = {
89
89
  id: currentDealId,
90
90
  closureOutcome: 'lost' as const,
91
- status: 'loose',
91
+ status: 'lost',
92
92
  lossReasonId: input.lossReasonId,
93
93
  ...(input.lossNotes ? { lossNotes: input.lossNotes } : {}),
94
94
  }
@@ -28,6 +28,7 @@ import { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'
28
28
  import { Tag } from '@open-mercato/ui/primitives/tag'
29
29
  import { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'
30
30
  import { Briefcase, AlertTriangle, X } from 'lucide-react'
31
+ import { isLostDealStatus, isWonDealStatus } from '../../../lib/dealStatus'
31
32
  import { formatRelativeTime } from '@open-mercato/shared/lib/time'
32
33
  import { ViewTabsRow } from './pipeline/components/ViewTabsRow'
33
34
  import { DealsKpiStrip } from '../../../components/DealsKpiStrip'
@@ -86,7 +87,7 @@ function makeDealsPresets(): FilterPreset[] {
86
87
  },
87
88
  },
88
89
  // The Deal entity has no dedicated "at risk" or health-score field — `customer_deals`
89
- // exposes only `status` (open/win/loose/closed/in_progress, dictionary-driven) and
90
+ // exposes only `status` (open/win/lost/closed/in_progress, dictionary-driven) and
90
91
  // `closure_outcome`. Rather than fabricate a mapping, the "At risk" preset is omitted
91
92
  // until the data model exposes a first-class signal.
92
93
  {
@@ -855,11 +856,11 @@ export default function CustomersDealsPage() {
855
856
  subtitle = (
856
857
  <span className="text-xs text-status-error-text">{t('customers.deals.list.close.overdue')}</span>
857
858
  )
858
- } else if (row.original.status === 'win') {
859
+ } else if (isWonDealStatus(row.original.status)) {
859
860
  subtitle = (
860
861
  <span className="text-xs text-muted-foreground">{t('customers.deals.list.close.won')}</span>
861
862
  )
862
- } else if (row.original.status === 'loose') {
863
+ } else if (isLostDealStatus(row.original.status)) {
863
864
  subtitle = (
864
865
  <span className="text-xs text-muted-foreground">{t('customers.deals.list.close.lost')}</span>
865
866
  )
@@ -21,11 +21,11 @@ import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
21
21
  import { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'
22
22
 
23
23
  // Deal status values written by the kanban flow. These intentionally do NOT include 'closed'
24
- // because the rest of the app only persists 'loose' for lost deals (see StatusFilterPopover
24
+ // because the rest of the app only persists 'lost' for lost deals (see StatusFilterPopover
25
25
  // notes). 'in_progress' is kept for backwards compatibility with quick-deals created from
26
- // the previous version of this dialog. The labels are looked up via i18n at render time so
27
- // the historic typo 'loose' doesn't leak to operators the storage value stays unchanged.
28
- const STATUS_OPTIONS = ['open', 'in_progress', 'win', 'loose'] as const
26
+ // the previous version of this dialog. Deals written before 0.7.1 carry the misspelled
27
+ // 'loose'; readers still match it through `lib/dealStatus.ts`, but no writer produces it.
28
+ const STATUS_OPTIONS = ['open', 'in_progress', 'win', 'lost'] as const
29
29
 
30
30
  /**
31
31
  * Tenant currency option. The page resolves these from the currencies module (see
@@ -266,7 +266,7 @@ export function QuickDealDialog({
266
266
  id: 'status',
267
267
  label: translateWithFallback(t, 'customers.deals.kanban.quickDeal.status', 'Status'),
268
268
  type: 'select',
269
- // Storage value is preserved (`loose` etc.) only the displayed label is
269
+ // Storage value is preserved (`lost` etc.), only the displayed label is
270
270
  // translated, so we never write a localised string back to the DB.
271
271
  options: STATUS_OPTIONS.map((statusValue) => ({
272
272
  value: statusValue,
@@ -23,13 +23,13 @@ import { FilterPopoverShell } from './FilterPopoverShell'
23
23
  * Filter options exposed to the operator.
24
24
  *
25
25
  * The deal `status` column is dictionary-driven (`deal-statuses`); the seeded dictionary
26
- * carries five values (`open`, `closed`, `win`, `loose`, `in_progress`) plus any
26
+ * carries five values (`open`, `closed`, `win`, `lost`, `in_progress`) plus any
27
27
  * tenant-custom entries. We render every dictionary entry de-duplicated by canonical
28
28
  * spelling so the kanban Status pill stays aligned with the list page's advanced filter
29
29
  * (which also uses the dictionary). A hard-coded fallback keeps the popover usable while
30
30
  * the dictionary is loading or when a tenant has no entries.
31
31
  *
32
- * `won` / `lost` are accepted as aliases for `win` / `loose` at the API layer (see
32
+ * `won` / `loose` are accepted as aliases for `win` / `lost` at the API layer (see
33
33
  * `lib/dealStatus.ts:expandDealStatusAliases`, shared by the deals list route and the
34
34
  * kanban aggregate route). The UI only exposes the canonical values to avoid duplicate
35
35
  * pills, but `canonicalDealStatus` normalizes any alias passed in through `values` so
@@ -57,7 +57,7 @@ const FALLBACK_STATUS_OPTIONS: Array<{
57
57
  dotClass: 'bg-status-success-icon',
58
58
  },
59
59
  {
60
- value: 'loose',
60
+ value: 'lost',
61
61
  labelKey: 'customers.deals.kanban.filter.status.lost',
62
62
  labelFallback: 'Lost',
63
63
  dotClass: 'bg-status-error-icon',
@@ -1766,7 +1766,7 @@ export default function DealsKanbanPage(): React.ReactElement {
1766
1766
  }, [invalidateKanbanData, scopeVersion, selectedPipelineId])
1767
1767
 
1768
1768
  const updateDealStatus = React.useCallback(
1769
- async (dealId: string, status: 'win' | 'loose') => {
1769
+ async (dealId: string, status: 'win' | 'lost') => {
1770
1770
  const dealVersion = deals.find((deal) => deal.id === dealId)?.updatedAt ?? null
1771
1771
  setPendingDealId(dealId)
1772
1772
  try {
@@ -2348,7 +2348,7 @@ export default function DealsKanbanPage(): React.ReactElement {
2348
2348
  {
2349
2349
  id: 'mark-lost',
2350
2350
  label: translateWithFallback(t, 'customers.deals.kanban.menu.markLost', 'Mark as Lost'),
2351
- onSelect: () => void updateDealStatus(deal.id, 'loose'),
2351
+ onSelect: () => void updateDealStatus(deal.id, 'lost'),
2352
2352
  },
2353
2353
  {
2354
2354
  id: 'delete',
@@ -59,7 +59,7 @@ export const DEAL_STATUS_DEFAULTS: DictionaryDefault[] = [
59
59
  { value: 'open', label: 'Open', color: '#2563eb', icon: 'lucide:circle' },
60
60
  { value: 'closed', label: 'Closed', color: '#6b7280', icon: 'lucide:check-circle' },
61
61
  { value: 'win', label: 'Win', color: '#22c55e', icon: 'lucide:trophy' },
62
- { value: 'loose', label: 'Loose', color: '#ef4444', icon: 'lucide:flag' },
62
+ { value: 'lost', label: 'Lost', color: '#ef4444', icon: 'lucide:flag' },
63
63
  { value: 'in_progress', label: 'In progress', color: '#f59e0b', icon: 'lucide:activity' },
64
64
  ]
65
65
 
@@ -70,7 +70,7 @@ export const PIPELINE_STAGE_DEFAULTS: DictionaryDefault[] = [
70
70
  { value: 'offering', label: 'Offering', color: '#22c55e', icon: 'lucide:package' },
71
71
  { value: 'negotiations', label: 'Negotiations', color: '#facc15', icon: 'lucide:handshake' },
72
72
  { value: 'win', label: 'Win', color: '#16a34a', icon: 'lucide:award' },
73
- { value: 'loose', label: 'Loose', color: '#ef4444', icon: 'lucide:flag' },
73
+ { value: 'lost', label: 'Lost', color: '#ef4444', icon: 'lucide:flag' },
74
74
  { value: 'stalled', label: 'Stalled', color: '#6b7280', icon: 'lucide:alert-circle' },
75
75
  ]
76
76
 
@@ -827,8 +827,8 @@ export const CUSTOMER_EXAMPLES: ExampleCompany[] = [
827
827
  slug: 'cedar-creek-retreat',
828
828
  title: 'Cedar Creek Retreat Expansion',
829
829
  description: 'New wellness center build-out including retail area and treatment rooms.',
830
- status: 'loose',
831
- pipelineStage: 'loose',
830
+ status: 'lost',
831
+ pipelineStage: 'lost',
832
832
  valueAmount: 98000,
833
833
  valueCurrency: 'USD',
834
834
  expectedCloseAt: isoDaysFromNow(-70),
@@ -2549,7 +2549,7 @@ async function seedCustomerStressTest(
2549
2549
  const dealId = randomUUID()
2550
2550
  const valueAmount = toAmount(monetaryBase + randomInt(0, 7500))
2551
2551
  const expectedCloseAt =
2552
- dealStatus === 'win' || dealStatus === 'closed' || dealStatus === 'loose'
2552
+ dealStatus === 'win' || dealStatus === 'closed' || dealStatus === 'lost'
2553
2553
  ? randomPastDate(120)
2554
2554
  : randomFutureDate(120)
2555
2555
  const dealRow: CustomerDealRow = {
@@ -120,9 +120,9 @@ function resolveRequestedClosureOutcome(input: DealUpdateInput): DealClosureOutc
120
120
  if (input.closureOutcome === 'won' || input.closureOutcome === 'lost') {
121
121
  return input.closureOutcome
122
122
  }
123
- // `win` / `loose` are what the UI closure flows persist; `won` / `lost` are the
124
- // spellings the AI stage tool persists. Both must derive the same closure outcome so
125
- // every closed deal lands in the pipeline's terminal stage (#5107).
123
+ // `win` / `lost` are what the UI closure flows persist; `won` / `lost` are the
124
+ // spellings the AI stage tool persists (`loose` before 0.7.1). Both must derive the same
125
+ // closure outcome so every closed deal lands in the pipeline's terminal stage (#5107).
126
126
  return dealClosureOutcomeFromStatus(input.status)
127
127
  }
128
128
 
@@ -846,6 +846,7 @@ const updateDealCommand: CommandHandler<DealUpdateInput, { dealId: string }> = {
846
846
  // subscribers (workflow event triggers, business-rules triggers) drop a
847
847
  // null-scoped event before trigger matching.
848
848
  const newStatus = record.status
849
+ // `loose` stays mapped here for deals written before the 0.7.1 rename.
849
850
  const normalizedStatus = newStatus === 'win' ? 'won' : newStatus === 'loose' ? 'lost' : newStatus
850
851
  if (previousStatus !== newStatus && (normalizedStatus === 'won' || normalizedStatus === 'lost')) {
851
852
  const closureEvent = normalizedStatus === 'won' ? 'customers.deal.won' : 'customers.deal.lost'
@@ -14,6 +14,7 @@ const TERMINAL_OUTCOME_LABELS = new Set([
14
14
  'closed won',
15
15
  'closed win',
16
16
  'lost',
17
+ // Pre-0.7.1 misspelling; unmigrated stage labels still carry it.
17
18
  'loose',
18
19
  'closed lost',
19
20
  'closed loose',
@@ -276,7 +276,7 @@ export function createDealLinkAdapter(options: DealAdapterOptions): LinkEntityAd
276
276
  { id: 'all', label: 'All' },
277
277
  { id: 'open', label: 'Open' },
278
278
  { id: 'win', label: 'Won' },
279
- { id: 'loose', label: 'Lost' },
279
+ { id: 'lost', label: 'Lost' },
280
280
  { id: 'orphan', label: 'Orphan', dotColor: '#eb9426' /* TODO(ds-review): #eb9426 — requires LinkEntityDialog to support semantic token classNames */ },
281
281
  ],
282
282
  defaultId: 'all',
@@ -286,7 +286,7 @@ export function createDealLinkAdapter(options: DealAdapterOptions): LinkEntityAd
286
286
  if (filterId === 'orphan') return meta.isOrphan === true
287
287
  // Server-side status filter already applied; this keeps all rows if server honored the filter.
288
288
  // Fallback client check on stage in case server returns mixed statuses.
289
- if (filterId === 'open' || filterId === 'win' || filterId === 'loose') {
289
+ if (filterId === 'open' || filterId === 'win' || filterId === 'lost') {
290
290
  return (meta.stage ?? '').toLowerCase().includes(filterId) || true
291
291
  }
292
292
  return true
@@ -1380,7 +1380,7 @@
1380
1380
  "customers.deals.kanban.quickDeal.probability": "Probability",
1381
1381
  "customers.deals.kanban.quickDeal.status": "Status",
1382
1382
  "customers.deals.kanban.quickDeal.status.in_progress": "In Bearbeitung",
1383
- "customers.deals.kanban.quickDeal.status.loose": "Stagniert",
1383
+ "customers.deals.kanban.quickDeal.status.lost": "Verloren",
1384
1384
  "customers.deals.kanban.quickDeal.status.open": "Offen",
1385
1385
  "customers.deals.kanban.quickDeal.status.win": "Gewonnen",
1386
1386
  "customers.deals.kanban.quickDeal.submit": "Add deal",
@@ -1380,7 +1380,7 @@
1380
1380
  "customers.deals.kanban.quickDeal.probability": "Probability",
1381
1381
  "customers.deals.kanban.quickDeal.status": "Status",
1382
1382
  "customers.deals.kanban.quickDeal.status.in_progress": "In progress",
1383
- "customers.deals.kanban.quickDeal.status.loose": "Stalled",
1383
+ "customers.deals.kanban.quickDeal.status.lost": "Lost",
1384
1384
  "customers.deals.kanban.quickDeal.status.open": "Open",
1385
1385
  "customers.deals.kanban.quickDeal.status.win": "Won",
1386
1386
  "customers.deals.kanban.quickDeal.submit": "Add deal",
@@ -1380,7 +1380,7 @@
1380
1380
  "customers.deals.kanban.quickDeal.probability": "Probability",
1381
1381
  "customers.deals.kanban.quickDeal.status": "Status",
1382
1382
  "customers.deals.kanban.quickDeal.status.in_progress": "En curso",
1383
- "customers.deals.kanban.quickDeal.status.loose": "Estancado",
1383
+ "customers.deals.kanban.quickDeal.status.lost": "Perdido",
1384
1384
  "customers.deals.kanban.quickDeal.status.open": "Abierto",
1385
1385
  "customers.deals.kanban.quickDeal.status.win": "Ganado",
1386
1386
  "customers.deals.kanban.quickDeal.submit": "Add deal",
@@ -1380,7 +1380,7 @@
1380
1380
  "customers.deals.kanban.quickDeal.probability": "확률",
1381
1381
  "customers.deals.kanban.quickDeal.status": "상태",
1382
1382
  "customers.deals.kanban.quickDeal.status.in_progress": "진행 중",
1383
- "customers.deals.kanban.quickDeal.status.loose": "정체됨",
1383
+ "customers.deals.kanban.quickDeal.status.lost": "실패",
1384
1384
  "customers.deals.kanban.quickDeal.status.open": "열림",
1385
1385
  "customers.deals.kanban.quickDeal.status.win": "성사",
1386
1386
  "customers.deals.kanban.quickDeal.submit": "거래 추가",
@@ -1380,7 +1380,7 @@
1380
1380
  "customers.deals.kanban.quickDeal.probability": "Prawdopodobieństwo",
1381
1381
  "customers.deals.kanban.quickDeal.status": "Status",
1382
1382
  "customers.deals.kanban.quickDeal.status.in_progress": "W toku",
1383
- "customers.deals.kanban.quickDeal.status.loose": "Wstrzymany",
1383
+ "customers.deals.kanban.quickDeal.status.lost": "Przegrany",
1384
1384
  "customers.deals.kanban.quickDeal.status.open": "Otwarty",
1385
1385
  "customers.deals.kanban.quickDeal.status.win": "Wygrany",
1386
1386
  "customers.deals.kanban.quickDeal.submit": "+ Dodaj szansę",
@@ -19,7 +19,8 @@ export const TERMINAL_PIPELINE_STAGE_LABELS: Record<DealClosureOutcome, Readonly
19
19
  }
20
20
 
21
21
  /**
22
- * Map a deal status spelling (UI `win`/`loose` or AI `won`/`lost`) to its closure
22
+ * Map a deal status spelling (`win`/`lost`, the AI tool's `won`, or the pre-0.7.1
23
+ * `loose`) to its closure
23
24
  * outcome. Case-insensitive because callers such as the AI tool pass free-form model
24
25
  * text through, while every persisted status is lower-case.
25
26
  */