@open-mercato/core 0.4.2-canary-49d47ff90e → 0.4.2-canary-02d8ce2991

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 (51) hide show
  1. package/dist/modules/auth/backend/auth/profile/page.js.map +1 -1
  2. package/dist/modules/auth/backend/roles/[id]/edit/page.js +4 -1
  3. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  4. package/dist/modules/auth/backend/users/[id]/edit/page.js +4 -1
  5. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  6. package/dist/modules/auth/cli.js +13 -12
  7. package/dist/modules/auth/cli.js.map +2 -2
  8. package/dist/modules/business_rules/api/execute/route.js +7 -1
  9. package/dist/modules/business_rules/api/execute/route.js.map +2 -2
  10. package/dist/modules/business_rules/lib/rule-engine.js +33 -3
  11. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  12. package/dist/modules/dashboards/cli.js +12 -4
  13. package/dist/modules/dashboards/cli.js.map +2 -2
  14. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +16 -11
  15. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +3 -3
  16. package/dist/modules/dashboards/services/widgetDataService.js +46 -2
  17. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  18. package/dist/modules/notifications/data/validators.js +5 -1
  19. package/dist/modules/notifications/data/validators.js.map +2 -2
  20. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +2 -1
  21. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  22. package/dist/modules/notifications/lib/deliveryConfig.js +4 -2
  23. package/dist/modules/notifications/lib/deliveryConfig.js.map +2 -2
  24. package/dist/modules/notifications/lib/deliveryStrategies.js +14 -0
  25. package/dist/modules/notifications/lib/deliveryStrategies.js.map +7 -0
  26. package/dist/modules/notifications/subscribers/deliver-notification.js +33 -7
  27. package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
  28. package/dist/modules/workflows/lib/transition-handler.js +14 -6
  29. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  30. package/package.json +2 -2
  31. package/src/modules/auth/README.md +1 -1
  32. package/src/modules/auth/__tests__/cli-setup-acl.test.ts +1 -1
  33. package/src/modules/auth/backend/auth/profile/page.tsx +2 -2
  34. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +4 -1
  35. package/src/modules/auth/backend/users/[id]/edit/page.tsx +4 -1
  36. package/src/modules/auth/cli.ts +25 -12
  37. package/src/modules/business_rules/api/execute/route.ts +8 -1
  38. package/src/modules/business_rules/lib/__tests__/rule-engine.test.ts +51 -0
  39. package/src/modules/business_rules/lib/rule-engine.ts +57 -3
  40. package/src/modules/dashboards/cli.ts +14 -4
  41. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +22 -11
  42. package/src/modules/dashboards/services/widgetDataService.ts +52 -2
  43. package/src/modules/notifications/__tests__/deliver-notification.test.ts +195 -0
  44. package/src/modules/notifications/__tests__/deliveryStrategies.test.ts +19 -0
  45. package/src/modules/notifications/__tests__/notificationService.test.ts +208 -0
  46. package/src/modules/notifications/data/validators.ts +5 -0
  47. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +2 -0
  48. package/src/modules/notifications/lib/deliveryConfig.ts +8 -0
  49. package/src/modules/notifications/lib/deliveryStrategies.ts +50 -0
  50. package/src/modules/notifications/subscribers/deliver-notification.ts +39 -10
  51. package/src/modules/workflows/lib/transition-handler.ts +18 -6
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/dashboards/services/widgetDataService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n} from '../lib/aggregations'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nfunction assertSafeIdentifier(value: string, name: string): void {\n if (!SAFE_IDENTIFIER_PATTERN.test(value)) {\n throw new Error(`Invalid ${name}: ${value}`)\n }\n}\n\nexport type WidgetDataRequest = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n resolveLabels?: boolean\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n dateRange?: {\n field: string\n preset: DateRangePreset\n }\n comparison?: {\n type: 'previous_period' | 'previous_year'\n }\n}\n\nexport type WidgetDataItem = {\n groupKey: unknown\n groupLabel?: string\n value: number | null\n}\n\nexport type WidgetDataResponse = {\n value: number | null\n data: WidgetDataItem[]\n comparison?: {\n value: number | null\n change: number\n direction: 'up' | 'down' | 'unchanged'\n }\n metadata: {\n fetchedAt: string\n recordCount: number\n }\n}\n\nexport type WidgetDataScope = {\n tenantId: string\n organizationIds?: string[]\n}\n\nexport type WidgetDataServiceOptions = {\n em: EntityManager\n scope: WidgetDataScope\n registry: AnalyticsRegistry\n cache?: CacheStrategy\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n\n constructor(options: WidgetDataServiceOptions) {\n this.em = options.em\n this.scope = options.scope\n this.registry = options.registry\n this.cache = options.cache\n }\n\n private buildCacheKey(request: WidgetDataRequest): string {\n const hash = createHash('sha256')\n hash.update(JSON.stringify({ request, scope: this.scope }))\n return `widget-data:${hash.digest('hex').slice(0, 16)}`\n }\n\n private getCacheTags(entityType: string): string[] {\n return ['widget-data', `widget-data:${entityType}`]\n }\n\n async fetchWidgetData(request: WidgetDataRequest): Promise<WidgetDataResponse> {\n this.validateRequest(request)\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n try {\n const cached = await this.cache.get(cacheKey)\n if (cached && typeof cached === 'object' && 'value' in (cached as object)) {\n return cached as WidgetDataResponse\n }\n } catch {\n }\n }\n\n const now = new Date()\n let dateRangeResolved: { start: Date; end: Date } | undefined\n let comparisonRange: { start: Date; end: Date } | undefined\n\n if (request.dateRange) {\n dateRangeResolved = resolveDateRange(request.dateRange.preset, now)\n if (request.comparison) {\n comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset)\n }\n }\n\n const mainResult = await this.executeQuery(request, dateRangeResolved)\n\n let comparisonResult: { value: number | null; data: WidgetDataItem[] } | undefined\n if (comparisonRange && request.dateRange) {\n comparisonResult = await this.executeQuery(request, comparisonRange)\n }\n\n const response: WidgetDataResponse = {\n value: mainResult.value,\n data: mainResult.data,\n metadata: {\n fetchedAt: now.toISOString(),\n recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0),\n },\n }\n\n if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {\n response.comparison = {\n value: comparisonResult.value,\n change: calculatePercentageChange(mainResult.value, comparisonResult.value),\n direction: determineChangeDirection(mainResult.value, comparisonResult.value),\n }\n }\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n const tags = this.getCacheTags(request.entityType)\n try {\n await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags })\n } catch {\n }\n }\n\n return response\n }\n\n private validateRequest(request: WidgetDataRequest): void {\n if (!this.registry.isValidEntityType(request.entityType)) {\n throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`)\n }\n\n if (!request.metric?.field || !request.metric?.aggregate) {\n throw new WidgetDataValidationError('Metric field and aggregate are required')\n }\n\n const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field)\n if (!metricMapping) {\n throw new WidgetDataValidationError(\n `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`\n )\n }\n\n const validAggregates: AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\n if (!validAggregates.includes(request.metric.aggregate)) {\n throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`)\n }\n\n if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {\n throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`)\n }\n\n if (request.groupBy) {\n const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field)\n if (!groupMapping) {\n const [baseField] = request.groupBy.field.split('.')\n const baseMapping = this.registry.getFieldMapping(request.entityType, baseField)\n if (!baseMapping || baseMapping.type !== 'jsonb') {\n throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`)\n }\n }\n }\n }\n\n private async executeQuery(\n request: WidgetDataRequest,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildAggregationQuery({\n entityType: request.entityType,\n metric: request.metric,\n groupBy: request.groupBy,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (request.groupBy) {\n let data: WidgetDataItem[] = results.map((row: Record<string, unknown>) => ({\n groupKey: row.group_key,\n value: row.value !== null ? Number(row.value) : null,\n }))\n\n if (request.groupBy.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n const singleValue = results[0]?.value !== undefined ? Number(results[0].value) : null\n return { value: singleValue, data: [] }\n }\n\n private async resolveGroupLabels(\n data: WidgetDataItem[],\n entityType: string,\n groupByField: string,\n ): Promise<WidgetDataItem[]> {\n const config = this.registry.getLabelResolverConfig(entityType, groupByField)\n\n if (!config) {\n return data.map((item) => ({\n ...item,\n groupLabel: item.groupKey != null && item.groupKey !== '' ? String(item.groupKey) : undefined,\n }))\n }\n\n const ids = data\n .map((item) => item.groupKey)\n .filter((id): id is string => {\n if (typeof id !== 'string' || id.length === 0) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)\n })\n\n if (ids.length === 0) {\n return data.map((item) => ({ ...item, groupLabel: undefined }))\n }\n\n const uniqueIds = [...new Set(ids)]\n\n assertSafeIdentifier(config.table, 'table name')\n assertSafeIdentifier(config.idColumn, 'id column')\n assertSafeIdentifier(config.labelColumn, 'label column')\n\n const clauses = [`\"${config.idColumn}\" = ANY(?::uuid[])`, 'tenant_id = ?']\n const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]\n\n if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n clauses.push('organization_id = ANY(?::uuid[])')\n params.push(`{${this.scope.organizationIds.join(',')}}`)\n }\n\n const sql = `SELECT \"${config.idColumn}\" as id, \"${config.labelColumn}\" as label FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null }>) {\n if (row.id && row.label != null && row.label !== '') {\n labelMap.set(row.id, row.label)\n }\n }\n\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n } catch {\n return data.map((item) => ({\n ...item,\n groupLabel: undefined,\n }))\n }\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n} from '../lib/aggregations'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nfunction assertSafeIdentifier(value: string, name: string): void {\n if (!SAFE_IDENTIFIER_PATTERN.test(value)) {\n throw new Error(`Invalid ${name}: ${value}`)\n }\n}\n\nexport type WidgetDataRequest = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n resolveLabels?: boolean\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n dateRange?: {\n field: string\n preset: DateRangePreset\n }\n comparison?: {\n type: 'previous_period' | 'previous_year'\n }\n}\n\nexport type WidgetDataItem = {\n groupKey: unknown\n groupLabel?: string\n value: number | null\n}\n\nexport type WidgetDataResponse = {\n value: number | null\n data: WidgetDataItem[]\n comparison?: {\n value: number | null\n change: number\n direction: 'up' | 'down' | 'unchanged'\n }\n metadata: {\n fetchedAt: string\n recordCount: number\n }\n}\n\nexport type WidgetDataScope = {\n tenantId: string\n organizationIds?: string[]\n}\n\nexport type WidgetDataServiceOptions = {\n em: EntityManager\n scope: WidgetDataScope\n registry: AnalyticsRegistry\n cache?: CacheStrategy\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n\n constructor(options: WidgetDataServiceOptions) {\n this.em = options.em\n this.scope = options.scope\n this.registry = options.registry\n this.cache = options.cache\n }\n\n private buildCacheKey(request: WidgetDataRequest): string {\n const hash = createHash('sha256')\n hash.update(JSON.stringify({ request, scope: this.scope }))\n return `widget-data:${hash.digest('hex').slice(0, 16)}`\n }\n\n private getCacheTags(entityType: string): string[] {\n return ['widget-data', `widget-data:${entityType}`]\n }\n\n async fetchWidgetData(request: WidgetDataRequest): Promise<WidgetDataResponse> {\n this.validateRequest(request)\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n try {\n const cached = await this.cache.get(cacheKey)\n if (cached && typeof cached === 'object' && 'value' in (cached as object)) {\n return cached as WidgetDataResponse\n }\n } catch {\n }\n }\n\n const now = new Date()\n let dateRangeResolved: { start: Date; end: Date } | undefined\n let comparisonRange: { start: Date; end: Date } | undefined\n\n if (request.dateRange) {\n dateRangeResolved = resolveDateRange(request.dateRange.preset, now)\n if (request.comparison) {\n comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset)\n }\n }\n\n const mainResult = await this.executeQuery(request, dateRangeResolved)\n\n let comparisonResult: { value: number | null; data: WidgetDataItem[] } | undefined\n if (comparisonRange && request.dateRange) {\n comparisonResult = await this.executeQuery(request, comparisonRange)\n }\n\n const response: WidgetDataResponse = {\n value: mainResult.value,\n data: mainResult.data,\n metadata: {\n fetchedAt: now.toISOString(),\n recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0),\n },\n }\n\n if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {\n response.comparison = {\n value: comparisonResult.value,\n change: calculatePercentageChange(mainResult.value, comparisonResult.value),\n direction: determineChangeDirection(mainResult.value, comparisonResult.value),\n }\n }\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n const tags = this.getCacheTags(request.entityType)\n try {\n await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags })\n } catch {\n }\n }\n\n return response\n }\n\n private validateRequest(request: WidgetDataRequest): void {\n if (!this.registry.isValidEntityType(request.entityType)) {\n throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`)\n }\n\n if (!request.metric?.field || !request.metric?.aggregate) {\n throw new WidgetDataValidationError('Metric field and aggregate are required')\n }\n\n const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field)\n if (!metricMapping) {\n throw new WidgetDataValidationError(\n `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`\n )\n }\n\n const validAggregates: AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\n if (!validAggregates.includes(request.metric.aggregate)) {\n throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`)\n }\n\n if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {\n throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`)\n }\n\n if (request.groupBy) {\n const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field)\n if (!groupMapping) {\n const [baseField] = request.groupBy.field.split('.')\n const baseMapping = this.registry.getFieldMapping(request.entityType, baseField)\n if (!baseMapping || baseMapping.type !== 'jsonb') {\n throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`)\n }\n }\n }\n }\n\n private async executeQuery(\n request: WidgetDataRequest,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildAggregationQuery({\n entityType: request.entityType,\n metric: request.metric,\n groupBy: request.groupBy,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (request.groupBy) {\n let data: WidgetDataItem[] = results.map((row: Record<string, unknown>) => ({\n groupKey: row.group_key,\n value: row.value !== null ? Number(row.value) : null,\n }))\n\n if (request.groupBy.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n const singleValue = results[0]?.value !== undefined ? Number(results[0].value) : null\n return { value: singleValue, data: [] }\n }\n\n private async resolveGroupLabels(\n data: WidgetDataItem[],\n entityType: string,\n groupByField: string,\n ): Promise<WidgetDataItem[]> {\n const config = this.registry.getLabelResolverConfig(entityType, groupByField)\n\n if (!config) {\n return data.map((item) => ({\n ...item,\n groupLabel: item.groupKey != null && item.groupKey !== '' ? String(item.groupKey) : undefined,\n }))\n }\n\n const ids = data\n .map((item) => item.groupKey)\n .filter((id): id is string => {\n if (typeof id !== 'string' || id.length === 0) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)\n })\n\n if (ids.length === 0) {\n return data.map((item) => ({ ...item, groupLabel: undefined }))\n }\n\n const uniqueIds = [...new Set(ids)]\n\n assertSafeIdentifier(config.table, 'table name')\n assertSafeIdentifier(config.idColumn, 'id column')\n assertSafeIdentifier(config.labelColumn, 'label column')\n\n const clauses = [`\"${config.idColumn}\" = ANY(?::uuid[])`, 'tenant_id = ?']\n const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]\n\n if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n clauses.push('organization_id = ANY(?::uuid[])')\n params.push(`{${this.scope.organizationIds.join(',')}}`)\n }\n\n const sql = `SELECT \"${config.idColumn}\" as id, \"${config.labelColumn}\" as label FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n const meta = this.resolveEntityMetadata(config.table)\n const entityId = this.resolveEntityId(meta)\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const organizationId = this.resolveOrganizationId()\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null }>) {\n let labelValue = row.label\n if (entityId && encryptionService?.isEnabled() && labelValue != null) {\n const decrypted = await encryptionService.decryptEntityPayload(\n entityId,\n { [config.labelColumn]: labelValue },\n this.scope.tenantId,\n organizationId,\n )\n const resolved = decrypted[config.labelColumn]\n if (typeof resolved === 'string' || typeof resolved === 'number') {\n labelValue = String(resolved)\n }\n }\n\n if (row.id && labelValue != null && labelValue !== '') {\n labelMap.set(row.id, labelValue)\n }\n }\n\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n } catch {\n return data.map((item) => ({\n ...item,\n groupLabel: undefined,\n }))\n }\n }\n\n private resolveOrganizationId(): string | null {\n if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null\n return this.scope.organizationIds[0] ?? null\n }\n\n private resolveEntityMetadata(tableName: string): Record<string, any> | null {\n const registry = (this.em as any)?.getMetadata?.()\n if (!registry) return null\n const entries =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}))\n const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {})\n const match = metas.find((meta: any) => {\n const table = meta?.tableName ?? meta?.collection\n if (typeof table !== 'string') return false\n if (table === tableName) return true\n return table.split('.').pop() === tableName\n })\n return match ?? null\n }\n\n private resolveEntityId(meta: Record<string, any> | null): string | null {\n if (!meta) return null\n try {\n return resolveEntityIdFromMetadata(meta as any)\n } catch {\n return null\n }\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -69,12 +69,16 @@ const notificationDeliveryEmailSchema = notificationDeliveryStrategySchema.exten
69
69
  replyTo: z.string().trim().min(1).optional(),
70
70
  subjectPrefix: z.string().trim().min(1).optional()
71
71
  });
72
+ const notificationDeliveryCustomSchema = notificationDeliveryStrategySchema.extend({
73
+ config: z.unknown().optional()
74
+ });
72
75
  const notificationDeliveryConfigSchema = z.object({
73
76
  appUrl: z.string().url().optional(),
74
77
  panelPath: safeRelativeHrefSchema.optional(),
75
78
  strategies: z.object({
76
79
  database: notificationDeliveryStrategySchema.optional(),
77
- email: notificationDeliveryEmailSchema.optional()
80
+ email: notificationDeliveryEmailSchema.optional(),
81
+ custom: z.record(z.string(), notificationDeliveryCustomSchema).optional()
78
82
  }).optional()
79
83
  });
80
84
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/notifications/data/validators.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { isSafeNotificationHref } from '../lib/safeHref'\n\nexport const notificationStatusSchema = z.enum(['unread', 'read', 'actioned', 'dismissed'])\nexport const notificationSeveritySchema = z.enum(['info', 'warning', 'success', 'error'])\n\nexport const safeRelativeHrefSchema = z.string().min(1).refine(\n (href) => isSafeNotificationHref(href),\n { message: 'Href must be a same-origin relative path starting with /' }\n)\n\nexport const notificationActionSchema = z.object({\n id: z.string().min(1),\n label: z.string().min(1),\n labelKey: z.string().optional(),\n variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost']).optional(),\n icon: z.string().optional(),\n commandId: z.string().optional(),\n href: safeRelativeHrefSchema.optional(),\n confirmRequired: z.boolean().optional(),\n confirmMessage: z.string().optional(),\n})\n\nconst baseNotificationFieldsSchema = z.object({\n type: z.string().min(1).max(100),\n titleKey: z.string().min(1).max(200).optional(),\n bodyKey: z.string().min(1).max(200).optional(),\n titleVariables: z.record(z.string(), z.string()).optional(),\n bodyVariables: z.record(z.string(), z.string()).optional(),\n title: z.string().min(1).max(500).optional(),\n body: z.string().max(2000).optional(),\n icon: z.string().max(100).optional(),\n severity: notificationSeveritySchema.optional().default('info'),\n actions: z.array(notificationActionSchema).optional(),\n primaryActionId: z.string().optional(),\n sourceModule: z.string().optional(),\n sourceEntityType: z.string().optional(),\n sourceEntityId: z.string().uuid().optional(),\n linkHref: safeRelativeHrefSchema.optional(),\n groupKey: z.string().optional(),\n expiresAt: z.string().datetime().optional(),\n})\n\nconst titleRequiredRefinement = {\n refine: (data: { titleKey?: string; title?: string }) => data.titleKey || data.title,\n message: 'Either titleKey or title must be provided',\n} as const\n\nexport const createNotificationSchema = baseNotificationFieldsSchema\n .extend({ recipientUserId: z.string().uuid() })\n .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })\n\nexport const createBatchNotificationSchema = baseNotificationFieldsSchema\n .extend({ recipientUserIds: z.array(z.string().uuid()).min(1).max(1000) })\n .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })\n\nexport const createRoleNotificationSchema = baseNotificationFieldsSchema\n .extend({ roleId: z.string().uuid() })\n .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })\n\nexport const createFeatureNotificationSchema = baseNotificationFieldsSchema\n .extend({ requiredFeature: z.string().min(1).max(100) })\n .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })\n\nexport const listNotificationsSchema = z.object({\n status: z.union([notificationStatusSchema, z.array(notificationStatusSchema)]).optional(),\n type: z.string().optional(),\n severity: notificationSeveritySchema.optional(),\n sourceEntityType: z.string().optional(),\n sourceEntityId: z.string().uuid().optional(),\n since: z.string().datetime().optional(),\n page: z.coerce.number().int().min(1).optional().default(1),\n pageSize: z.coerce.number().int().min(1).max(100).optional().default(20),\n})\n\nexport const executeActionSchema = z.object({\n actionId: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const restoreNotificationSchema = z.object({\n status: z.enum(['read', 'unread']).optional(),\n})\n\nconst notificationDeliveryStrategySchema = z.object({\n enabled: z.boolean().optional(),\n})\n\nconst notificationDeliveryEmailSchema = notificationDeliveryStrategySchema.extend({\n from: z.string().trim().min(1).optional(),\n replyTo: z.string().trim().min(1).optional(),\n subjectPrefix: z.string().trim().min(1).optional(),\n})\n\nexport const notificationDeliveryConfigSchema = z.object({\n appUrl: z.string().url().optional(),\n panelPath: safeRelativeHrefSchema.optional(),\n strategies: z.object({\n database: notificationDeliveryStrategySchema.optional(),\n email: notificationDeliveryEmailSchema.optional(),\n }).optional(),\n})\n\nexport type CreateNotificationInput = z.infer<typeof createNotificationSchema>\nexport type CreateBatchNotificationInput = z.infer<typeof createBatchNotificationSchema>\nexport type CreateRoleNotificationInput = z.infer<typeof createRoleNotificationSchema>\nexport type CreateFeatureNotificationInput = z.infer<typeof createFeatureNotificationSchema>\nexport type ListNotificationsInput = z.infer<typeof listNotificationsSchema>\nexport type ExecuteActionInput = z.infer<typeof executeActionSchema>\nexport type NotificationDeliveryConfigInput = z.infer<typeof notificationDeliveryConfigSchema>\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { isSafeNotificationHref } from '../lib/safeHref'\n\nexport const notificationStatusSchema = z.enum(['unread', 'read', 'actioned', 'dismissed'])\nexport const notificationSeveritySchema = z.enum(['info', 'warning', 'success', 'error'])\n\nexport const safeRelativeHrefSchema = z.string().min(1).refine(\n (href) => isSafeNotificationHref(href),\n { message: 'Href must be a same-origin relative path starting with /' }\n)\n\nexport const notificationActionSchema = z.object({\n id: z.string().min(1),\n label: z.string().min(1),\n labelKey: z.string().optional(),\n variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost']).optional(),\n icon: z.string().optional(),\n commandId: z.string().optional(),\n href: safeRelativeHrefSchema.optional(),\n confirmRequired: z.boolean().optional(),\n confirmMessage: z.string().optional(),\n})\n\nconst baseNotificationFieldsSchema = z.object({\n type: z.string().min(1).max(100),\n titleKey: z.string().min(1).max(200).optional(),\n bodyKey: z.string().min(1).max(200).optional(),\n titleVariables: z.record(z.string(), z.string()).optional(),\n bodyVariables: z.record(z.string(), z.string()).optional(),\n title: z.string().min(1).max(500).optional(),\n body: z.string().max(2000).optional(),\n icon: z.string().max(100).optional(),\n severity: notificationSeveritySchema.optional().default('info'),\n actions: z.array(notificationActionSchema).optional(),\n primaryActionId: z.string().optional(),\n sourceModule: z.string().optional(),\n sourceEntityType: z.string().optional(),\n sourceEntityId: z.string().uuid().optional(),\n linkHref: safeRelativeHrefSchema.optional(),\n groupKey: z.string().optional(),\n expiresAt: z.string().datetime().optional(),\n})\n\nconst titleRequiredRefinement = {\n refine: (data: { titleKey?: string; title?: string }) => data.titleKey || data.title,\n message: 'Either titleKey or title must be provided',\n} as const\n\nexport const createNotificationSchema = baseNotificationFieldsSchema\n .extend({ recipientUserId: z.string().uuid() })\n .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })\n\nexport const createBatchNotificationSchema = baseNotificationFieldsSchema\n .extend({ recipientUserIds: z.array(z.string().uuid()).min(1).max(1000) })\n .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })\n\nexport const createRoleNotificationSchema = baseNotificationFieldsSchema\n .extend({ roleId: z.string().uuid() })\n .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })\n\nexport const createFeatureNotificationSchema = baseNotificationFieldsSchema\n .extend({ requiredFeature: z.string().min(1).max(100) })\n .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })\n\nexport const listNotificationsSchema = z.object({\n status: z.union([notificationStatusSchema, z.array(notificationStatusSchema)]).optional(),\n type: z.string().optional(),\n severity: notificationSeveritySchema.optional(),\n sourceEntityType: z.string().optional(),\n sourceEntityId: z.string().uuid().optional(),\n since: z.string().datetime().optional(),\n page: z.coerce.number().int().min(1).optional().default(1),\n pageSize: z.coerce.number().int().min(1).max(100).optional().default(20),\n})\n\nexport const executeActionSchema = z.object({\n actionId: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const restoreNotificationSchema = z.object({\n status: z.enum(['read', 'unread']).optional(),\n})\n\nconst notificationDeliveryStrategySchema = z.object({\n enabled: z.boolean().optional(),\n})\n\nconst notificationDeliveryEmailSchema = notificationDeliveryStrategySchema.extend({\n from: z.string().trim().min(1).optional(),\n replyTo: z.string().trim().min(1).optional(),\n subjectPrefix: z.string().trim().min(1).optional(),\n})\n\nconst notificationDeliveryCustomSchema = notificationDeliveryStrategySchema.extend({\n config: z.unknown().optional(),\n})\n\nexport const notificationDeliveryConfigSchema = z.object({\n appUrl: z.string().url().optional(),\n panelPath: safeRelativeHrefSchema.optional(),\n strategies: z.object({\n database: notificationDeliveryStrategySchema.optional(),\n email: notificationDeliveryEmailSchema.optional(),\n custom: z.record(z.string(), notificationDeliveryCustomSchema).optional(),\n }).optional(),\n})\n\nexport type CreateNotificationInput = z.infer<typeof createNotificationSchema>\nexport type CreateBatchNotificationInput = z.infer<typeof createBatchNotificationSchema>\nexport type CreateRoleNotificationInput = z.infer<typeof createRoleNotificationSchema>\nexport type CreateFeatureNotificationInput = z.infer<typeof createFeatureNotificationSchema>\nexport type ListNotificationsInput = z.infer<typeof listNotificationsSchema>\nexport type ExecuteActionInput = z.infer<typeof executeActionSchema>\nexport type NotificationDeliveryConfigInput = z.infer<typeof notificationDeliveryConfigSchema>\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,8BAA8B;AAEhC,MAAM,2BAA2B,EAAE,KAAK,CAAC,UAAU,QAAQ,YAAY,WAAW,CAAC;AACnF,MAAM,6BAA6B,EAAE,KAAK,CAAC,QAAQ,WAAW,WAAW,OAAO,CAAC;AAEjF,MAAM,yBAAyB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;AAAA,EACtD,CAAC,SAAS,uBAAuB,IAAI;AAAA,EACrC,EAAE,SAAS,2DAA2D;AACxE;AAEO,MAAM,2BAA2B,EAAE,OAAO;AAAA,EAC/C,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACpB,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACvB,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,SAAS,EAAE,KAAK,CAAC,WAAW,aAAa,eAAe,WAAW,OAAO,CAAC,EAAE,SAAS;AAAA,EACtF,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,MAAM,uBAAuB,SAAS;AAAA,EACtC,iBAAiB,EAAE,QAAQ,EAAE,SAAS;AAAA,EACtC,gBAAgB,EAAE,OAAO,EAAE,SAAS;AACtC,CAAC;AAED,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC/B,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC9C,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC7C,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,EAC1D,eAAe,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,EACzD,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC3C,MAAM,EAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS;AAAA,EACpC,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACnC,UAAU,2BAA2B,SAAS,EAAE,QAAQ,MAAM;AAAA,EAC9D,SAAS,EAAE,MAAM,wBAAwB,EAAE,SAAS;AAAA,EACpD,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,EACrC,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,kBAAkB,EAAE,OAAO,EAAE,SAAS;AAAA,EACtC,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,UAAU,uBAAuB,SAAS;AAAA,EAC1C,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC5C,CAAC;AAED,MAAM,0BAA0B;AAAA,EAC9B,QAAQ,CAAC,SAAgD,KAAK,YAAY,KAAK;AAAA,EAC/E,SAAS;AACX;AAEO,MAAM,2BAA2B,6BACrC,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAC7C,OAAO,wBAAwB,QAAQ,EAAE,SAAS,wBAAwB,QAAQ,CAAC;AAE/E,MAAM,gCAAgC,6BAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI,EAAE,CAAC,EACxE,OAAO,wBAAwB,QAAQ,EAAE,SAAS,wBAAwB,QAAQ,CAAC;AAE/E,MAAM,+BAA+B,6BACzC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EACpC,OAAO,wBAAwB,QAAQ,EAAE,SAAS,wBAAwB,QAAQ,CAAC;AAE/E,MAAM,kCAAkC,6BAC5C,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,CAAC,EACtD,OAAO,wBAAwB,QAAQ,EAAE,SAAS,wBAAwB,QAAQ,CAAC;AAE/E,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,QAAQ,EAAE,MAAM,CAAC,0BAA0B,EAAE,MAAM,wBAAwB,CAAC,CAAC,EAAE,SAAS;AAAA,EACxF,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,UAAU,2BAA2B,SAAS;AAAA,EAC9C,kBAAkB,EAAE,OAAO,EAAE,SAAS;AAAA,EACtC,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACtC,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC;AAAA,EACzD,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE;AACzE,CAAC;AAEM,MAAM,sBAAsB,EAAE,OAAO;AAAA,EAC1C,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AACtD,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,QAAQ,EAAE,KAAK,CAAC,QAAQ,QAAQ,CAAC,EAAE,SAAS;AAC9C,CAAC;AAED,MAAM,qCAAqC,EAAE,OAAO;AAAA,EAClD,SAAS,EAAE,QAAQ,EAAE,SAAS;AAChC,CAAC;AAED,MAAM,kCAAkC,mCAAmC,OAAO;AAAA,EAChF,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACxC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EAC3C,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,SAAS;AACnD,CAAC;AAED,MAAM,mCAAmC,mCAAmC,OAAO;AAAA,EACjF,QAAQ,EAAE,QAAQ,EAAE,SAAS;AAC/B,CAAC;AAEM,MAAM,mCAAmC,EAAE,OAAO;AAAA,EACvD,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EAClC,WAAW,uBAAuB,SAAS;AAAA,EAC3C,YAAY,EAAE,OAAO;AAAA,IACnB,UAAU,mCAAmC,SAAS;AAAA,IACtD,OAAO,gCAAgC,SAAS;AAAA,IAChD,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,gCAAgC,EAAE,SAAS;AAAA,EAC1E,CAAC,EAAE,SAAS;AACd,CAAC;",
6
6
  "names": []
7
7
  }
@@ -14,7 +14,8 @@ const emptySettings = {
14
14
  panelPath: "/backend/notifications",
15
15
  strategies: {
16
16
  database: { enabled: true },
17
- email: { enabled: true }
17
+ email: { enabled: true },
18
+ custom: {}
18
19
  }
19
20
  };
20
21
  function NotificationSettingsPageClient() {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/notifications/frontend/NotificationSettingsPageClient.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\n\ntype NotificationDeliveryConfig = {\n appUrl?: string\n panelPath: string\n strategies: {\n database: { enabled: boolean }\n email: { enabled: boolean; from?: string; replyTo?: string; subjectPrefix?: string }\n }\n}\n\ntype SettingsResponse = {\n settings?: NotificationDeliveryConfig\n error?: string\n}\n\nconst emptySettings: NotificationDeliveryConfig = {\n panelPath: '/backend/notifications',\n strategies: {\n database: { enabled: true },\n email: { enabled: true },\n },\n}\n\nexport function NotificationSettingsPageClient() {\n const t = useT()\n const [settings, setSettings] = React.useState<NotificationDeliveryConfig | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchSettings = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const body = await readApiResultOrThrow<SettingsResponse>(\n '/api/notifications/settings',\n undefined,\n { errorMessage: t('notifications.settings.loadError', 'Failed to load notification settings'), allowNullResult: true },\n )\n if (body?.settings) {\n setSettings(body.settings)\n } else {\n setSettings(emptySettings)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.settings.loadError', 'Failed to load notification settings')\n setError(message)\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n fetchSettings()\n }, [fetchSettings])\n\n const updateSettings = (patch: Partial<NotificationDeliveryConfig>) => {\n setSettings((prev) => (prev ? { ...prev, ...patch } : prev))\n }\n\n const updateStrategy = (\n strategy: keyof NotificationDeliveryConfig['strategies'],\n patch: Partial<NotificationDeliveryConfig['strategies'][keyof NotificationDeliveryConfig['strategies']]>,\n ) => {\n setSettings((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n strategies: {\n ...prev.strategies,\n [strategy]: {\n ...prev.strategies[strategy],\n ...patch,\n },\n },\n }\n })\n }\n\n const handleSave = async () => {\n if (!settings) return\n setSaving(true)\n try {\n const response = await apiCall<SettingsResponse>('/api/notifications/settings', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(settings),\n })\n if (!response.ok) {\n const message = response.result?.error || t('notifications.settings.saveError', 'Failed to save notification settings')\n throw new Error(message)\n }\n if (response.result?.settings) {\n setSettings(response.result.settings)\n }\n flash(t('notifications.settings.saveSuccess', 'Notification settings saved'), 'success')\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.settings.saveError', 'Failed to save notification settings')\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }\n\n if (loading || !settings) {\n return (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner size=\"sm\" />\n {t('notifications.settings.loading', 'Loading notification settings...')}\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{t('notifications.settings.pageTitle', 'Notification Delivery')}</h1>\n <p className=\"text-muted-foreground text-sm\">\n {t('notifications.settings.pageDescription', 'Configure delivery strategies for in-app notifications.')}\n </p>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('notifications.settings.core.title', 'Core delivery')}</CardTitle>\n <CardDescription>{t('notifications.settings.core.description', 'Control the default notification center and panel link used by external channels.')}</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-app-url\">{t('notifications.settings.core.appUrl', 'Application URL')}</Label>\n <Input\n id=\"notifications-app-url\"\n value={settings.appUrl ?? ''}\n placeholder=\"https://app.open-mercato.com\"\n onChange={(event) => updateSettings({ appUrl: event.target.value || undefined })}\n />\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.appUrlHint', 'Used to build absolute links in email notifications.')}</p>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-panel-path\">{t('notifications.settings.core.panelPath', 'Notification panel path')}</Label>\n <Input\n id=\"notifications-panel-path\"\n value={settings.panelPath}\n onChange={(event) => updateSettings({ panelPath: event.target.value })}\n />\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.panelPathHint', 'Relative path for the read-only notification panel.')}</p>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <p className=\"text-sm font-medium\">{t('notifications.settings.core.databaseLabel', 'In-app notifications')}</p>\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.databaseHint', 'Store notifications in the database for the panel and bell.')}</p>\n </div>\n <Switch\n checked={settings.strategies.database.enabled}\n disabled\n onCheckedChange={(checked) => updateStrategy('database', { enabled: checked })}\n />\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('notifications.settings.email.title', 'Email strategy')}</CardTitle>\n <CardDescription>{t('notifications.settings.email.description', 'Send notification copies via Resend using React templates.')}</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border p-3 md:col-span-2\">\n <div>\n <p className=\"text-sm font-medium\">{t('notifications.settings.email.enabledLabel', 'Enable email delivery')}</p>\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.email.enabledHint', 'Email actions are read-only and link back to the notification center.')}</p>\n </div>\n <Switch\n checked={settings.strategies.email.enabled}\n onCheckedChange={(checked) => updateStrategy('email', { enabled: checked })}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-email-from\">{t('notifications.settings.email.from', 'From address')}</Label>\n <Input\n id=\"notifications-email-from\"\n value={settings.strategies.email.from ?? ''}\n placeholder=\"notifications@open-mercato.com\"\n onChange={(event) => updateStrategy('email', { from: event.target.value || undefined })}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-email-reply\">{t('notifications.settings.email.replyTo', 'Reply-to')}</Label>\n <Input\n id=\"notifications-email-reply\"\n value={settings.strategies.email.replyTo ?? ''}\n placeholder=\"support@open-mercato.com\"\n onChange={(event) => updateStrategy('email', { replyTo: event.target.value || undefined })}\n />\n </div>\n <div className=\"space-y-2 md:col-span-2\">\n <Label htmlFor=\"notifications-email-subject-prefix\">{t('notifications.settings.email.subjectPrefix', 'Subject prefix')}</Label>\n <Input\n id=\"notifications-email-subject-prefix\"\n value={settings.strategies.email.subjectPrefix ?? ''}\n placeholder=\"[Open Mercato]\"\n onChange={(event) => updateStrategy('email', { subjectPrefix: event.target.value || undefined })}\n />\n </div>\n </CardContent>\n </Card>\n\n <div className=\"flex items-center gap-3\">\n <Button type=\"button\" onClick={handleSave} disabled={saving}>\n {saving ? t('notifications.settings.saving', 'Saving...') : t('notifications.settings.save', 'Save settings')}\n </Button>\n {error && <span className=\"text-sm text-destructive\">{error}</span>}\n </div>\n </div>\n )\n}\n\nexport default NotificationSettingsPageClient\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\n\ntype NotificationDeliveryConfig = {\n appUrl?: string\n panelPath: string\n strategies: {\n database: { enabled: boolean }\n email: { enabled: boolean; from?: string; replyTo?: string; subjectPrefix?: string }\n custom?: Record<string, { enabled?: boolean; config?: unknown }>\n }\n}\n\ntype SettingsResponse = {\n settings?: NotificationDeliveryConfig\n error?: string\n}\n\nconst emptySettings: NotificationDeliveryConfig = {\n panelPath: '/backend/notifications',\n strategies: {\n database: { enabled: true },\n email: { enabled: true },\n custom: {},\n },\n}\n\nexport function NotificationSettingsPageClient() {\n const t = useT()\n const [settings, setSettings] = React.useState<NotificationDeliveryConfig | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchSettings = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const body = await readApiResultOrThrow<SettingsResponse>(\n '/api/notifications/settings',\n undefined,\n { errorMessage: t('notifications.settings.loadError', 'Failed to load notification settings'), allowNullResult: true },\n )\n if (body?.settings) {\n setSettings(body.settings)\n } else {\n setSettings(emptySettings)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.settings.loadError', 'Failed to load notification settings')\n setError(message)\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n fetchSettings()\n }, [fetchSettings])\n\n const updateSettings = (patch: Partial<NotificationDeliveryConfig>) => {\n setSettings((prev) => (prev ? { ...prev, ...patch } : prev))\n }\n\n const updateStrategy = (\n strategy: keyof NotificationDeliveryConfig['strategies'],\n patch: Partial<NotificationDeliveryConfig['strategies'][keyof NotificationDeliveryConfig['strategies']]>,\n ) => {\n setSettings((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n strategies: {\n ...prev.strategies,\n [strategy]: {\n ...prev.strategies[strategy],\n ...patch,\n },\n },\n }\n })\n }\n\n const handleSave = async () => {\n if (!settings) return\n setSaving(true)\n try {\n const response = await apiCall<SettingsResponse>('/api/notifications/settings', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(settings),\n })\n if (!response.ok) {\n const message = response.result?.error || t('notifications.settings.saveError', 'Failed to save notification settings')\n throw new Error(message)\n }\n if (response.result?.settings) {\n setSettings(response.result.settings)\n }\n flash(t('notifications.settings.saveSuccess', 'Notification settings saved'), 'success')\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.settings.saveError', 'Failed to save notification settings')\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }\n\n if (loading || !settings) {\n return (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner size=\"sm\" />\n {t('notifications.settings.loading', 'Loading notification settings...')}\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{t('notifications.settings.pageTitle', 'Notification Delivery')}</h1>\n <p className=\"text-muted-foreground text-sm\">\n {t('notifications.settings.pageDescription', 'Configure delivery strategies for in-app notifications.')}\n </p>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('notifications.settings.core.title', 'Core delivery')}</CardTitle>\n <CardDescription>{t('notifications.settings.core.description', 'Control the default notification center and panel link used by external channels.')}</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-app-url\">{t('notifications.settings.core.appUrl', 'Application URL')}</Label>\n <Input\n id=\"notifications-app-url\"\n value={settings.appUrl ?? ''}\n placeholder=\"https://app.open-mercato.com\"\n onChange={(event) => updateSettings({ appUrl: event.target.value || undefined })}\n />\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.appUrlHint', 'Used to build absolute links in email notifications.')}</p>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-panel-path\">{t('notifications.settings.core.panelPath', 'Notification panel path')}</Label>\n <Input\n id=\"notifications-panel-path\"\n value={settings.panelPath}\n onChange={(event) => updateSettings({ panelPath: event.target.value })}\n />\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.panelPathHint', 'Relative path for the read-only notification panel.')}</p>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <p className=\"text-sm font-medium\">{t('notifications.settings.core.databaseLabel', 'In-app notifications')}</p>\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.databaseHint', 'Store notifications in the database for the panel and bell.')}</p>\n </div>\n <Switch\n checked={settings.strategies.database.enabled}\n disabled\n onCheckedChange={(checked) => updateStrategy('database', { enabled: checked })}\n />\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('notifications.settings.email.title', 'Email strategy')}</CardTitle>\n <CardDescription>{t('notifications.settings.email.description', 'Send notification copies via Resend using React templates.')}</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border p-3 md:col-span-2\">\n <div>\n <p className=\"text-sm font-medium\">{t('notifications.settings.email.enabledLabel', 'Enable email delivery')}</p>\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.email.enabledHint', 'Email actions are read-only and link back to the notification center.')}</p>\n </div>\n <Switch\n checked={settings.strategies.email.enabled}\n onCheckedChange={(checked) => updateStrategy('email', { enabled: checked })}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-email-from\">{t('notifications.settings.email.from', 'From address')}</Label>\n <Input\n id=\"notifications-email-from\"\n value={settings.strategies.email.from ?? ''}\n placeholder=\"notifications@open-mercato.com\"\n onChange={(event) => updateStrategy('email', { from: event.target.value || undefined })}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-email-reply\">{t('notifications.settings.email.replyTo', 'Reply-to')}</Label>\n <Input\n id=\"notifications-email-reply\"\n value={settings.strategies.email.replyTo ?? ''}\n placeholder=\"support@open-mercato.com\"\n onChange={(event) => updateStrategy('email', { replyTo: event.target.value || undefined })}\n />\n </div>\n <div className=\"space-y-2 md:col-span-2\">\n <Label htmlFor=\"notifications-email-subject-prefix\">{t('notifications.settings.email.subjectPrefix', 'Subject prefix')}</Label>\n <Input\n id=\"notifications-email-subject-prefix\"\n value={settings.strategies.email.subjectPrefix ?? ''}\n placeholder=\"[Open Mercato]\"\n onChange={(event) => updateStrategy('email', { subjectPrefix: event.target.value || undefined })}\n />\n </div>\n </CardContent>\n </Card>\n\n <div className=\"flex items-center gap-3\">\n <Button type=\"button\" onClick={handleSave} disabled={saving}>\n {saving ? t('notifications.settings.saving', 'Saving...') : t('notifications.settings.save', 'Save settings')}\n </Button>\n {error && <span className=\"text-sm text-destructive\">{error}</span>}\n </div>\n </div>\n )\n}\n\nexport default NotificationSettingsPageClient\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -36,7 +36,8 @@ const DEFAULT_NOTIFICATION_DELIVERY_CONFIG = (() => {
36
36
  from: env.emailFrom,
37
37
  replyTo: env.emailReplyTo,
38
38
  subjectPrefix: env.emailSubjectPrefix
39
- }
39
+ },
40
+ custom: {}
40
41
  }
41
42
  };
42
43
  })();
@@ -59,7 +60,8 @@ const normalizeDeliveryConfig = (input) => {
59
60
  from: strategies.email?.from,
60
61
  replyTo: strategies.email?.replyTo,
61
62
  subjectPrefix: strategies.email?.subjectPrefix
62
- }
63
+ },
64
+ custom: strategies.custom ?? {}
63
65
  }
64
66
  };
65
67
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/notifications/lib/deliveryConfig.ts"],
4
- "sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { notificationDeliveryConfigSchema, type NotificationDeliveryConfigInput } from '../data/validators'\n\nexport const NOTIFICATIONS_DELIVERY_CONFIG_KEY = 'delivery_strategies'\n\nexport type NotificationDeliveryStrategyState = {\n enabled: boolean\n}\n\nexport type NotificationEmailDeliveryConfig = NotificationDeliveryStrategyState & {\n from?: string\n replyTo?: string\n subjectPrefix?: string\n}\n\nexport type NotificationDeliveryConfig = {\n appUrl?: string\n panelPath: string\n strategies: {\n database: NotificationDeliveryStrategyState\n email: NotificationEmailDeliveryConfig\n }\n}\n\nconst envString = (value: string | undefined | null) => {\n if (!value) return undefined\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n}\n\nconst resolveEnvDefaults = () => {\n const appUrl = envString(\n process.env.NOTIFICATIONS_APP_URL ||\n process.env.APPLICATION_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL\n )\n const panelPath = envString(process.env.NOTIFICATIONS_PANEL_PATH)\n const emailEnabled = parseBooleanWithDefault(process.env.NOTIFICATIONS_EMAIL_ENABLED, true)\n const emailFrom = envString(process.env.NOTIFICATIONS_EMAIL_FROM || process.env.EMAIL_FROM)\n const emailReplyTo = envString(process.env.NOTIFICATIONS_EMAIL_REPLY_TO || process.env.ADMIN_EMAIL)\n const emailSubjectPrefix = envString(process.env.NOTIFICATIONS_EMAIL_SUBJECT_PREFIX)\n\n return {\n appUrl,\n panelPath,\n emailEnabled,\n emailFrom,\n emailReplyTo,\n emailSubjectPrefix,\n }\n}\n\nexport const DEFAULT_NOTIFICATION_DELIVERY_CONFIG: NotificationDeliveryConfig = (() => {\n const env = resolveEnvDefaults()\n return {\n appUrl: env.appUrl,\n panelPath: env.panelPath ?? '/backend/notifications',\n strategies: {\n database: { enabled: true },\n email: {\n enabled: env.emailEnabled,\n from: env.emailFrom,\n replyTo: env.emailReplyTo,\n subjectPrefix: env.emailSubjectPrefix,\n },\n },\n }\n})()\n\nconst normalizeDeliveryConfig = (input?: unknown | null): NotificationDeliveryConfig => {\n const parsed = notificationDeliveryConfigSchema.safeParse(input ?? {})\n if (!parsed.success) {\n return { ...DEFAULT_NOTIFICATION_DELIVERY_CONFIG }\n }\n\n const value = parsed.data ?? {}\n const strategies = value.strategies ?? {}\n\n return {\n appUrl: value.appUrl,\n panelPath: value.panelPath ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG.panelPath,\n strategies: {\n database: {\n enabled: DEFAULT_NOTIFICATION_DELIVERY_CONFIG.strategies.database.enabled,\n },\n email: {\n enabled: strategies.email?.enabled ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG.strategies.email.enabled,\n from: strategies.email?.from,\n replyTo: strategies.email?.replyTo,\n subjectPrefix: strategies.email?.subjectPrefix,\n },\n },\n }\n}\n\ntype Resolver = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport async function resolveNotificationDeliveryConfig(\n resolver: Resolver,\n options?: { defaultValue?: NotificationDeliveryConfig }\n): Promise<NotificationDeliveryConfig> {\n const fallback = options?.defaultValue ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n return { ...fallback }\n }\n try {\n const value = await service.getValue('notifications', NOTIFICATIONS_DELIVERY_CONFIG_KEY, { defaultValue: fallback })\n return normalizeDeliveryConfig(value)\n } catch {\n return { ...fallback }\n }\n}\n\nexport async function saveNotificationDeliveryConfig(\n resolver: Resolver,\n config: NotificationDeliveryConfigInput\n): Promise<void> {\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n throw new Error('Configuration service unavailable')\n }\n\n const normalized = normalizeDeliveryConfig(config)\n await service.setValue('notifications', NOTIFICATIONS_DELIVERY_CONFIG_KEY, normalized)\n}\n\nexport function resolveNotificationPanelUrl(config: NotificationDeliveryConfig): string | null {\n const base = config.appUrl\n || process.env.APPLICATION_URL\n || process.env.NEXT_PUBLIC_APP_URL\n || process.env.APP_URL\n if (!base || !base.trim()) {\n return config.panelPath\n }\n return `${base.replace(/\\/$/, '')}${config.panelPath}`\n}\n"],
5
- "mappings": "AACA,SAAS,+BAA+B;AACxC,SAAS,wCAA8E;AAEhF,MAAM,oCAAoC;AAqBjD,MAAM,YAAY,CAAC,UAAqC;AACtD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,UAAU;AACpC;AAEA,MAAM,qBAAqB,MAAM;AAC/B,QAAM,SAAS;AAAA,IACb,QAAQ,IAAI,yBACZ,QAAQ,IAAI,mBACZ,QAAQ,IAAI,uBACZ,QAAQ,IAAI;AAAA,EACd;AACA,QAAM,YAAY,UAAU,QAAQ,IAAI,wBAAwB;AAChE,QAAM,eAAe,wBAAwB,QAAQ,IAAI,6BAA6B,IAAI;AAC1F,QAAM,YAAY,UAAU,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,UAAU;AAC1F,QAAM,eAAe,UAAU,QAAQ,IAAI,gCAAgC,QAAQ,IAAI,WAAW;AAClG,QAAM,qBAAqB,UAAU,QAAQ,IAAI,kCAAkC;AAEnF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEO,MAAM,wCAAoE,MAAM;AACrF,QAAM,MAAM,mBAAmB;AAC/B,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI,aAAa;AAAA,IAC5B,YAAY;AAAA,MACV,UAAU,EAAE,SAAS,KAAK;AAAA,MAC1B,OAAO;AAAA,QACL,SAAS,IAAI;AAAA,QACb,MAAM,IAAI;AAAA,QACV,SAAS,IAAI;AAAA,QACb,eAAe,IAAI;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF,GAAG;AAEH,MAAM,0BAA0B,CAAC,UAAuD;AACtF,QAAM,SAAS,iCAAiC,UAAU,SAAS,CAAC,CAAC;AACrE,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,EAAE,GAAG,qCAAqC;AAAA,EACnD;AAEA,QAAM,QAAQ,OAAO,QAAQ,CAAC;AAC9B,QAAM,aAAa,MAAM,cAAc,CAAC;AAExC,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,WAAW,MAAM,aAAa,qCAAqC;AAAA,IACnE,YAAY;AAAA,MACV,UAAU;AAAA,QACR,SAAS,qCAAqC,WAAW,SAAS;AAAA,MACpE;AAAA,MACA,OAAO;AAAA,QACL,SAAS,WAAW,OAAO,WAAW,qCAAqC,WAAW,MAAM;AAAA,QAC5F,MAAM,WAAW,OAAO;AAAA,QACxB,SAAS,WAAW,OAAO;AAAA,QAC3B,eAAe,WAAW,OAAO;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AACF;AAMA,eAAsB,kCACpB,UACA,SACqC;AACrC,QAAM,WAAW,SAAS,gBAAgB;AAC1C,MAAI;AACJ,MAAI;AACF,cAAU,SAAS,QAA6B,qBAAqB;AAAA,EACvE,QAAQ;AACN,WAAO,EAAE,GAAG,SAAS;AAAA,EACvB;AACA,MAAI;AACF,UAAM,QAAQ,MAAM,QAAQ,SAAS,iBAAiB,mCAAmC,EAAE,cAAc,SAAS,CAAC;AACnH,WAAO,wBAAwB,KAAK;AAAA,EACtC,QAAQ;AACN,WAAO,EAAE,GAAG,SAAS;AAAA,EACvB;AACF;AAEA,eAAsB,+BACpB,UACA,QACe;AACf,MAAI;AACJ,MAAI;AACF,cAAU,SAAS,QAA6B,qBAAqB;AAAA,EACvE,QAAQ;AACN,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAEA,QAAM,aAAa,wBAAwB,MAAM;AACjD,QAAM,QAAQ,SAAS,iBAAiB,mCAAmC,UAAU;AACvF;AAEO,SAAS,4BAA4B,QAAmD;AAC7F,QAAM,OAAO,OAAO,UACf,QAAQ,IAAI,mBACZ,QAAQ,IAAI,uBACZ,QAAQ,IAAI;AACjB,MAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,GAAG;AACzB,WAAO,OAAO;AAAA,EAChB;AACA,SAAO,GAAG,KAAK,QAAQ,OAAO,EAAE,CAAC,GAAG,OAAO,SAAS;AACtD;",
4
+ "sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { notificationDeliveryConfigSchema, type NotificationDeliveryConfigInput } from '../data/validators'\n\nexport const NOTIFICATIONS_DELIVERY_CONFIG_KEY = 'delivery_strategies'\n\nexport type NotificationDeliveryStrategyState = {\n enabled: boolean\n}\n\nexport type NotificationCustomDeliveryConfig = {\n enabled?: boolean\n config?: unknown\n}\n\nexport type NotificationEmailDeliveryConfig = NotificationDeliveryStrategyState & {\n from?: string\n replyTo?: string\n subjectPrefix?: string\n}\n\nexport type NotificationDeliveryConfig = {\n appUrl?: string\n panelPath: string\n strategies: {\n database: NotificationDeliveryStrategyState\n email: NotificationEmailDeliveryConfig\n custom?: Record<string, NotificationCustomDeliveryConfig>\n }\n}\n\nconst envString = (value: string | undefined | null) => {\n if (!value) return undefined\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n}\n\nconst resolveEnvDefaults = () => {\n const appUrl = envString(\n process.env.NOTIFICATIONS_APP_URL ||\n process.env.APPLICATION_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL\n )\n const panelPath = envString(process.env.NOTIFICATIONS_PANEL_PATH)\n const emailEnabled = parseBooleanWithDefault(process.env.NOTIFICATIONS_EMAIL_ENABLED, true)\n const emailFrom = envString(process.env.NOTIFICATIONS_EMAIL_FROM || process.env.EMAIL_FROM)\n const emailReplyTo = envString(process.env.NOTIFICATIONS_EMAIL_REPLY_TO || process.env.ADMIN_EMAIL)\n const emailSubjectPrefix = envString(process.env.NOTIFICATIONS_EMAIL_SUBJECT_PREFIX)\n\n return {\n appUrl,\n panelPath,\n emailEnabled,\n emailFrom,\n emailReplyTo,\n emailSubjectPrefix,\n }\n}\n\nexport const DEFAULT_NOTIFICATION_DELIVERY_CONFIG: NotificationDeliveryConfig = (() => {\n const env = resolveEnvDefaults()\n return {\n appUrl: env.appUrl,\n panelPath: env.panelPath ?? '/backend/notifications',\n strategies: {\n database: { enabled: true },\n email: {\n enabled: env.emailEnabled,\n from: env.emailFrom,\n replyTo: env.emailReplyTo,\n subjectPrefix: env.emailSubjectPrefix,\n },\n custom: {},\n },\n }\n})()\n\nconst normalizeDeliveryConfig = (input?: unknown | null): NotificationDeliveryConfig => {\n const parsed = notificationDeliveryConfigSchema.safeParse(input ?? {})\n if (!parsed.success) {\n return { ...DEFAULT_NOTIFICATION_DELIVERY_CONFIG }\n }\n\n const value = parsed.data ?? {}\n const strategies = value.strategies ?? {}\n\n return {\n appUrl: value.appUrl,\n panelPath: value.panelPath ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG.panelPath,\n strategies: {\n database: {\n enabled: DEFAULT_NOTIFICATION_DELIVERY_CONFIG.strategies.database.enabled,\n },\n email: {\n enabled: strategies.email?.enabled ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG.strategies.email.enabled,\n from: strategies.email?.from,\n replyTo: strategies.email?.replyTo,\n subjectPrefix: strategies.email?.subjectPrefix,\n },\n custom: strategies.custom ?? {},\n },\n }\n}\n\ntype Resolver = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport async function resolveNotificationDeliveryConfig(\n resolver: Resolver,\n options?: { defaultValue?: NotificationDeliveryConfig }\n): Promise<NotificationDeliveryConfig> {\n const fallback = options?.defaultValue ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n return { ...fallback }\n }\n try {\n const value = await service.getValue('notifications', NOTIFICATIONS_DELIVERY_CONFIG_KEY, { defaultValue: fallback })\n return normalizeDeliveryConfig(value)\n } catch {\n return { ...fallback }\n }\n}\n\nexport async function saveNotificationDeliveryConfig(\n resolver: Resolver,\n config: NotificationDeliveryConfigInput\n): Promise<void> {\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n throw new Error('Configuration service unavailable')\n }\n\n const normalized = normalizeDeliveryConfig(config)\n await service.setValue('notifications', NOTIFICATIONS_DELIVERY_CONFIG_KEY, normalized)\n}\n\nexport function resolveNotificationPanelUrl(config: NotificationDeliveryConfig): string | null {\n const base = config.appUrl\n || process.env.APPLICATION_URL\n || process.env.NEXT_PUBLIC_APP_URL\n || process.env.APP_URL\n if (!base || !base.trim()) {\n return config.panelPath\n }\n return `${base.replace(/\\/$/, '')}${config.panelPath}`\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,14 @@
1
+ const registry = [];
2
+ function registerNotificationDeliveryStrategy(strategy, options) {
3
+ const priority = options?.priority ?? 0;
4
+ registry.push({ ...strategy, priority });
5
+ registry.sort((a, b) => b.priority - a.priority);
6
+ }
7
+ function getNotificationDeliveryStrategies() {
8
+ return registry;
9
+ }
10
+ export {
11
+ getNotificationDeliveryStrategies,
12
+ registerNotificationDeliveryStrategy
13
+ };
14
+ //# sourceMappingURL=deliveryStrategies.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/notifications/lib/deliveryStrategies.ts"],
4
+ "sourcesContent": ["import type { Notification } from '../data/entities'\nimport type { NotificationDeliveryConfig } from './deliveryConfig'\n\nexport type NotificationDeliveryStrategyConfig = {\n enabled?: boolean\n config?: unknown\n}\n\nexport type NotificationDeliveryRecipient = {\n email?: string | null\n name?: string | null\n}\n\nexport type NotificationDeliveryContext = {\n notification: Notification\n recipient: NotificationDeliveryRecipient\n title: string\n body: string | null\n panelUrl: string | null\n panelLink: string | null\n actionLinks: Array<{ id: string; label: string; href: string }>\n deliveryConfig: NotificationDeliveryConfig\n config: NotificationDeliveryStrategyConfig\n resolve: <T = unknown>(name: string) => T\n t: (key: string, fallback?: string, variables?: Record<string, string>) => string\n}\n\nexport type NotificationDeliveryStrategy = {\n id: string\n label?: string\n defaultEnabled?: boolean\n deliver: (ctx: NotificationDeliveryContext) => Promise<void> | void\n}\n\ntype RegisteredStrategy = NotificationDeliveryStrategy & { priority: number }\n\nconst registry: RegisteredStrategy[] = []\n\nexport function registerNotificationDeliveryStrategy(\n strategy: NotificationDeliveryStrategy,\n options?: { priority?: number }\n): void {\n const priority = options?.priority ?? 0\n registry.push({ ...strategy, priority })\n registry.sort((a, b) => b.priority - a.priority)\n}\n\nexport function getNotificationDeliveryStrategies(): NotificationDeliveryStrategy[] {\n return registry\n}\n"],
5
+ "mappings": "AAoCA,MAAM,WAAiC,CAAC;AAEjC,SAAS,qCACd,UACA,SACM;AACN,QAAM,WAAW,SAAS,YAAY;AACtC,WAAS,KAAK,EAAE,GAAG,UAAU,SAAS,CAAC;AACvC,WAAS,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AACjD;AAEO,SAAS,oCAAoE;AAClF,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -1,6 +1,7 @@
1
1
  import { Notification } from "../data/entities.js";
2
2
  import { NOTIFICATION_EVENTS } from "../lib/events.js";
3
3
  import { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig, resolveNotificationPanelUrl } from "../lib/deliveryConfig.js";
4
+ import { getNotificationDeliveryStrategies } from "../lib/deliveryStrategies.js";
4
5
  import { sendEmail } from "@open-mercato/shared/lib/email/send";
5
6
  import NotificationEmail from "../emails/NotificationEmail.js";
6
7
  import { loadDictionary } from "@open-mercato/shared/lib/i18n/server";
@@ -66,7 +67,6 @@ async function handle(payload, ctx) {
66
67
  const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, { defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG });
67
68
  if (!deliveryConfig.strategies.email.enabled) {
68
69
  debug("email delivery disabled");
69
- return;
70
70
  }
71
71
  const em = ctx.resolve("em");
72
72
  const notification = await em.findOne(Notification, {
@@ -84,7 +84,7 @@ async function handle(payload, ctx) {
84
84
  } catch {
85
85
  encryptionService = null;
86
86
  }
87
- const recipient = await resolveRecipient(em, notification, encryptionService);
87
+ const recipient = await resolveRecipient(em, notification, encryptionService) ?? { email: null, name: null };
88
88
  if (!recipient?.email) {
89
89
  debug("recipient has no email", notification.recipientUserId);
90
90
  }
@@ -92,15 +92,14 @@ async function handle(payload, ctx) {
92
92
  const panelUrl = resolveNotificationPanelUrl(deliveryConfig);
93
93
  if (!panelUrl) {
94
94
  debug("missing panelUrl; check appUrl/panelPath settings");
95
- return;
96
95
  }
97
- const panelLink = buildPanelLink(panelUrl, notification.id);
98
- const actionLinks = (notification.actionData?.actions ?? []).map((action) => ({
96
+ const panelLink = panelUrl ? buildPanelLink(panelUrl, notification.id) : null;
97
+ const actionLinks = panelLink ? (notification.actionData?.actions ?? []).map((action) => ({
99
98
  id: action.id,
100
99
  label: action.labelKey ? t(action.labelKey, action.label) : action.label,
101
100
  href: panelLink
102
- }));
103
- if (deliveryConfig.strategies.email.enabled && recipient?.email) {
101
+ })) : [];
102
+ if (deliveryConfig.strategies.email.enabled && recipient?.email && panelLink) {
104
103
  const subjectPrefix = deliveryConfig.strategies.email.subjectPrefix?.trim();
105
104
  const subject = subjectPrefix ? `${subjectPrefix} ${title}` : title;
106
105
  const copy = {
@@ -130,6 +129,33 @@ async function handle(payload, ctx) {
130
129
  console.error("[notifications] email delivery failed", error);
131
130
  }
132
131
  }
132
+ const strategyConfigs = deliveryConfig.strategies.custom ?? {};
133
+ const strategies = getNotificationDeliveryStrategies();
134
+ for (const strategy of strategies) {
135
+ const strategyConfig = strategyConfigs[strategy.id];
136
+ const enabled = strategyConfig?.enabled ?? strategy.defaultEnabled ?? false;
137
+ if (!enabled) {
138
+ debug("custom delivery disabled", strategy.id);
139
+ continue;
140
+ }
141
+ try {
142
+ await strategy.deliver({
143
+ notification,
144
+ recipient,
145
+ title,
146
+ body,
147
+ panelUrl,
148
+ panelLink,
149
+ actionLinks,
150
+ deliveryConfig,
151
+ config: strategyConfig ?? {},
152
+ resolve: ctx.resolve,
153
+ t
154
+ });
155
+ } catch (error) {
156
+ console.error(`[notifications] delivery strategy failed (${strategy.id})`, error);
157
+ }
158
+ }
133
159
  return;
134
160
  }
135
161
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/notifications/subscribers/deliver-notification.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Notification } from '../data/entities'\nimport { NOTIFICATION_EVENTS } from '../lib/events'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig, resolveNotificationPanelUrl } from '../lib/deliveryConfig'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport NotificationEmail from '../emails/NotificationEmail'\nimport { loadDictionary } from '@open-mercato/shared/lib/i18n/server'\nimport { createFallbackTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport { defaultLocale } from '@open-mercato/shared/lib/i18n/config'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../auth/data/entities'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\n\nexport const metadata = {\n event: NOTIFICATION_EVENTS.CREATED,\n persistent: true,\n id: 'notifications:deliver',\n}\n\nconst DEBUG = process.env.NOTIFICATIONS_DEBUG === 'true'\n\nfunction debug(...args: unknown[]): void {\n if (DEBUG) {\n console.log('[notifications]', ...args)\n }\n}\n\ntype NotificationCreatedPayload = {\n notificationId: string\n recipientUserId: string\n tenantId: string\n organizationId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nconst buildPanelLink = (panelUrl: string, notificationId: string) => {\n if (panelUrl.startsWith('http://') || panelUrl.startsWith('https://')) {\n const url = new URL(panelUrl)\n url.searchParams.set('notificationId', notificationId)\n return url.toString()\n }\n const separator = panelUrl.includes('?') ? '&' : '?'\n return `${panelUrl}${separator}notificationId=${encodeURIComponent(notificationId)}`\n}\n\nconst resolveNotificationCopy = async (\n notification: Notification\n) => {\n const dict = await loadDictionary(defaultLocale)\n const t = createFallbackTranslator(dict)\n\n const title = notification.titleKey\n ? t(notification.titleKey, notification.title ?? notification.titleKey, notification.titleVariables ?? undefined)\n : notification.title\n\n const body = notification.bodyKey\n ? t(notification.bodyKey, notification.body ?? notification.bodyKey ?? '', notification.bodyVariables ?? undefined)\n : notification.body ?? null\n\n return { title, body, t }\n}\n\nconst resolveRecipient = async (\n em: EntityManager,\n notification: Notification,\n encryptionService?: TenantDataEncryptionService | null,\n) => {\n const where: Partial<User> & { deletedAt?: null } = {\n id: notification.recipientUserId,\n tenantId: notification.tenantId,\n deletedAt: null,\n }\n if (notification.organizationId) {\n where.organizationId = notification.organizationId\n }\n const record = await findOneWithDecryption(\n em,\n User,\n where,\n undefined,\n {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n encryptionService: encryptionService ?? null,\n },\n )\n if (!record) return null\n return {\n email: typeof record.email === 'string' ? record.email : null,\n name: typeof record.name === 'string' ? record.name : null,\n }\n}\n\n\nexport default async function handle(payload: NotificationCreatedPayload, ctx: ResolverContext) {\n debug('deliver notification event', payload)\n const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, { defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG })\n if (!deliveryConfig.strategies.email.enabled) {\n debug('email delivery disabled')\n return\n }\n\n const em = ctx.resolve('em') as EntityManager\n const notification = await em.findOne(Notification, {\n id: payload.notificationId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n if (!notification) {\n debug('notification not found', payload.notificationId)\n return\n }\n\n let encryptionService: TenantDataEncryptionService | null = null\n try {\n encryptionService = ctx.resolve<TenantDataEncryptionService>('tenantEncryptionService')\n } catch {\n encryptionService = null\n }\n\n const recipient = await resolveRecipient(em, notification, encryptionService)\n if (!recipient?.email) {\n debug('recipient has no email', notification.recipientUserId)\n }\n const { title, body, t } = await resolveNotificationCopy(notification)\n const panelUrl = resolveNotificationPanelUrl(deliveryConfig)\n if (!panelUrl) {\n debug('missing panelUrl; check appUrl/panelPath settings')\n return\n }\n\n const panelLink = buildPanelLink(panelUrl, notification.id)\n const actionLinks = (notification.actionData?.actions ?? []).map((action) => ({\n id: action.id,\n label: action.labelKey ? t(action.labelKey, action.label) : action.label,\n href: panelLink,\n }))\n\n if (deliveryConfig.strategies.email.enabled && recipient?.email) {\n const subjectPrefix = deliveryConfig.strategies.email.subjectPrefix?.trim()\n const subject = subjectPrefix ? `${subjectPrefix} ${title}` : title\n const copy = {\n preview: t('notifications.delivery.email.preview', 'New notification'),\n heading: t('notifications.delivery.email.heading', 'You have a new notification'),\n bodyIntro: t('notifications.delivery.email.bodyIntro', 'Review the notification details and take any required actions.'),\n actionNotice: t('notifications.delivery.email.actionNotice', 'Actions are available in Open Mercato and are read-only in this email.'),\n openCta: t('notifications.delivery.email.openCta', 'Open notification center'),\n footer: t('notifications.delivery.email.footer', 'Open Mercato notifications'),\n }\n\n try {\n debug('sending email', { to: recipient.email, from: deliveryConfig.strategies.email.from, subject })\n await sendEmail({\n to: recipient.email,\n subject,\n from: deliveryConfig.strategies.email.from,\n replyTo: deliveryConfig.strategies.email.replyTo,\n react: NotificationEmail({\n title,\n body,\n actions: actionLinks,\n panelUrl: panelLink,\n copy,\n }),\n })\n } catch (error) {\n console.error('[notifications] email delivery failed', error)\n }\n }\n\n return\n}\n"],
5
- "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,sCAAsC,mCAAmC,mCAAmC;AACrH,SAAS,iBAAiB;AAC1B,OAAO,uBAAuB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,gCAAgC;AACzC,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AACtC,SAAS,YAAY;AAGd,MAAM,WAAW;AAAA,EACtB,OAAO,oBAAoB;AAAA,EAC3B,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,MAAM,QAAQ,QAAQ,IAAI,wBAAwB;AAElD,SAAS,SAAS,MAAuB;AACvC,MAAI,OAAO;AACT,YAAQ,IAAI,mBAAmB,GAAG,IAAI;AAAA,EACxC;AACF;AAaA,MAAM,iBAAiB,CAAC,UAAkB,mBAA2B;AACnE,MAAI,SAAS,WAAW,SAAS,KAAK,SAAS,WAAW,UAAU,GAAG;AACrE,UAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,QAAI,aAAa,IAAI,kBAAkB,cAAc;AACrD,WAAO,IAAI,SAAS;AAAA,EACtB;AACA,QAAM,YAAY,SAAS,SAAS,GAAG,IAAI,MAAM;AACjD,SAAO,GAAG,QAAQ,GAAG,SAAS,kBAAkB,mBAAmB,cAAc,CAAC;AACpF;AAEA,MAAM,0BAA0B,OAC9B,iBACG;AACH,QAAM,OAAO,MAAM,eAAe,aAAa;AAC/C,QAAM,IAAI,yBAAyB,IAAI;AAEvC,QAAM,QAAQ,aAAa,WACvB,EAAE,aAAa,UAAU,aAAa,SAAS,aAAa,UAAU,aAAa,kBAAkB,MAAS,IAC9G,aAAa;AAEjB,QAAM,OAAO,aAAa,UACtB,EAAE,aAAa,SAAS,aAAa,QAAQ,aAAa,WAAW,IAAI,aAAa,iBAAiB,MAAS,IAChH,aAAa,QAAQ;AAEzB,SAAO,EAAE,OAAO,MAAM,EAAE;AAC1B;AAEA,MAAM,mBAAmB,OACvB,IACA,cACA,sBACG;AACH,QAAM,QAA8C;AAAA,IAClD,IAAI,aAAa;AAAA,IACjB,UAAU,aAAa;AAAA,IACvB,WAAW;AAAA,EACb;AACA,MAAI,aAAa,gBAAgB;AAC/B,UAAM,iBAAiB,aAAa;AAAA,EACtC;AACA,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,aAAa;AAAA,MACvB,gBAAgB,aAAa,kBAAkB;AAAA,MAC/C,mBAAmB,qBAAqB;AAAA,IAC1C;AAAA,EACF;AACA,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO;AAAA,IACL,OAAO,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAAA,IACzD,MAAM,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAAA,EACxD;AACF;AAGA,eAAO,OAA8B,SAAqC,KAAsB;AAC9F,QAAM,8BAA8B,OAAO;AAC3C,QAAM,iBAAiB,MAAM,kCAAkC,KAAK,EAAE,cAAc,qCAAqC,CAAC;AAC1H,MAAI,CAAC,eAAe,WAAW,MAAM,SAAS;AAC5C,UAAM,yBAAyB;AAC/B;AAAA,EACF;AAEA,QAAM,KAAK,IAAI,QAAQ,IAAI;AAC3B,QAAM,eAAe,MAAM,GAAG,QAAQ,cAAc;AAAA,IAClD,IAAI,QAAQ;AAAA,IACZ,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ,kBAAkB;AAAA,EAC5C,CAAC;AACD,MAAI,CAAC,cAAc;AACjB,UAAM,0BAA0B,QAAQ,cAAc;AACtD;AAAA,EACF;AAEA,MAAI,oBAAwD;AAC5D,MAAI;AACF,wBAAoB,IAAI,QAAqC,yBAAyB;AAAA,EACxF,QAAQ;AACN,wBAAoB;AAAA,EACtB;AAEA,QAAM,YAAY,MAAM,iBAAiB,IAAI,cAAc,iBAAiB;AAC5E,MAAI,CAAC,WAAW,OAAO;AACrB,UAAM,0BAA0B,aAAa,eAAe;AAAA,EAC9D;AACA,QAAM,EAAE,OAAO,MAAM,EAAE,IAAI,MAAM,wBAAwB,YAAY;AACrE,QAAM,WAAW,4BAA4B,cAAc;AAC3D,MAAI,CAAC,UAAU;AACb,UAAM,mDAAmD;AACzD;AAAA,EACF;AAEA,QAAM,YAAY,eAAe,UAAU,aAAa,EAAE;AAC1D,QAAM,eAAe,aAAa,YAAY,WAAW,CAAC,GAAG,IAAI,CAAC,YAAY;AAAA,IAC5E,IAAI,OAAO;AAAA,IACX,OAAO,OAAO,WAAW,EAAE,OAAO,UAAU,OAAO,KAAK,IAAI,OAAO;AAAA,IACnE,MAAM;AAAA,EACR,EAAE;AAEF,MAAI,eAAe,WAAW,MAAM,WAAW,WAAW,OAAO;AAC/D,UAAM,gBAAgB,eAAe,WAAW,MAAM,eAAe,KAAK;AAC1E,UAAM,UAAU,gBAAgB,GAAG,aAAa,IAAI,KAAK,KAAK;AAC9D,UAAM,OAAO;AAAA,MACX,SAAS,EAAE,wCAAwC,kBAAkB;AAAA,MACrE,SAAS,EAAE,wCAAwC,6BAA6B;AAAA,MAChF,WAAW,EAAE,0CAA0C,gEAAgE;AAAA,MACvH,cAAc,EAAE,6CAA6C,wEAAwE;AAAA,MACrI,SAAS,EAAE,wCAAwC,0BAA0B;AAAA,MAC7E,QAAQ,EAAE,uCAAuC,4BAA4B;AAAA,IAC/E;AAEA,QAAI;AACF,YAAM,iBAAiB,EAAE,IAAI,UAAU,OAAO,MAAM,eAAe,WAAW,MAAM,MAAM,QAAQ,CAAC;AACnG,YAAM,UAAU;AAAA,QACd,IAAI,UAAU;AAAA,QACd;AAAA,QACA,MAAM,eAAe,WAAW,MAAM;AAAA,QACtC,SAAS,eAAe,WAAW,MAAM;AAAA,QACzC,OAAO,kBAAkB;AAAA,UACvB;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UACT,UAAU;AAAA,UACV;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAAA,IACH,SAAS,OAAO;AACd,cAAQ,MAAM,yCAAyC,KAAK;AAAA,IAC9D;AAAA,EACF;AAEA;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Notification } from '../data/entities'\nimport { NOTIFICATION_EVENTS } from '../lib/events'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig, resolveNotificationPanelUrl } from '../lib/deliveryConfig'\nimport { getNotificationDeliveryStrategies } from '../lib/deliveryStrategies'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport NotificationEmail from '../emails/NotificationEmail'\nimport { loadDictionary } from '@open-mercato/shared/lib/i18n/server'\nimport { createFallbackTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport { defaultLocale } from '@open-mercato/shared/lib/i18n/config'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../auth/data/entities'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\n\nexport const metadata = {\n event: NOTIFICATION_EVENTS.CREATED,\n persistent: true,\n id: 'notifications:deliver',\n}\n\nconst DEBUG = process.env.NOTIFICATIONS_DEBUG === 'true'\n\nfunction debug(...args: unknown[]): void {\n if (DEBUG) {\n console.log('[notifications]', ...args)\n }\n}\n\ntype NotificationCreatedPayload = {\n notificationId: string\n recipientUserId: string\n tenantId: string\n organizationId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nconst buildPanelLink = (panelUrl: string, notificationId: string) => {\n if (panelUrl.startsWith('http://') || panelUrl.startsWith('https://')) {\n const url = new URL(panelUrl)\n url.searchParams.set('notificationId', notificationId)\n return url.toString()\n }\n const separator = panelUrl.includes('?') ? '&' : '?'\n return `${panelUrl}${separator}notificationId=${encodeURIComponent(notificationId)}`\n}\n\nconst resolveNotificationCopy = async (\n notification: Notification\n) => {\n const dict = await loadDictionary(defaultLocale)\n const t = createFallbackTranslator(dict)\n\n const title = notification.titleKey\n ? t(notification.titleKey, notification.title ?? notification.titleKey, notification.titleVariables ?? undefined)\n : notification.title\n\n const body = notification.bodyKey\n ? t(notification.bodyKey, notification.body ?? notification.bodyKey ?? '', notification.bodyVariables ?? undefined)\n : notification.body ?? null\n\n return { title, body, t }\n}\n\nconst resolveRecipient = async (\n em: EntityManager,\n notification: Notification,\n encryptionService?: TenantDataEncryptionService | null,\n) => {\n const where: Partial<User> & { deletedAt?: null } = {\n id: notification.recipientUserId,\n tenantId: notification.tenantId,\n deletedAt: null,\n }\n if (notification.organizationId) {\n where.organizationId = notification.organizationId\n }\n const record = await findOneWithDecryption(\n em,\n User,\n where,\n undefined,\n {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n encryptionService: encryptionService ?? null,\n },\n )\n if (!record) return null\n return {\n email: typeof record.email === 'string' ? record.email : null,\n name: typeof record.name === 'string' ? record.name : null,\n }\n}\n\n\nexport default async function handle(payload: NotificationCreatedPayload, ctx: ResolverContext) {\n debug('deliver notification event', payload)\n const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, { defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG })\n if (!deliveryConfig.strategies.email.enabled) {\n debug('email delivery disabled')\n }\n\n const em = ctx.resolve('em') as EntityManager\n const notification = await em.findOne(Notification, {\n id: payload.notificationId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n if (!notification) {\n debug('notification not found', payload.notificationId)\n return\n }\n\n let encryptionService: TenantDataEncryptionService | null = null\n try {\n encryptionService = ctx.resolve<TenantDataEncryptionService>('tenantEncryptionService')\n } catch {\n encryptionService = null\n }\n\n const recipient = (await resolveRecipient(em, notification, encryptionService)) ?? { email: null, name: null }\n if (!recipient?.email) {\n debug('recipient has no email', notification.recipientUserId)\n }\n const { title, body, t } = await resolveNotificationCopy(notification)\n const panelUrl = resolveNotificationPanelUrl(deliveryConfig)\n if (!panelUrl) {\n debug('missing panelUrl; check appUrl/panelPath settings')\n }\n\n const panelLink = panelUrl ? buildPanelLink(panelUrl, notification.id) : null\n const actionLinks = panelLink\n ? (notification.actionData?.actions ?? []).map((action) => ({\n id: action.id,\n label: action.labelKey ? t(action.labelKey, action.label) : action.label,\n href: panelLink,\n }))\n : []\n\n if (deliveryConfig.strategies.email.enabled && recipient?.email && panelLink) {\n const subjectPrefix = deliveryConfig.strategies.email.subjectPrefix?.trim()\n const subject = subjectPrefix ? `${subjectPrefix} ${title}` : title\n const copy = {\n preview: t('notifications.delivery.email.preview', 'New notification'),\n heading: t('notifications.delivery.email.heading', 'You have a new notification'),\n bodyIntro: t('notifications.delivery.email.bodyIntro', 'Review the notification details and take any required actions.'),\n actionNotice: t('notifications.delivery.email.actionNotice', 'Actions are available in Open Mercato and are read-only in this email.'),\n openCta: t('notifications.delivery.email.openCta', 'Open notification center'),\n footer: t('notifications.delivery.email.footer', 'Open Mercato notifications'),\n }\n\n try {\n debug('sending email', { to: recipient.email, from: deliveryConfig.strategies.email.from, subject })\n await sendEmail({\n to: recipient.email,\n subject,\n from: deliveryConfig.strategies.email.from,\n replyTo: deliveryConfig.strategies.email.replyTo,\n react: NotificationEmail({\n title,\n body,\n actions: actionLinks,\n panelUrl: panelLink,\n copy,\n }),\n })\n } catch (error) {\n console.error('[notifications] email delivery failed', error)\n }\n }\n\n const strategyConfigs = deliveryConfig.strategies.custom ?? {}\n const strategies = getNotificationDeliveryStrategies()\n for (const strategy of strategies) {\n const strategyConfig = strategyConfigs[strategy.id]\n const enabled = strategyConfig?.enabled ?? strategy.defaultEnabled ?? false\n if (!enabled) {\n debug('custom delivery disabled', strategy.id)\n continue\n }\n try {\n await strategy.deliver({\n notification,\n recipient,\n title,\n body,\n panelUrl,\n panelLink,\n actionLinks,\n deliveryConfig,\n config: strategyConfig ?? {},\n resolve: ctx.resolve,\n t,\n })\n } catch (error) {\n console.error(`[notifications] delivery strategy failed (${strategy.id})`, error)\n }\n }\n\n return\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -121,6 +121,12 @@ async function findValidTransitions(em, instance, fromStepId, context) {
121
121
  }
122
122
  async function executeTransition(em, container, instance, fromStepId, toStepId, context) {
123
123
  try {
124
+ let eventBus = null;
125
+ try {
126
+ eventBus = container.resolve("eventBus");
127
+ } catch {
128
+ eventBus = null;
129
+ }
124
130
  const evaluation = await evaluateTransition(
125
131
  em,
126
132
  instance,
@@ -139,7 +145,8 @@ async function executeTransition(em, container, instance, fromStepId, toStepId,
139
145
  em,
140
146
  instance,
141
147
  transition,
142
- context
148
+ context,
149
+ eventBus
143
150
  );
144
151
  if (!preConditionsResult.allowed) {
145
152
  const failedRules = preConditionsResult.executedRules.filter((r) => !r.conditionResult).map((r) => ({
@@ -303,7 +310,8 @@ async function executeTransition(em, container, instance, fromStepId, toStepId,
303
310
  em,
304
311
  instance,
305
312
  transition,
306
- context
313
+ context,
314
+ eventBus
307
315
  );
308
316
  if (!postConditionsResult.allowed) {
309
317
  const failedRules = postConditionsResult.errors?.join(", ") || "Unknown post-condition failure";
@@ -403,7 +411,7 @@ async function evaluateTransitionConditions(em, instance, transition, context) {
403
411
  };
404
412
  }
405
413
  }
406
- async function evaluatePreConditions(em, instance, transition, context) {
414
+ async function evaluatePreConditions(em, instance, transition, context, eventBus) {
407
415
  try {
408
416
  const definition = await em.findOne(WorkflowDefinition, {
409
417
  id: instance.definitionId
@@ -435,7 +443,7 @@ async function evaluatePreConditions(em, instance, transition, context) {
435
443
  organizationId: instance.organizationId,
436
444
  executedBy: context.userId
437
445
  };
438
- const result = await ruleEngine.executeRules(em, ruleContext);
446
+ const result = await ruleEngine.executeRules(em, ruleContext, { eventBus });
439
447
  return result;
440
448
  } catch (error) {
441
449
  console.error("Error evaluating pre-conditions:", error);
@@ -447,7 +455,7 @@ async function evaluatePreConditions(em, instance, transition, context) {
447
455
  };
448
456
  }
449
457
  }
450
- async function evaluatePostConditions(em, instance, transition, context) {
458
+ async function evaluatePostConditions(em, instance, transition, context, eventBus) {
451
459
  try {
452
460
  const definition = await em.findOne(WorkflowDefinition, {
453
461
  id: instance.definitionId
@@ -479,7 +487,7 @@ async function evaluatePostConditions(em, instance, transition, context) {
479
487
  organizationId: instance.organizationId,
480
488
  executedBy: context.userId
481
489
  };
482
- const result = await ruleEngine.executeRules(em, ruleContext);
490
+ const result = await ruleEngine.executeRules(em, ruleContext, { eventBus });
483
491
  return result;
484
492
  } catch (error) {
485
493
  console.error("Error evaluating post-conditions:", error);