@open-mercato/core 0.7.1-develop.7173.1.a901520d0f → 0.7.1-develop.7176.1.d229567a50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/phone_call/index.js +53 -0
- package/dist/generated/entities/phone_call/index.js.map +7 -0
- package/dist/generated/entities/phone_call_participant/index.js +27 -0
- package/dist/generated/entities/phone_call_participant/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +5 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +41 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +20 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/entities/cli.js +46 -3
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/entities/migrations/Migration20260822120000.js +44 -0
- package/dist/modules/entities/migrations/Migration20260822120000.js.map +7 -0
- package/dist/modules/phone_calls/acl.js +15 -0
- package/dist/modules/phone_calls/acl.js.map +7 -0
- package/dist/modules/phone_calls/api/calls/route.js +132 -0
- package/dist/modules/phone_calls/api/calls/route.js.map +7 -0
- package/dist/modules/phone_calls/api/openapi.js +23 -0
- package/dist/modules/phone_calls/api/openapi.js.map +7 -0
- package/dist/modules/phone_calls/backend/page.js +261 -0
- package/dist/modules/phone_calls/backend/page.js.map +7 -0
- package/dist/modules/phone_calls/backend/page.meta.js +16 -0
- package/dist/modules/phone_calls/backend/page.meta.js.map +7 -0
- package/dist/modules/phone_calls/commands/calls.js +125 -0
- package/dist/modules/phone_calls/commands/calls.js.map +7 -0
- package/dist/modules/phone_calls/commands/index.js +2 -0
- package/dist/modules/phone_calls/commands/index.js.map +7 -0
- package/dist/modules/phone_calls/data/entities.js +162 -0
- package/dist/modules/phone_calls/data/entities.js.map +7 -0
- package/dist/modules/phone_calls/data/validators.js +68 -0
- package/dist/modules/phone_calls/data/validators.js.map +7 -0
- package/dist/modules/phone_calls/di.js +12 -0
- package/dist/modules/phone_calls/di.js.map +7 -0
- package/dist/modules/phone_calls/encryption.js +28 -0
- package/dist/modules/phone_calls/encryption.js.map +7 -0
- package/dist/modules/phone_calls/events.js +18 -0
- package/dist/modules/phone_calls/events.js.map +7 -0
- package/dist/modules/phone_calls/index.js +15 -0
- package/dist/modules/phone_calls/index.js.map +7 -0
- package/dist/modules/phone_calls/migrations/Migration20260706124120_phone_calls.js +22 -0
- package/dist/modules/phone_calls/migrations/Migration20260706124120_phone_calls.js.map +7 -0
- package/dist/modules/phone_calls/migrations/Migration20260814124333_phone_calls.js +13 -0
- package/dist/modules/phone_calls/migrations/Migration20260814124333_phone_calls.js.map +7 -0
- package/dist/modules/phone_calls/setup.js +16 -0
- package/dist/modules/phone_calls/setup.js.map +7 -0
- package/generated/entities/phone_call/index.ts +25 -0
- package/generated/entities/phone_call_participant/index.ts +12 -0
- package/generated/entities.ids.generated.ts +5 -0
- package/generated/entity-fields-registry.ts +41 -0
- package/package.json +7 -7
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/configs/lib/upgrade-actions.ts +20 -0
- package/src/modules/entities/cli.ts +61 -1
- package/src/modules/entities/migrations/Migration20260822120000.ts +56 -0
- package/src/modules/phone_calls/acl.ts +11 -0
- package/src/modules/phone_calls/api/calls/route.ts +150 -0
- package/src/modules/phone_calls/api/openapi.ts +25 -0
- package/src/modules/phone_calls/backend/page.meta.ts +12 -0
- package/src/modules/phone_calls/backend/page.tsx +303 -0
- package/src/modules/phone_calls/commands/calls.ts +156 -0
- package/src/modules/phone_calls/commands/index.ts +1 -0
- package/src/modules/phone_calls/data/entities.ts +144 -0
- package/src/modules/phone_calls/data/validators.ts +69 -0
- package/src/modules/phone_calls/di.ts +10 -0
- package/src/modules/phone_calls/encryption.ts +26 -0
- package/src/modules/phone_calls/events.ts +25 -0
- package/src/modules/phone_calls/i18n/de.json +42 -0
- package/src/modules/phone_calls/i18n/en.json +42 -0
- package/src/modules/phone_calls/i18n/es.json +42 -0
- package/src/modules/phone_calls/i18n/ko.json +42 -0
- package/src/modules/phone_calls/i18n/pl.json +42 -0
- package/src/modules/phone_calls/index.ts +13 -0
- package/src/modules/phone_calls/migrations/.snapshot-open-mercato.json +772 -0
- package/src/modules/phone_calls/migrations/Migration20260706124120_phone_calls.ts +23 -0
- package/src/modules/phone_calls/migrations/Migration20260814124333_phone_calls.ts +13 -0
- package/src/modules/phone_calls/setup.ts +14 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/phone_calls/setup.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: [\n 'phone_calls.*',\n ],\n employee: [\n 'phone_calls.view',\n ],\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO;AAAA,MACL;AAAA,IACF;AAAA,IACA,UAAU;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const id = "id";
|
|
2
|
+
export const organization_id = "organization_id";
|
|
3
|
+
export const tenant_id = "tenant_id";
|
|
4
|
+
export const provider_key = "provider_key";
|
|
5
|
+
export const integration_id = "integration_id";
|
|
6
|
+
export const external_call_id = "external_call_id";
|
|
7
|
+
export const external_conversation_id = "external_conversation_id";
|
|
8
|
+
export const direction = "direction";
|
|
9
|
+
export const status = "status";
|
|
10
|
+
export const started_at = "started_at";
|
|
11
|
+
export const answered_at = "answered_at";
|
|
12
|
+
export const ended_at = "ended_at";
|
|
13
|
+
export const duration_seconds = "duration_seconds";
|
|
14
|
+
export const recording_url = "recording_url";
|
|
15
|
+
export const recording_attachment_id = "recording_attachment_id";
|
|
16
|
+
export const active_transcript_version_id = "active_transcript_version_id";
|
|
17
|
+
export const active_summary_version_id = "active_summary_version_id";
|
|
18
|
+
export const communication_projection_id = "communication_projection_id";
|
|
19
|
+
export const provider_facts = "provider_facts";
|
|
20
|
+
export const raw_snapshot = "raw_snapshot";
|
|
21
|
+
export const ingest_status = "ingest_status";
|
|
22
|
+
export const last_ingested_at = "last_ingested_at";
|
|
23
|
+
export const created_at = "created_at";
|
|
24
|
+
export const updated_at = "updated_at";
|
|
25
|
+
export const deleted_at = "deleted_at";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const id = "id";
|
|
2
|
+
export const organization_id = "organization_id";
|
|
3
|
+
export const tenant_id = "tenant_id";
|
|
4
|
+
export const phone_call_id = "phone_call_id";
|
|
5
|
+
export const provider_participant_id = "provider_participant_id";
|
|
6
|
+
export const role = "role";
|
|
7
|
+
export const phone_number = "phone_number";
|
|
8
|
+
export const display_name = "display_name";
|
|
9
|
+
export const email = "email";
|
|
10
|
+
export const metadata = "metadata";
|
|
11
|
+
export const created_at = "created_at";
|
|
12
|
+
export const updated_at = "updated_at";
|
|
@@ -34,6 +34,7 @@ export const M = {
|
|
|
34
34
|
"messages": "messages",
|
|
35
35
|
"communication_channels": "communication_channels",
|
|
36
36
|
"push_notifications": "push_notifications",
|
|
37
|
+
"phone_calls": "phone_calls",
|
|
37
38
|
"translations": "translations",
|
|
38
39
|
"inbox_ops": "inbox_ops",
|
|
39
40
|
"payment_gateways": "payment_gateways",
|
|
@@ -293,6 +294,10 @@ export const M = {
|
|
|
293
294
|
"push_notifications": {
|
|
294
295
|
"push_notification_delivery": "push_notifications:push_notification_delivery"
|
|
295
296
|
},
|
|
297
|
+
"phone_calls": {
|
|
298
|
+
"phone_call": "phone_calls:phone_call",
|
|
299
|
+
"phone_call_participant": "phone_calls:phone_call_participant"
|
|
300
|
+
},
|
|
296
301
|
"translations": {
|
|
297
302
|
"entity_translation": "translations:entity_translation"
|
|
298
303
|
},
|
|
@@ -1872,6 +1872,47 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
|
|
|
1872
1872
|
"updated_at": "updated_at",
|
|
1873
1873
|
"deleted_at": "deleted_at"
|
|
1874
1874
|
},
|
|
1875
|
+
"phone_call": {
|
|
1876
|
+
"id": "id",
|
|
1877
|
+
"organization_id": "organization_id",
|
|
1878
|
+
"tenant_id": "tenant_id",
|
|
1879
|
+
"provider_key": "provider_key",
|
|
1880
|
+
"integration_id": "integration_id",
|
|
1881
|
+
"external_call_id": "external_call_id",
|
|
1882
|
+
"external_conversation_id": "external_conversation_id",
|
|
1883
|
+
"direction": "direction",
|
|
1884
|
+
"status": "status",
|
|
1885
|
+
"started_at": "started_at",
|
|
1886
|
+
"answered_at": "answered_at",
|
|
1887
|
+
"ended_at": "ended_at",
|
|
1888
|
+
"duration_seconds": "duration_seconds",
|
|
1889
|
+
"recording_url": "recording_url",
|
|
1890
|
+
"recording_attachment_id": "recording_attachment_id",
|
|
1891
|
+
"active_transcript_version_id": "active_transcript_version_id",
|
|
1892
|
+
"active_summary_version_id": "active_summary_version_id",
|
|
1893
|
+
"communication_projection_id": "communication_projection_id",
|
|
1894
|
+
"provider_facts": "provider_facts",
|
|
1895
|
+
"raw_snapshot": "raw_snapshot",
|
|
1896
|
+
"ingest_status": "ingest_status",
|
|
1897
|
+
"last_ingested_at": "last_ingested_at",
|
|
1898
|
+
"created_at": "created_at",
|
|
1899
|
+
"updated_at": "updated_at",
|
|
1900
|
+
"deleted_at": "deleted_at"
|
|
1901
|
+
},
|
|
1902
|
+
"phone_call_participant": {
|
|
1903
|
+
"id": "id",
|
|
1904
|
+
"organization_id": "organization_id",
|
|
1905
|
+
"tenant_id": "tenant_id",
|
|
1906
|
+
"phone_call_id": "phone_call_id",
|
|
1907
|
+
"provider_participant_id": "provider_participant_id",
|
|
1908
|
+
"role": "role",
|
|
1909
|
+
"phone_number": "phone_number",
|
|
1910
|
+
"display_name": "display_name",
|
|
1911
|
+
"email": "email",
|
|
1912
|
+
"metadata": "metadata",
|
|
1913
|
+
"created_at": "created_at",
|
|
1914
|
+
"updated_at": "updated_at"
|
|
1915
|
+
},
|
|
1875
1916
|
"planner_availability_rule": {
|
|
1876
1917
|
"id": "id",
|
|
1877
1918
|
"tenant_id": "tenant_id",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7176.1.d229567a50",
|
|
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.
|
|
256
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
257
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7176.1.d229567a50",
|
|
256
|
+
"@open-mercato/shared": "0.7.1-develop.7176.1.d229567a50",
|
|
257
|
+
"@open-mercato/ui": "0.7.1-develop.7176.1.d229567a50",
|
|
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.
|
|
263
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
264
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7176.1.d229567a50",
|
|
263
|
+
"@open-mercato/shared": "0.7.1-develop.7176.1.d229567a50",
|
|
264
|
+
"@open-mercato/ui": "0.7.1-develop.7176.1.d229567a50",
|
|
265
265
|
"@testing-library/dom": "^10.4.1",
|
|
266
266
|
"@testing-library/jest-dom": "^7.0.1",
|
|
267
267
|
"@testing-library/react": "^16.3.3",
|
|
@@ -203,6 +203,8 @@
|
|
|
203
203
|
"auth.acl.features.payment_gateways.view": "View payment gateway transactions",
|
|
204
204
|
"auth.acl.features.perspectives.role_defaults": "Manage role default perspectives",
|
|
205
205
|
"auth.acl.features.perspectives.use": "Use table perspectives",
|
|
206
|
+
"auth.acl.features.phone_calls.manage": "Telefonanrufe verwalten (Abruf, Rohdaten)",
|
|
207
|
+
"auth.acl.features.phone_calls.view": "Telefonanrufe anzeigen",
|
|
206
208
|
"auth.acl.features.planner.manage_availability": "Manage availability",
|
|
207
209
|
"auth.acl.features.planner.view": "View schedules",
|
|
208
210
|
"auth.acl.features.progress.cancel": "Cancel progress jobs",
|
|
@@ -280,6 +282,7 @@
|
|
|
280
282
|
"auth.acl.features.storage_providers.manage": "Manage storage providers",
|
|
281
283
|
"auth.acl.features.sync_excel.run": "Upload CSV files and prepare imports",
|
|
282
284
|
"auth.acl.features.sync_excel.view": "View sync_excel uploads and previews",
|
|
285
|
+
"auth.acl.features.tillio.manage": "Tillio-Integration verwalten",
|
|
283
286
|
"auth.acl.features.translations.manage": "Manage entity translations",
|
|
284
287
|
"auth.acl.features.translations.manage_locales": "Configure supported locales",
|
|
285
288
|
"auth.acl.features.translations.view": "View translations",
|
|
@@ -203,6 +203,8 @@
|
|
|
203
203
|
"auth.acl.features.payment_gateways.view": "View payment gateway transactions",
|
|
204
204
|
"auth.acl.features.perspectives.role_defaults": "Manage role default perspectives",
|
|
205
205
|
"auth.acl.features.perspectives.use": "Use table perspectives",
|
|
206
|
+
"auth.acl.features.phone_calls.manage": "Manage phone calls (pull, raw payload)",
|
|
207
|
+
"auth.acl.features.phone_calls.view": "View phone calls",
|
|
206
208
|
"auth.acl.features.planner.manage_availability": "Manage availability",
|
|
207
209
|
"auth.acl.features.planner.view": "View schedules",
|
|
208
210
|
"auth.acl.features.progress.cancel": "Cancel progress jobs",
|
|
@@ -280,6 +282,7 @@
|
|
|
280
282
|
"auth.acl.features.storage_providers.manage": "Manage storage providers",
|
|
281
283
|
"auth.acl.features.sync_excel.run": "Upload CSV files and prepare imports",
|
|
282
284
|
"auth.acl.features.sync_excel.view": "View sync_excel uploads and previews",
|
|
285
|
+
"auth.acl.features.tillio.manage": "Manage Tillio integration",
|
|
283
286
|
"auth.acl.features.translations.manage": "Manage entity translations",
|
|
284
287
|
"auth.acl.features.translations.manage_locales": "Configure supported locales",
|
|
285
288
|
"auth.acl.features.translations.view": "View translations",
|
|
@@ -203,6 +203,8 @@
|
|
|
203
203
|
"auth.acl.features.payment_gateways.view": "View payment gateway transactions",
|
|
204
204
|
"auth.acl.features.perspectives.role_defaults": "Manage role default perspectives",
|
|
205
205
|
"auth.acl.features.perspectives.use": "Use table perspectives",
|
|
206
|
+
"auth.acl.features.phone_calls.manage": "Gestionar llamadas telefónicas (importación, datos sin procesar)",
|
|
207
|
+
"auth.acl.features.phone_calls.view": "Ver llamadas telefónicas",
|
|
206
208
|
"auth.acl.features.planner.manage_availability": "Manage availability",
|
|
207
209
|
"auth.acl.features.planner.view": "View schedules",
|
|
208
210
|
"auth.acl.features.progress.cancel": "Cancel progress jobs",
|
|
@@ -280,6 +282,7 @@
|
|
|
280
282
|
"auth.acl.features.storage_providers.manage": "Manage storage providers",
|
|
281
283
|
"auth.acl.features.sync_excel.run": "Upload CSV files and prepare imports",
|
|
282
284
|
"auth.acl.features.sync_excel.view": "View sync_excel uploads and previews",
|
|
285
|
+
"auth.acl.features.tillio.manage": "Gestionar la integración con Tillio",
|
|
283
286
|
"auth.acl.features.translations.manage": "Manage entity translations",
|
|
284
287
|
"auth.acl.features.translations.manage_locales": "Configure supported locales",
|
|
285
288
|
"auth.acl.features.translations.view": "View translations",
|
|
@@ -203,6 +203,8 @@
|
|
|
203
203
|
"auth.acl.features.payment_gateways.view": "View payment gateway transactions",
|
|
204
204
|
"auth.acl.features.perspectives.role_defaults": "Manage role default perspectives",
|
|
205
205
|
"auth.acl.features.perspectives.use": "Use table perspectives",
|
|
206
|
+
"auth.acl.features.phone_calls.manage": "전화 통화 관리(가져오기, 원시 페이로드)",
|
|
207
|
+
"auth.acl.features.phone_calls.view": "전화 통화 보기",
|
|
206
208
|
"auth.acl.features.planner.manage_availability": "Manage availability",
|
|
207
209
|
"auth.acl.features.planner.view": "View schedules",
|
|
208
210
|
"auth.acl.features.progress.cancel": "Cancel progress jobs",
|
|
@@ -280,6 +282,7 @@
|
|
|
280
282
|
"auth.acl.features.storage_providers.manage": "Manage storage providers",
|
|
281
283
|
"auth.acl.features.sync_excel.run": "Upload CSV files and prepare imports",
|
|
282
284
|
"auth.acl.features.sync_excel.view": "View sync_excel uploads and previews",
|
|
285
|
+
"auth.acl.features.tillio.manage": "Tillio 통합 관리",
|
|
283
286
|
"auth.acl.features.translations.manage": "Manage entity translations",
|
|
284
287
|
"auth.acl.features.translations.manage_locales": "Configure supported locales",
|
|
285
288
|
"auth.acl.features.translations.view": "View translations",
|
|
@@ -203,6 +203,8 @@
|
|
|
203
203
|
"auth.acl.features.payment_gateways.view": "Wyświetl transakcje bramek płatniczych",
|
|
204
204
|
"auth.acl.features.perspectives.role_defaults": "Zarządzaj domyślnymi perspektywami ról",
|
|
205
205
|
"auth.acl.features.perspectives.use": "Używaj perspektyw tabel",
|
|
206
|
+
"auth.acl.features.phone_calls.manage": "Zarządzaj połączeniami telefonicznymi (pobieranie, surowe dane)",
|
|
207
|
+
"auth.acl.features.phone_calls.view": "Wyświetl połączenia telefoniczne",
|
|
206
208
|
"auth.acl.features.planner.manage_availability": "Zarządzaj dostępnością",
|
|
207
209
|
"auth.acl.features.planner.view": "Wyświetl harmonogramy",
|
|
208
210
|
"auth.acl.features.progress.cancel": "Anuluj zadania postępu",
|
|
@@ -280,6 +282,7 @@
|
|
|
280
282
|
"auth.acl.features.storage_providers.manage": "Zarządzaj dostawcami pamięci masowej",
|
|
281
283
|
"auth.acl.features.sync_excel.run": "Przesyłaj pliki CSV i przygotowuj importy",
|
|
282
284
|
"auth.acl.features.sync_excel.view": "Wyświetl przesłane pliki i podglądy importu Excel / CSV",
|
|
285
|
+
"auth.acl.features.tillio.manage": "Zarządzaj integracją Tillio",
|
|
283
286
|
"auth.acl.features.translations.manage": "Zarządzaj tłumaczeniami encji",
|
|
284
287
|
"auth.acl.features.translations.manage_locales": "Konfiguruj obsługiwane ustawienia regionalne",
|
|
285
288
|
"auth.acl.features.translations.view": "Wyświetl tłumaczenia",
|
|
@@ -126,6 +126,26 @@ export const upgradeActions: UpgradeActionDefinition[] = [
|
|
|
126
126
|
await registerSessionInitializationPruneSchedule(container, { tenantId, organizationId })
|
|
127
127
|
},
|
|
128
128
|
},
|
|
129
|
+
{
|
|
130
|
+
id: 'phone_calls.seed-call-encryption-maps',
|
|
131
|
+
version: '0.7.1',
|
|
132
|
+
messageKey: 'phone_calls.config.upgradeActions.encryptionMaps.message',
|
|
133
|
+
ctaKey: 'phone_calls.config.upgradeActions.encryptionMaps.cta',
|
|
134
|
+
successKey: 'phone_calls.config.upgradeActions.encryptionMaps.success',
|
|
135
|
+
loadingKey: 'phone_calls.config.upgradeActions.encryptionMaps.loading',
|
|
136
|
+
// `Migration20260822120000` backfills these maps, but only for scopes that had active maps when
|
|
137
|
+
// it ran. A tenant that upgraded with encryption disabled and turned it on afterwards is left
|
|
138
|
+
// without a map, so ingest writes call PII as plaintext with nothing to signal it. This is the
|
|
139
|
+
// heal for that case, and the same re-assert path devices exposes for its own map.
|
|
140
|
+
// Lazy-imported so configs stays decoupled from phone_calls/entities (mirrors the devices action).
|
|
141
|
+
run: async ({ em, tenantId, organizationId }) => {
|
|
142
|
+
const [{ default: phoneCallsEncryptionMaps }, { upsertEncryptionMapSpecs }] = await Promise.all([
|
|
143
|
+
import('@open-mercato/core/modules/phone_calls/encryption'),
|
|
144
|
+
import('@open-mercato/core/modules/entities/cli'),
|
|
145
|
+
])
|
|
146
|
+
await upsertEncryptionMapSpecs(em, tenantId, organizationId ?? null, phoneCallsEncryptionMaps)
|
|
147
|
+
},
|
|
148
|
+
},
|
|
129
149
|
]
|
|
130
150
|
|
|
131
151
|
export function actionsUpToVersion(version: string): UpgradeActionDefinition[] {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
TenantDataEncryptionService,
|
|
24
24
|
parseDecryptedFieldValue,
|
|
25
|
+
resolveEncryptionKeyId,
|
|
25
26
|
} from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
|
|
26
27
|
import { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'
|
|
27
28
|
import { listEntityMetadata } from '@open-mercato/shared/lib/db/entityMetadata'
|
|
@@ -549,6 +550,27 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
549
550
|
}
|
|
550
551
|
|
|
551
552
|
const oldDekCache = new Map<string, TenantDek | null>()
|
|
553
|
+
// A dry run must not provision key material. `encryptEntityPayload` creates and
|
|
554
|
+
// persists a tenant DEK in KMS/Vault the first time it runs for a tenant, so a
|
|
555
|
+
// read-only preview would silently mutate KMS state (#5950). Probe read-only
|
|
556
|
+
// once per tenant instead, and report what a real run would rewrite.
|
|
557
|
+
//
|
|
558
|
+
// The tenant id IS the key id here: this command skips every system-scoped
|
|
559
|
+
// entity above, and its service is built without `defaultEncryptionMaps`, so
|
|
560
|
+
// no map it sees can resolve to a `system:<entityId>` key.
|
|
561
|
+
const dekAvailability = new Map<string, boolean>()
|
|
562
|
+
const hasExistingDek = async (tenantId: string): Promise<boolean> => {
|
|
563
|
+
const cached = dekAvailability.get(tenantId)
|
|
564
|
+
if (cached !== undefined) return cached
|
|
565
|
+
const available = Boolean(await encryptionService.getDek(tenantId))
|
|
566
|
+
dekAvailability.set(tenantId, available)
|
|
567
|
+
if (!available) {
|
|
568
|
+
console.warn(
|
|
569
|
+
`[dry-run] Tenant ${tenantId} has no data-encryption key yet. Reporting the rows a real run would encrypt; no key material was provisioned.`,
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
return available
|
|
573
|
+
}
|
|
552
574
|
const processScope = async (
|
|
553
575
|
entityId: string,
|
|
554
576
|
meta: any,
|
|
@@ -576,6 +598,7 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
576
598
|
const rows = await conn.execute(selectSql, [scope.tenantId, scope.organizationId])
|
|
577
599
|
const list = Array.isArray(rows) ? rows : []
|
|
578
600
|
if (!list.length) return 0
|
|
601
|
+
const dekAvailable = dryRun ? await hasExistingDek(scope.tenantId) : true
|
|
579
602
|
let updated = 0
|
|
580
603
|
for (const row of list) {
|
|
581
604
|
const payload: Record<string, unknown> = {}
|
|
@@ -611,11 +634,26 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
611
634
|
payload[rule.field] = parseDecryptedFieldValue(decrypted)
|
|
612
635
|
}
|
|
613
636
|
}
|
|
637
|
+
if (!dekAvailable) {
|
|
638
|
+
// Nothing was encrypted because no key exists and this run refuses to
|
|
639
|
+
// create one. Count the rows a real run would rewrite by applying the
|
|
640
|
+
// same plaintext/ciphertext filters the update path uses below.
|
|
641
|
+
const wouldChange = fields.some((rule) => {
|
|
642
|
+
const col = resolveProperty(meta, rule.field)?.columnName
|
|
643
|
+
if (!col) return false
|
|
644
|
+
const value = row[col]
|
|
645
|
+
if (value === null || value === undefined) return false
|
|
646
|
+
return rotate ? isEncryptedPayload(value) : !isEncryptedPayload(value)
|
|
647
|
+
})
|
|
648
|
+
if (wouldChange) updated += 1
|
|
649
|
+
continue
|
|
650
|
+
}
|
|
614
651
|
const encrypted = await encryptionService.encryptEntityPayload(
|
|
615
652
|
entityId,
|
|
616
653
|
payload,
|
|
617
654
|
scope.tenantId,
|
|
618
655
|
scope.organizationId,
|
|
656
|
+
{ createMissingDek: !dryRun },
|
|
619
657
|
)
|
|
620
658
|
const updates: Record<string, unknown> = {}
|
|
621
659
|
for (const rule of fields) {
|
|
@@ -1129,6 +1167,17 @@ const backfillSystemEncryption: ModuleCli = {
|
|
|
1129
1167
|
const columnList = Array.from(columns)
|
|
1130
1168
|
const selectList = columnList.map((column) => `"${column}"`).join(', ')
|
|
1131
1169
|
|
|
1170
|
+
// Same dry-run guarantee as rotate-encryption-key: previewing must not make
|
|
1171
|
+
// the KMS provision this entity's system DEK as a side effect (#5950).
|
|
1172
|
+
const systemDekAvailable = dryRun
|
|
1173
|
+
? Boolean(await encryptionService.getDek(resolveEncryptionKeyId(entityId, 'system', null)))
|
|
1174
|
+
: true
|
|
1175
|
+
if (!systemDekAvailable) {
|
|
1176
|
+
console.warn(
|
|
1177
|
+
`[dry-run] ${entityId} has no system data-encryption key yet. Reporting the rows a real run would encrypt; no key material was provisioned.`,
|
|
1178
|
+
)
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1132
1181
|
let cursor: unknown = null
|
|
1133
1182
|
let entityRowsScanned = 0
|
|
1134
1183
|
let entityRowsUpdated = 0
|
|
@@ -1160,8 +1209,19 @@ const backfillSystemEncryption: ModuleCli = {
|
|
|
1160
1209
|
}
|
|
1161
1210
|
}
|
|
1162
1211
|
if (!hasPlaintext) continue
|
|
1212
|
+
if (!systemDekAvailable) {
|
|
1213
|
+
// `hasPlaintext` already means a real run would rewrite this row.
|
|
1214
|
+
entityRowsUpdated += 1
|
|
1215
|
+
continue
|
|
1216
|
+
}
|
|
1163
1217
|
|
|
1164
|
-
const encrypted = await encryptionService.encryptEntityPayload(
|
|
1218
|
+
const encrypted = await encryptionService.encryptEntityPayload(
|
|
1219
|
+
entityId,
|
|
1220
|
+
payload,
|
|
1221
|
+
null,
|
|
1222
|
+
null,
|
|
1223
|
+
{ createMissingDek: !dryRun },
|
|
1224
|
+
)
|
|
1165
1225
|
const updates: Record<string, unknown> = {}
|
|
1166
1226
|
for (const rule of map.fields) {
|
|
1167
1227
|
const resolved = resolveProperty(meta, rule.field)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
// Backfill the phone_calls encryption maps for every pre-existing (tenant, org) scope that already
|
|
4
|
+
// has active encryption maps. Encryption maps are seeded only at tenant creation (`entities
|
|
5
|
+
// seed-encryption`), so a tenant that predates the phone_calls module has no map for either of its
|
|
6
|
+
// entities, `encryptEntityPayload` no-ops, and caller/destination numbers plus the untouched Tillio
|
|
7
|
+
// payload are written as PLAINTEXT, both in the base tables and in the query index doc. Restricting
|
|
8
|
+
// the insert to scopes that ALREADY have maps mirrors what seed-encryption does and correctly skips
|
|
9
|
+
// tenants with encryption disabled (which have no maps at all). Idempotent via the NOT EXISTS guard,
|
|
10
|
+
// so re-runs are a no-op and it stays consistent with the runtime `upsertEncryptionMapSpecs` helper.
|
|
11
|
+
// Same shape as the `devices:user_device` backfill in Migration20260722120000.
|
|
12
|
+
export class Migration20260822120000 extends Migration {
|
|
13
|
+
|
|
14
|
+
override async up(): Promise<void> {
|
|
15
|
+
this.addSql(`
|
|
16
|
+
insert into "encryption_maps" ("id", "entity_id", "tenant_id", "organization_id", "fields_json", "is_active", "created_at", "updated_at")
|
|
17
|
+
select gen_random_uuid(), 'phone_calls:phone_call', src."tenant_id", src."organization_id", '[{"field":"raw_snapshot"},{"field":"provider_facts"},{"field":"recording_url"}]'::jsonb, true, now(), now()
|
|
18
|
+
from (
|
|
19
|
+
select distinct "tenant_id", "organization_id"
|
|
20
|
+
from "encryption_maps"
|
|
21
|
+
where "is_active" = true and "deleted_at" is null
|
|
22
|
+
) src
|
|
23
|
+
where not exists (
|
|
24
|
+
select 1 from "encryption_maps" existing
|
|
25
|
+
where existing."entity_id" = 'phone_calls:phone_call'
|
|
26
|
+
and existing."tenant_id" is not distinct from src."tenant_id"
|
|
27
|
+
and existing."organization_id" is not distinct from src."organization_id"
|
|
28
|
+
and existing."deleted_at" is null
|
|
29
|
+
);
|
|
30
|
+
`);
|
|
31
|
+
|
|
32
|
+
this.addSql(`
|
|
33
|
+
insert into "encryption_maps" ("id", "entity_id", "tenant_id", "organization_id", "fields_json", "is_active", "created_at", "updated_at")
|
|
34
|
+
select gen_random_uuid(), 'phone_calls:phone_call_participant', src."tenant_id", src."organization_id", '[{"field":"phone_number"},{"field":"display_name"},{"field":"email"}]'::jsonb, true, now(), now()
|
|
35
|
+
from (
|
|
36
|
+
select distinct "tenant_id", "organization_id"
|
|
37
|
+
from "encryption_maps"
|
|
38
|
+
where "is_active" = true and "deleted_at" is null
|
|
39
|
+
) src
|
|
40
|
+
where not exists (
|
|
41
|
+
select 1 from "encryption_maps" existing
|
|
42
|
+
where existing."entity_id" = 'phone_calls:phone_call_participant'
|
|
43
|
+
and existing."tenant_id" is not distinct from src."tenant_id"
|
|
44
|
+
and existing."organization_id" is not distinct from src."organization_id"
|
|
45
|
+
and existing."deleted_at" is null
|
|
46
|
+
);
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override async down(): Promise<void> {
|
|
51
|
+
// Reverts the feature's encryption maps. phone_calls maps have no source other than this module,
|
|
52
|
+
// so deleting them all returns the schema to its pre-feature state.
|
|
53
|
+
this.addSql(`delete from "encryption_maps" where "entity_id" in ('phone_calls:phone_call', 'phone_calls:phone_call_participant');`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const features = [
|
|
2
|
+
{ id: 'phone_calls.view', title: 'View phone calls', module: 'phone_calls' },
|
|
3
|
+
{
|
|
4
|
+
id: 'phone_calls.manage',
|
|
5
|
+
title: 'Manage phone calls (pull, raw payload)',
|
|
6
|
+
module: 'phone_calls',
|
|
7
|
+
dependsOn: ['phone_calls.view'],
|
|
8
|
+
},
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export default features
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
3
|
+
import type { Where } from '@open-mercato/shared/lib/query/types'
|
|
4
|
+
import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
|
|
5
|
+
import { E } from '#generated/entities.ids.generated'
|
|
6
|
+
import { PhoneCall } from '../../data/entities'
|
|
7
|
+
import { createPhoneCallsCrudOpenApi, createPagedListResponseSchema } from '../openapi'
|
|
8
|
+
|
|
9
|
+
const filterDateSchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.trim()
|
|
12
|
+
.min(1)
|
|
13
|
+
.refine((value) => !Number.isNaN(new Date(value).getTime()), { message: 'Expected a parsable date' })
|
|
14
|
+
|
|
15
|
+
const listSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
page: z.coerce.number().min(1).default(1),
|
|
18
|
+
pageSize: z.coerce.number().min(1).max(100).default(50),
|
|
19
|
+
q: z.string().optional(),
|
|
20
|
+
providerKey: z.string().optional(),
|
|
21
|
+
status: z.string().optional(),
|
|
22
|
+
direction: z.string().optional(),
|
|
23
|
+
// Validated rather than parsed later: an unparsable value used to be dropped, so a typo in a
|
|
24
|
+
// filter silently returned the unfiltered page.
|
|
25
|
+
startedFrom: filterDateSchema.optional(),
|
|
26
|
+
startedTo: filterDateSchema.optional(),
|
|
27
|
+
sortField: z.string().optional(),
|
|
28
|
+
sortDir: z.enum(['asc', 'desc']).optional(),
|
|
29
|
+
id: z.string().uuid().optional(),
|
|
30
|
+
})
|
|
31
|
+
.passthrough()
|
|
32
|
+
|
|
33
|
+
const dayOnlySchema = z.iso.date()
|
|
34
|
+
|
|
35
|
+
// A day is a range, not an instant. `new Date('2026-08-20')` is midnight UTC, so using it as
|
|
36
|
+
// the upper bound drops every call on the day the user picked. The filter bar sends bare
|
|
37
|
+
// `yyyy-MM-dd` for both ends, so each is widened to the day it names; a caller that sends a
|
|
38
|
+
// full timestamp keeps the instant it asked for.
|
|
39
|
+
function rangeBoundary(value: string, edge: 'start' | 'end'): Date {
|
|
40
|
+
if (!dayOnlySchema.safeParse(value).success) return new Date(value)
|
|
41
|
+
return new Date(`${value}T${edge === 'start' ? '00:00:00.000' : '23:59:59.999'}Z`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const routeMetadata = {
|
|
45
|
+
GET: { requireAuth: true, requireFeatures: ['phone_calls.view'] },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const metadata = routeMetadata
|
|
49
|
+
|
|
50
|
+
// No `recording_url`: provider recording links carry their own access token, and this route
|
|
51
|
+
// is gated on `phone_calls.view`. The key still ships as null so the response shape holds.
|
|
52
|
+
const listFields = [
|
|
53
|
+
'id',
|
|
54
|
+
'organization_id',
|
|
55
|
+
'tenant_id',
|
|
56
|
+
'provider_key',
|
|
57
|
+
'integration_id',
|
|
58
|
+
'external_call_id',
|
|
59
|
+
'external_conversation_id',
|
|
60
|
+
'direction',
|
|
61
|
+
'status',
|
|
62
|
+
'started_at',
|
|
63
|
+
'answered_at',
|
|
64
|
+
'ended_at',
|
|
65
|
+
'duration_seconds',
|
|
66
|
+
'ingest_status',
|
|
67
|
+
'last_ingested_at',
|
|
68
|
+
'created_at',
|
|
69
|
+
'updated_at',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
const crud = makeCrudRoute({
|
|
73
|
+
metadata: routeMetadata,
|
|
74
|
+
orm: {
|
|
75
|
+
entity: PhoneCall,
|
|
76
|
+
idField: 'id',
|
|
77
|
+
orgField: 'organizationId',
|
|
78
|
+
tenantField: 'tenantId',
|
|
79
|
+
softDeleteField: 'deletedAt',
|
|
80
|
+
},
|
|
81
|
+
indexer: { entityType: E.phone_calls.phone_call },
|
|
82
|
+
list: {
|
|
83
|
+
schema: listSchema,
|
|
84
|
+
entityId: E.phone_calls.phone_call,
|
|
85
|
+
fields: listFields,
|
|
86
|
+
transformItem: (item: Record<string, unknown>) => ({ ...item, recording_url: null }),
|
|
87
|
+
sortFieldMap: {
|
|
88
|
+
externalCallId: 'external_call_id',
|
|
89
|
+
startedAt: 'started_at',
|
|
90
|
+
endedAt: 'ended_at',
|
|
91
|
+
durationSeconds: 'duration_seconds',
|
|
92
|
+
status: 'status',
|
|
93
|
+
direction: 'direction',
|
|
94
|
+
providerKey: 'provider_key',
|
|
95
|
+
ingestStatus: 'ingest_status',
|
|
96
|
+
createdAt: 'created_at',
|
|
97
|
+
updatedAt: 'updated_at',
|
|
98
|
+
},
|
|
99
|
+
buildFilters: (query): Where<Record<string, unknown>> => {
|
|
100
|
+
const filters: Record<string, unknown> = {}
|
|
101
|
+
if (query.id) filters.id = { $eq: query.id }
|
|
102
|
+
if (query.providerKey) filters.provider_key = { $eq: query.providerKey }
|
|
103
|
+
if (query.status) filters.status = { $eq: query.status }
|
|
104
|
+
if (query.direction) filters.direction = { $eq: query.direction }
|
|
105
|
+
if (query.q) {
|
|
106
|
+
const term = buildIlikeTerm(query.q)
|
|
107
|
+
filters.$or = [
|
|
108
|
+
{ external_call_id: { $ilike: term } },
|
|
109
|
+
{ external_conversation_id: { $ilike: term } },
|
|
110
|
+
{ provider_key: { $ilike: term } },
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
const startedRange: Record<string, Date> = {}
|
|
114
|
+
if (query.startedFrom) startedRange.$gte = rangeBoundary(query.startedFrom, 'start')
|
|
115
|
+
if (query.startedTo) startedRange.$lte = rangeBoundary(query.startedTo, 'end')
|
|
116
|
+
if (Object.keys(startedRange).length) filters.started_at = startedRange
|
|
117
|
+
return filters
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
export const GET = crud.GET
|
|
123
|
+
|
|
124
|
+
const phoneCallListItemSchema = z.object({
|
|
125
|
+
id: z.string().uuid(),
|
|
126
|
+
organization_id: z.string().uuid().nullable().optional(),
|
|
127
|
+
tenant_id: z.string().uuid().nullable().optional(),
|
|
128
|
+
provider_key: z.string().nullable().optional(),
|
|
129
|
+
integration_id: z.string().nullable().optional(),
|
|
130
|
+
external_call_id: z.string().nullable().optional(),
|
|
131
|
+
external_conversation_id: z.string().nullable().optional(),
|
|
132
|
+
direction: z.string().nullable().optional(),
|
|
133
|
+
status: z.string().nullable().optional(),
|
|
134
|
+
started_at: z.string().nullable().optional(),
|
|
135
|
+
answered_at: z.string().nullable().optional(),
|
|
136
|
+
ended_at: z.string().nullable().optional(),
|
|
137
|
+
duration_seconds: z.number().nullable().optional(),
|
|
138
|
+
recording_url: z.string().nullable().optional(),
|
|
139
|
+
ingest_status: z.string().nullable().optional(),
|
|
140
|
+
last_ingested_at: z.string().nullable().optional(),
|
|
141
|
+
created_at: z.string().nullable().optional(),
|
|
142
|
+
updated_at: z.string().nullable().optional(),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
export const openApi = createPhoneCallsCrudOpenApi({
|
|
146
|
+
resourceName: 'PhoneCall',
|
|
147
|
+
pluralName: 'PhoneCalls',
|
|
148
|
+
querySchema: listSchema,
|
|
149
|
+
listResponseSchema: createPagedListResponseSchema(phoneCallListItemSchema),
|
|
150
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ZodTypeAny } from 'zod'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import {
|
|
4
|
+
createCrudOpenApiFactory,
|
|
5
|
+
createPagedListResponseSchema as createSharedPagedListResponseSchema,
|
|
6
|
+
defaultOkResponseSchema as sharedDefaultOkResponseSchema,
|
|
7
|
+
type CrudOpenApiOptions,
|
|
8
|
+
} from '@open-mercato/shared/lib/openapi/crud'
|
|
9
|
+
|
|
10
|
+
export const defaultOkResponseSchema = sharedDefaultOkResponseSchema
|
|
11
|
+
|
|
12
|
+
export function createPagedListResponseSchema(itemSchema: ZodTypeAny) {
|
|
13
|
+
return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const buildPhoneCallsCrudOpenApi = createCrudOpenApiFactory({
|
|
17
|
+
defaultTag: 'PhoneCalls',
|
|
18
|
+
defaultOkResponseSchema,
|
|
19
|
+
makeListDescription: ({ pluralLower }) =>
|
|
20
|
+
`Returns a paginated collection of ${pluralLower} scoped to the authenticated organization.`,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export function createPhoneCallsCrudOpenApi(options: CrudOpenApiOptions): OpenApiRouteDoc {
|
|
24
|
+
return buildPhoneCallsCrudOpenApi(options)
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const metadata = {
|
|
2
|
+
requireAuth: true,
|
|
3
|
+
requireFeatures: ['phone_calls.view'],
|
|
4
|
+
pageTitle: 'Phone Calls',
|
|
5
|
+
pageTitleKey: 'phone_calls.nav.calls',
|
|
6
|
+
pageGroup: 'Communication',
|
|
7
|
+
pageGroupKey: 'phone_calls.nav.group',
|
|
8
|
+
pagePriority: 10,
|
|
9
|
+
pageOrder: 100,
|
|
10
|
+
icon: 'phone-call',
|
|
11
|
+
breadcrumb: [{ label: 'Phone Calls', labelKey: 'phone_calls.nav.calls' }],
|
|
12
|
+
}
|