@open-mercato/core 0.6.6-develop.6228.1.cf483c68b5 → 0.6.6-develop.6241.1.47c01ad4ed
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +7 -8
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +4 -3
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +7 -8
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +4 -3
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/integrations/lib/state-service.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { getIntegration, type IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\nimport { IntegrationState } from '../data/entities'\n\nexport type ResolvedIntegrationState = {\n isEnabled: boolean\n apiVersion: string | null\n reauthRequired: boolean\n lastHealthStatus: string | null\n lastHealthCheckedAt: Date | null\n lastHealthLatencyMs: number | null\n enabledAt: Date | null\n}\n\nexport function createIntegrationStateService(em: EntityManager) {\n function resolveDefinitionDefaults(integrationId: string): ResolvedIntegrationState {\n const definition = getIntegration(integrationId)\n\n return {\n isEnabled: definition?.defaultState?.isEnabled ?? false,\n apiVersion: null,\n reauthRequired: false,\n lastHealthStatus: null,\n lastHealthCheckedAt: null,\n lastHealthLatencyMs: null,\n enabledAt: null,\n }\n }\n\n return {\n async get(integrationId: string, scope: IntegrationScope): Promise<IntegrationState | null> {\n return findOneWithDecryption(\n em,\n IntegrationState,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async resolveState(integrationId: string, scope: IntegrationScope): Promise<ResolvedIntegrationState> {\n const state = await this.get(integrationId, scope)\n const defaults = resolveDefinitionDefaults(integrationId)\n\n return {\n isEnabled: state?.isEnabled ?? defaults.isEnabled,\n apiVersion: state?.apiVersion ?? null,\n reauthRequired: state?.reauthRequired ?? false,\n lastHealthStatus: state?.lastHealthStatus ?? null,\n lastHealthCheckedAt: state?.lastHealthCheckedAt ?? null,\n lastHealthLatencyMs: state?.lastHealthLatencyMs ?? null,\n enabledAt: state?.enabledAt ?? null,\n }\n },\n\n async isEnabled(integrationId: string, scope: IntegrationScope): Promise<boolean> {\n const state = await this.resolveState(integrationId, scope)\n return state.isEnabled\n },\n\n async upsert(\n integrationId: string,\n input: Partial<\n Pick<\n IntegrationState,\n | 'isEnabled'\n | 'apiVersion'\n | 'reauthRequired'\n | 'lastHealthStatus'\n | 'lastHealthCheckedAt'\n | 'lastHealthLatencyMs'\n | 'enabledAt'\n >\n >,\n scope: IntegrationScope,\n ): Promise<IntegrationState> {\n const current = await this.get(integrationId, scope)\n const resolvedBefore = await this.resolveState(integrationId, scope)\n const enableTransition =\n input.isEnabled === true && resolvedBefore.isEnabled === false\n\n if (current) {\n if (input.isEnabled !== undefined) current.isEnabled = input.isEnabled\n if (input.apiVersion !== undefined) current.apiVersion = input.apiVersion\n if (input.reauthRequired !== undefined) current.reauthRequired = input.reauthRequired\n if (input.lastHealthStatus !== undefined) current.lastHealthStatus = input.lastHealthStatus\n if (input.lastHealthCheckedAt !== undefined) current.lastHealthCheckedAt = input.lastHealthCheckedAt\n if (input.lastHealthLatencyMs !== undefined) current.lastHealthLatencyMs = input.lastHealthLatencyMs\n if (input.enabledAt !== undefined) current.enabledAt = input.enabledAt\n else if (enableTransition) current.enabledAt = new Date()\n await em.flush()\n return current\n }\n\n const created = em.create(IntegrationState, {\n integrationId,\n isEnabled: input.isEnabled ?? false,\n apiVersion: input.apiVersion,\n reauthRequired: input.reauthRequired ?? false,\n lastHealthStatus: input.lastHealthStatus,\n lastHealthCheckedAt: input.lastHealthCheckedAt,\n lastHealthLatencyMs: input.lastHealthLatencyMs,\n enabledAt: input.enabledAt !== undefined ? input.enabledAt : enableTransition ? new Date() : undefined,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persist(created).flush()\n return created\n },\n\n async resolveApiVersion(integrationId: string, scope: IntegrationScope): Promise<string | undefined> {\n const state = await this.get(integrationId, scope)\n return state?.apiVersion ?? undefined\n },\n\n async setReauthRequired(integrationId: string, required: boolean, scope: IntegrationScope): Promise<IntegrationState> {\n return this.upsert(integrationId, { reauthRequired: required }, scope)\n },\n }\n}\n\nexport type IntegrationStateService = ReturnType<typeof createIntegrationStateService>\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,sBAA6C;AACtD,SAAS,wBAAwB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { getIntegration, type IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\nimport { IntegrationState } from '../data/entities'\n\nexport type ResolvedIntegrationState = {\n isEnabled: boolean\n apiVersion: string | null\n reauthRequired: boolean\n lastHealthStatus: string | null\n lastHealthCheckedAt: Date | null\n lastHealthLatencyMs: number | null\n enabledAt: Date | null\n updatedAt: Date | null\n}\n\nexport function createIntegrationStateService(em: EntityManager) {\n function resolveDefinitionDefaults(integrationId: string): ResolvedIntegrationState {\n const definition = getIntegration(integrationId)\n\n return {\n isEnabled: definition?.defaultState?.isEnabled ?? false,\n apiVersion: null,\n reauthRequired: false,\n lastHealthStatus: null,\n lastHealthCheckedAt: null,\n lastHealthLatencyMs: null,\n enabledAt: null,\n updatedAt: null,\n }\n }\n\n return {\n async get(integrationId: string, scope: IntegrationScope): Promise<IntegrationState | null> {\n return findOneWithDecryption(\n em,\n IntegrationState,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async resolveState(integrationId: string, scope: IntegrationScope): Promise<ResolvedIntegrationState> {\n const state = await this.get(integrationId, scope)\n const defaults = resolveDefinitionDefaults(integrationId)\n\n return {\n isEnabled: state?.isEnabled ?? defaults.isEnabled,\n apiVersion: state?.apiVersion ?? null,\n reauthRequired: state?.reauthRequired ?? false,\n lastHealthStatus: state?.lastHealthStatus ?? null,\n lastHealthCheckedAt: state?.lastHealthCheckedAt ?? null,\n lastHealthLatencyMs: state?.lastHealthLatencyMs ?? null,\n enabledAt: state?.enabledAt ?? null,\n updatedAt: state?.updatedAt ?? null,\n }\n },\n\n async isEnabled(integrationId: string, scope: IntegrationScope): Promise<boolean> {\n const state = await this.resolveState(integrationId, scope)\n return state.isEnabled\n },\n\n async upsert(\n integrationId: string,\n input: Partial<\n Pick<\n IntegrationState,\n | 'isEnabled'\n | 'apiVersion'\n | 'reauthRequired'\n | 'lastHealthStatus'\n | 'lastHealthCheckedAt'\n | 'lastHealthLatencyMs'\n | 'enabledAt'\n >\n >,\n scope: IntegrationScope,\n ): Promise<IntegrationState> {\n const current = await this.get(integrationId, scope)\n const resolvedBefore = await this.resolveState(integrationId, scope)\n const enableTransition =\n input.isEnabled === true && resolvedBefore.isEnabled === false\n\n if (current) {\n if (input.isEnabled !== undefined) current.isEnabled = input.isEnabled\n if (input.apiVersion !== undefined) current.apiVersion = input.apiVersion\n if (input.reauthRequired !== undefined) current.reauthRequired = input.reauthRequired\n if (input.lastHealthStatus !== undefined) current.lastHealthStatus = input.lastHealthStatus\n if (input.lastHealthCheckedAt !== undefined) current.lastHealthCheckedAt = input.lastHealthCheckedAt\n if (input.lastHealthLatencyMs !== undefined) current.lastHealthLatencyMs = input.lastHealthLatencyMs\n if (input.enabledAt !== undefined) current.enabledAt = input.enabledAt\n else if (enableTransition) current.enabledAt = new Date()\n await em.flush()\n return current\n }\n\n const created = em.create(IntegrationState, {\n integrationId,\n isEnabled: input.isEnabled ?? false,\n apiVersion: input.apiVersion,\n reauthRequired: input.reauthRequired ?? false,\n lastHealthStatus: input.lastHealthStatus,\n lastHealthCheckedAt: input.lastHealthCheckedAt,\n lastHealthLatencyMs: input.lastHealthLatencyMs,\n enabledAt: input.enabledAt !== undefined ? input.enabledAt : enableTransition ? new Date() : undefined,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persist(created).flush()\n return created\n },\n\n async resolveApiVersion(integrationId: string, scope: IntegrationScope): Promise<string | undefined> {\n const state = await this.get(integrationId, scope)\n return state?.apiVersion ?? undefined\n },\n\n async setReauthRequired(integrationId: string, required: boolean, scope: IntegrationScope): Promise<IntegrationState> {\n return this.upsert(integrationId, { reauthRequired: required }, scope)\n },\n }\n}\n\nexport type IntegrationStateService = ReturnType<typeof createIntegrationStateService>\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sql } from "kysely";
|
|
2
|
-
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
2
|
+
import { CrudHttpError, conflict } from "@open-mercato/shared/lib/crud/errors";
|
|
3
3
|
import { Notification } from "../data/entities.js";
|
|
4
4
|
import { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from "./events.js";
|
|
5
5
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
@@ -308,6 +308,28 @@ function createNotificationService(deps) {
|
|
|
308
308
|
if (!action) {
|
|
309
309
|
throw new Error("Action not found");
|
|
310
310
|
}
|
|
311
|
+
if (notification.status === "actioned") {
|
|
312
|
+
throw conflict("Notification action already executed");
|
|
313
|
+
}
|
|
314
|
+
const actionedAt = /* @__PURE__ */ new Date();
|
|
315
|
+
const previousStatus = notification.status;
|
|
316
|
+
const previousActionedAt = notification.actionedAt ?? null;
|
|
317
|
+
const previousActionTaken = notification.actionTaken ?? null;
|
|
318
|
+
const claimResult = await getDb(em).updateTable("notifications").set({
|
|
319
|
+
status: "actioned",
|
|
320
|
+
actioned_at: actionedAt,
|
|
321
|
+
action_taken: input.actionId
|
|
322
|
+
}).where("id", "=", notification.id).where("recipient_user_id", "=", ctx.userId).where("tenant_id", "=", ctx.tenantId).where("status", "!=", "actioned").executeTakeFirst();
|
|
323
|
+
if (Number(claimResult?.numUpdatedRows ?? 0) === 0) {
|
|
324
|
+
throw conflict("Notification action already executed");
|
|
325
|
+
}
|
|
326
|
+
const releaseClaim = async () => {
|
|
327
|
+
await getDb(em).updateTable("notifications").set({
|
|
328
|
+
status: previousStatus,
|
|
329
|
+
actioned_at: previousActionedAt,
|
|
330
|
+
action_taken: previousActionTaken
|
|
331
|
+
}).where("id", "=", notification.id).where("recipient_user_id", "=", ctx.userId).where("tenant_id", "=", ctx.tenantId).where("status", "=", "actioned").executeTakeFirst();
|
|
332
|
+
};
|
|
311
333
|
let result = null;
|
|
312
334
|
if (action.commandId && commandBus && container) {
|
|
313
335
|
const commandInput = {
|
|
@@ -325,19 +347,29 @@ function createNotificationService(deps) {
|
|
|
325
347
|
selectedOrganizationId: ctx.organizationId ?? null,
|
|
326
348
|
organizationIds: ctx.organizationId ? [ctx.organizationId] : null
|
|
327
349
|
};
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
350
|
+
let commandResult;
|
|
351
|
+
try {
|
|
352
|
+
commandResult = await commandBus.execute(action.commandId, {
|
|
353
|
+
input: commandInput,
|
|
354
|
+
ctx: commandCtx,
|
|
355
|
+
metadata: {
|
|
356
|
+
tenantId: ctx.tenantId,
|
|
357
|
+
organizationId: ctx.organizationId,
|
|
358
|
+
resourceKind: "notifications"
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
} catch (err) {
|
|
362
|
+
try {
|
|
363
|
+
await releaseClaim();
|
|
364
|
+
} catch (releaseErr) {
|
|
365
|
+
debug("failed to release notification action claim", releaseErr);
|
|
335
366
|
}
|
|
336
|
-
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
337
369
|
result = commandResult.result;
|
|
338
370
|
}
|
|
339
371
|
notification.status = "actioned";
|
|
340
|
-
notification.actionedAt =
|
|
372
|
+
notification.actionedAt = actionedAt;
|
|
341
373
|
notification.actionTaken = input.actionId;
|
|
342
374
|
notification.actionResult = result;
|
|
343
375
|
if (!notification.readAt) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/notifications/lib/notificationService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { Notification, type NotificationStatus } from '../data/entities'\nimport type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'\nimport type { NotificationPollData } from '@open-mercato/shared/modules/notifications/types'\nimport { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from './events'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildNotificationEntity,\n emitNotificationCreated,\n emitNotificationCreatedBatch,\n type NotificationContentInput,\n type NotificationTenantContext,\n} from './notificationFactory'\nimport { toNotificationDto } from './notificationMapper'\nimport { getRecipientUserIdsForFeature, getRecipientUserIdsForRole } from './notificationRecipients'\nimport { assertSafeNotificationHref, sanitizeNotificationActions } from './safeHref'\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\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\nconst UNIQUE_NOTIFICATION_ACTIVE_STATUSES: NotificationStatus[] = ['unread', 'read', 'actioned']\n\nfunction normalizeOrgScope(organizationId: string | null | undefined): string | null {\n return organizationId ?? null\n}\n\nfunction applyNotificationContent(\n notification: Notification,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n) {\n const actions = sanitizeNotificationActions(input.actions)\n const linkHref = assertSafeNotificationHref(input.linkHref)\n\n notification.recipientUserId = recipientUserId\n notification.type = input.type\n notification.titleKey = input.titleKey\n notification.bodyKey = input.bodyKey\n notification.titleVariables = input.titleVariables\n notification.bodyVariables = input.bodyVariables\n notification.title = input.title || input.titleKey || ''\n notification.body = input.body\n notification.icon = input.icon\n notification.severity = input.severity ?? 'info'\n notification.actionData = actions\n ? {\n actions,\n primaryActionId: input.primaryActionId,\n }\n : null\n notification.sourceModule = input.sourceModule\n notification.sourceEntityType = input.sourceEntityType\n notification.sourceEntityId = input.sourceEntityId\n notification.linkHref = linkHref\n notification.groupKey = input.groupKey\n notification.expiresAt = input.expiresAt ? new Date(input.expiresAt) : null\n notification.tenantId = ctx.tenantId\n notification.organizationId = normalizeOrgScope(ctx.organizationId)\n notification.status = 'unread'\n notification.readAt = null\n notification.actionedAt = null\n notification.dismissedAt = null\n notification.actionTaken = null\n notification.actionResult = null\n notification.createdAt = new Date()\n}\n\nasync function findScopedNotificationOrThrow(\n em: EntityManager,\n notificationId: string,\n ctx: NotificationServiceContext,\n): Promise<Notification> {\n const notification = await findOneWithDecryption(\n em,\n Notification,\n {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n },\n undefined,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!notification) {\n throw new CrudHttpError(404, { error: 'Notification not found' })\n }\n return notification\n}\n\nasync function emitNotificationSseEvents(\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> },\n notifications: Notification[],\n ctx: NotificationServiceContext,\n recipientUserIds: string[],\n): Promise<void> {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.BATCH_CREATED, {\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n recipientUserIds,\n count: notifications.length,\n })\n\n for (const notification of notifications) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n}\n\nasync function createOrRefreshNotification(\n em: EntityManager,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n): Promise<Notification> {\n if (input.groupKey && input.groupKey.trim().length > 0) {\n const orgScope = normalizeOrgScope(ctx.organizationId) ?? 'global'\n const lockKey = `notifications:${ctx.tenantId}:${orgScope}:${recipientUserId}:${input.type}:${input.groupKey}`\n try {\n const db = getDb(em)\n await sql`select pg_advisory_xact_lock(hashtext(${lockKey}))`.execute(db)\n } catch {\n // If advisory locks are unavailable, continue with best-effort dedupe.\n }\n\n const existing = await em.findOne(Notification, {\n recipientUserId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n type: input.type,\n groupKey: input.groupKey,\n status: { $in: UNIQUE_NOTIFICATION_ACTIVE_STATUSES },\n }, {\n orderBy: { createdAt: 'desc' },\n })\n\n if (existing) {\n applyNotificationContent(existing, input, recipientUserId, ctx)\n return existing\n }\n }\n\n return buildNotificationEntity(em, input, recipientUserId, ctx)\n}\n\nexport interface NotificationServiceContext {\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}\n\nexport interface NotificationService {\n create(input: CreateNotificationInput, ctx: NotificationServiceContext): Promise<Notification>\n createBatch(input: CreateBatchNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForRole(input: CreateRoleNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForFeature(input: CreateFeatureNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n markAsRead(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n markAllAsRead(ctx: NotificationServiceContext): Promise<number>\n dismiss(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n restoreDismissed(\n notificationId: string,\n status: 'read' | 'unread' | undefined,\n ctx: NotificationServiceContext\n ): Promise<Notification>\n executeAction(\n notificationId: string,\n input: ExecuteActionInput,\n ctx: NotificationServiceContext\n ): Promise<{ notification: Notification; result: unknown }>\n getUnreadCount(ctx: NotificationServiceContext): Promise<number>\n getPollData(ctx: NotificationServiceContext, since?: string): Promise<NotificationPollData>\n cleanupExpired(): Promise<number>\n deleteBySource(\n sourceEntityType: string,\n sourceEntityId: string,\n ctx: NotificationServiceContext\n ): Promise<number>\n}\n\nexport interface NotificationServiceDeps {\n em: EntityManager\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> }\n commandBus?: {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown }>\n }\n container?: { resolve: (name: string) => unknown }\n}\n\nexport function createNotificationService(deps: NotificationServiceDeps): NotificationService {\n const { em: rootEm, eventBus, commandBus, container } = deps\n\n return {\n async create(input, ctx) {\n const { recipientUserId, ...content } = input\n const writeEm = rootEm.fork()\n const notification = await writeEm.transactional(async (tx) => {\n const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx)\n await tx.flush()\n return entity\n })\n\n await emitNotificationCreated(eventBus, notification, ctx)\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n\n return notification\n },\n\n async createBatch(input, ctx) {\n const recipientUserIds = Array.from(new Set(input.recipientUserIds))\n const { recipientUserIds: _recipientUserIds, ...content } = input\n const notifications: Notification[] = []\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const recipientUserId of recipientUserIds) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx, recipientUserIds)\n\n return notifications\n },\n\n async createForRole(input, ctx) {\n const em = rootEm.fork()\n\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForRole(db, ctx.tenantId, input.roleId)\n if (recipientUserIds.length === 0) {\n return []\n }\n\n const { roleId: _roleId, ...content } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const recipientUserId of uniqueRecipientUserIds) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx, uniqueRecipientUserIds)\n\n return notifications\n },\n\n async createForFeature(input, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForFeature(db, ctx.tenantId, input.requiredFeature)\n\n if (recipientUserIds.length === 0) {\n debug('No users found with feature:', input.requiredFeature, 'in tenant:', ctx.tenantId)\n return []\n }\n\n debug('Creating notifications for', recipientUserIds.length, 'user(s) with feature:', input.requiredFeature)\n\n const { requiredFeature: _requiredFeature, ...content } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const recipientUserId of uniqueRecipientUserIds) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx, uniqueRecipientUserIds)\n\n return notifications\n },\n\n async markAsRead(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status === 'unread') {\n notification.status = 'read'\n notification.readAt = new Date()\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n }\n\n return notification\n },\n\n async markAllAsRead(ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const applyScope = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '=', 'unread')\n if (ctx.organizationId) {\n chain = chain.where('organization_id' as any, '=', ctx.organizationId)\n }\n return chain\n }\n\n const targetRows = await applyScope(\n db\n .selectFrom('notifications' as any)\n .select([\n 'id' as any,\n 'organization_id' as any,\n 'recipient_user_id' as any,\n ]),\n ).execute() as Array<{ id: string }>\n\n if (!targetRows.length) {\n return 0\n }\n\n const updateResult = await applyScope(\n db.updateTable('notifications' as any).set({\n status: 'read',\n read_at: sql`now()`,\n } as any) as any,\n ).executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n const result = Number(updateResult?.numUpdatedRows ?? targetRows.length)\n\n const notifications = await findWithDecryption(em, Notification, {\n id: { $in: targetRows.map((row) => row.id) },\n }, undefined, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n })\n\n for (const notification of notifications) {\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return result\n },\n\n async dismiss(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n notification.status = 'dismissed'\n notification.dismissedAt = new Date()\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.DISMISSED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return notification\n },\n\n async restoreDismissed(notificationId, status, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status !== 'dismissed') {\n return notification\n }\n\n const targetStatus = status ?? 'read'\n notification.status = targetStatus\n notification.dismissedAt = null\n\n if (targetStatus === 'unread') {\n notification.readAt = null\n } else if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.RESTORED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n status: targetStatus,\n })\n\n return notification\n },\n\n async executeAction(notificationId, input, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n const actionData = notification.actionData\n const action = actionData?.actions?.find((a) => a.id === input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n let result: unknown = null\n\n if (action.commandId && commandBus && container) {\n const commandInput = {\n id: notification.sourceEntityId,\n ...input.payload,\n }\n\n // Build a CommandRuntimeContext from the notification service context\n const commandCtx = {\n container,\n auth: {\n sub: ctx.userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: ctx.organizationId ?? null,\n organizationIds: ctx.organizationId ? [ctx.organizationId] : null,\n }\n\n const commandResult = await commandBus.execute(action.commandId, {\n input: commandInput,\n ctx: commandCtx,\n metadata: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n resourceKind: 'notifications',\n },\n })\n\n result = commandResult.result\n }\n\n notification.status = 'actioned'\n notification.actionedAt = new Date()\n notification.actionTaken = input.actionId\n notification.actionResult = result as Record<string, unknown>\n\n if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.ACTIONED, {\n notificationId: notification.id,\n actionId: input.actionId,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return { notification, result }\n },\n\n async getUnreadCount(ctx) {\n const em = rootEm.fork()\n return em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n })\n },\n\n async getPollData(ctx, since) {\n const em = rootEm.fork()\n const filters: Record<string, unknown> = {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n }\n\n if (since) {\n filters.createdAt = { $gt: new Date(since) }\n }\n\n const [notifications, unreadCount] = await Promise.all([\n em.find(Notification, filters, {\n orderBy: { createdAt: 'desc' },\n limit: 50,\n }),\n em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n }),\n ])\n\n const recent = notifications.map(toNotificationDto)\n const hasNew = since ? recent.length > 0 : false\n\n return {\n unreadCount,\n recent,\n hasNew,\n lastId: recent[0]?.id,\n }\n },\n\n async cleanupExpired() {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const updateResult = await db\n .updateTable('notifications' as any)\n .set({\n status: 'dismissed',\n dismissed_at: sql`now()`,\n } as any)\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n\n return Number(updateResult?.numUpdatedRows ?? 0)\n },\n\n async deleteBySource(sourceEntityType, sourceEntityId, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const deleteResult = await db\n .deleteFrom('notifications' as any)\n .where('source_entity_type' as any, '=', sourceEntityType)\n .where('source_entity_id' as any, '=', sourceEntityId)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .executeTakeFirst() as { numDeletedRows?: bigint | number } | undefined\n\n return Number(deleteResult?.numDeletedRows ?? 0)\n },\n }\n}\n\n/**\n * Helper to create notification service from a DI container.\n * Use this in API routes and commands to avoid DI resolution issues.\n */\nexport function resolveNotificationService(container: {\n resolve: (name: string) => unknown\n}): NotificationService {\n const em = container.resolve('em') as EntityManager\n const eventBus = container.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n\n // commandBus may not be registered in all contexts, so resolve it safely\n let commandBus: NotificationServiceDeps['commandBus']\n try {\n commandBus = container.resolve('commandBus') as typeof commandBus\n } catch {\n // commandBus not available - actions with commandId won't work\n commandBus = undefined\n }\n\n return createNotificationService({ em, eventBus, commandBus, container })\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { CrudHttpError, conflict } from '@open-mercato/shared/lib/crud/errors'\nimport { Notification, type NotificationStatus } from '../data/entities'\nimport type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'\nimport type { NotificationPollData } from '@open-mercato/shared/modules/notifications/types'\nimport { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from './events'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildNotificationEntity,\n emitNotificationCreated,\n emitNotificationCreatedBatch,\n type NotificationContentInput,\n type NotificationTenantContext,\n} from './notificationFactory'\nimport { toNotificationDto } from './notificationMapper'\nimport { getRecipientUserIdsForFeature, getRecipientUserIdsForRole } from './notificationRecipients'\nimport { assertSafeNotificationHref, sanitizeNotificationActions } from './safeHref'\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\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\nconst UNIQUE_NOTIFICATION_ACTIVE_STATUSES: NotificationStatus[] = ['unread', 'read', 'actioned']\n\nfunction normalizeOrgScope(organizationId: string | null | undefined): string | null {\n return organizationId ?? null\n}\n\nfunction applyNotificationContent(\n notification: Notification,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n) {\n const actions = sanitizeNotificationActions(input.actions)\n const linkHref = assertSafeNotificationHref(input.linkHref)\n\n notification.recipientUserId = recipientUserId\n notification.type = input.type\n notification.titleKey = input.titleKey\n notification.bodyKey = input.bodyKey\n notification.titleVariables = input.titleVariables\n notification.bodyVariables = input.bodyVariables\n notification.title = input.title || input.titleKey || ''\n notification.body = input.body\n notification.icon = input.icon\n notification.severity = input.severity ?? 'info'\n notification.actionData = actions\n ? {\n actions,\n primaryActionId: input.primaryActionId,\n }\n : null\n notification.sourceModule = input.sourceModule\n notification.sourceEntityType = input.sourceEntityType\n notification.sourceEntityId = input.sourceEntityId\n notification.linkHref = linkHref\n notification.groupKey = input.groupKey\n notification.expiresAt = input.expiresAt ? new Date(input.expiresAt) : null\n notification.tenantId = ctx.tenantId\n notification.organizationId = normalizeOrgScope(ctx.organizationId)\n notification.status = 'unread'\n notification.readAt = null\n notification.actionedAt = null\n notification.dismissedAt = null\n notification.actionTaken = null\n notification.actionResult = null\n notification.createdAt = new Date()\n}\n\nasync function findScopedNotificationOrThrow(\n em: EntityManager,\n notificationId: string,\n ctx: NotificationServiceContext,\n): Promise<Notification> {\n const notification = await findOneWithDecryption(\n em,\n Notification,\n {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n },\n undefined,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!notification) {\n throw new CrudHttpError(404, { error: 'Notification not found' })\n }\n return notification\n}\n\nasync function emitNotificationSseEvents(\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> },\n notifications: Notification[],\n ctx: NotificationServiceContext,\n recipientUserIds: string[],\n): Promise<void> {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.BATCH_CREATED, {\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n recipientUserIds,\n count: notifications.length,\n })\n\n for (const notification of notifications) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n}\n\nasync function createOrRefreshNotification(\n em: EntityManager,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n): Promise<Notification> {\n if (input.groupKey && input.groupKey.trim().length > 0) {\n const orgScope = normalizeOrgScope(ctx.organizationId) ?? 'global'\n const lockKey = `notifications:${ctx.tenantId}:${orgScope}:${recipientUserId}:${input.type}:${input.groupKey}`\n try {\n const db = getDb(em)\n await sql`select pg_advisory_xact_lock(hashtext(${lockKey}))`.execute(db)\n } catch {\n // If advisory locks are unavailable, continue with best-effort dedupe.\n }\n\n const existing = await em.findOne(Notification, {\n recipientUserId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n type: input.type,\n groupKey: input.groupKey,\n status: { $in: UNIQUE_NOTIFICATION_ACTIVE_STATUSES },\n }, {\n orderBy: { createdAt: 'desc' },\n })\n\n if (existing) {\n applyNotificationContent(existing, input, recipientUserId, ctx)\n return existing\n }\n }\n\n return buildNotificationEntity(em, input, recipientUserId, ctx)\n}\n\nexport interface NotificationServiceContext {\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}\n\nexport interface NotificationService {\n create(input: CreateNotificationInput, ctx: NotificationServiceContext): Promise<Notification>\n createBatch(input: CreateBatchNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForRole(input: CreateRoleNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForFeature(input: CreateFeatureNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n markAsRead(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n markAllAsRead(ctx: NotificationServiceContext): Promise<number>\n dismiss(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n restoreDismissed(\n notificationId: string,\n status: 'read' | 'unread' | undefined,\n ctx: NotificationServiceContext\n ): Promise<Notification>\n executeAction(\n notificationId: string,\n input: ExecuteActionInput,\n ctx: NotificationServiceContext\n ): Promise<{ notification: Notification; result: unknown }>\n getUnreadCount(ctx: NotificationServiceContext): Promise<number>\n getPollData(ctx: NotificationServiceContext, since?: string): Promise<NotificationPollData>\n cleanupExpired(): Promise<number>\n deleteBySource(\n sourceEntityType: string,\n sourceEntityId: string,\n ctx: NotificationServiceContext\n ): Promise<number>\n}\n\nexport interface NotificationServiceDeps {\n em: EntityManager\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> }\n commandBus?: {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown }>\n }\n container?: { resolve: (name: string) => unknown }\n}\n\nexport function createNotificationService(deps: NotificationServiceDeps): NotificationService {\n const { em: rootEm, eventBus, commandBus, container } = deps\n\n return {\n async create(input, ctx) {\n const { recipientUserId, ...content } = input\n const writeEm = rootEm.fork()\n const notification = await writeEm.transactional(async (tx) => {\n const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx)\n await tx.flush()\n return entity\n })\n\n await emitNotificationCreated(eventBus, notification, ctx)\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n\n return notification\n },\n\n async createBatch(input, ctx) {\n const recipientUserIds = Array.from(new Set(input.recipientUserIds))\n const { recipientUserIds: _recipientUserIds, ...content } = input\n const notifications: Notification[] = []\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const recipientUserId of recipientUserIds) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx, recipientUserIds)\n\n return notifications\n },\n\n async createForRole(input, ctx) {\n const em = rootEm.fork()\n\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForRole(db, ctx.tenantId, input.roleId)\n if (recipientUserIds.length === 0) {\n return []\n }\n\n const { roleId: _roleId, ...content } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const recipientUserId of uniqueRecipientUserIds) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx, uniqueRecipientUserIds)\n\n return notifications\n },\n\n async createForFeature(input, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForFeature(db, ctx.tenantId, input.requiredFeature)\n\n if (recipientUserIds.length === 0) {\n debug('No users found with feature:', input.requiredFeature, 'in tenant:', ctx.tenantId)\n return []\n }\n\n debug('Creating notifications for', recipientUserIds.length, 'user(s) with feature:', input.requiredFeature)\n\n const { requiredFeature: _requiredFeature, ...content } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const recipientUserId of uniqueRecipientUserIds) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx, uniqueRecipientUserIds)\n\n return notifications\n },\n\n async markAsRead(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status === 'unread') {\n notification.status = 'read'\n notification.readAt = new Date()\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n }\n\n return notification\n },\n\n async markAllAsRead(ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const applyScope = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '=', 'unread')\n if (ctx.organizationId) {\n chain = chain.where('organization_id' as any, '=', ctx.organizationId)\n }\n return chain\n }\n\n const targetRows = await applyScope(\n db\n .selectFrom('notifications' as any)\n .select([\n 'id' as any,\n 'organization_id' as any,\n 'recipient_user_id' as any,\n ]),\n ).execute() as Array<{ id: string }>\n\n if (!targetRows.length) {\n return 0\n }\n\n const updateResult = await applyScope(\n db.updateTable('notifications' as any).set({\n status: 'read',\n read_at: sql`now()`,\n } as any) as any,\n ).executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n const result = Number(updateResult?.numUpdatedRows ?? targetRows.length)\n\n const notifications = await findWithDecryption(em, Notification, {\n id: { $in: targetRows.map((row) => row.id) },\n }, undefined, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n })\n\n for (const notification of notifications) {\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return result\n },\n\n async dismiss(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n notification.status = 'dismissed'\n notification.dismissedAt = new Date()\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.DISMISSED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return notification\n },\n\n async restoreDismissed(notificationId, status, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status !== 'dismissed') {\n return notification\n }\n\n const targetStatus = status ?? 'read'\n notification.status = targetStatus\n notification.dismissedAt = null\n\n if (targetStatus === 'unread') {\n notification.readAt = null\n } else if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.RESTORED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n status: targetStatus,\n })\n\n return notification\n },\n\n async executeAction(notificationId, input, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n const actionData = notification.actionData\n const action = actionData?.actions?.find((a) => a.id === input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n // Reject an already-actioned notification before dispatching the command,\n // so a retry or double-click cannot re-run the side effect.\n if (notification.status === 'actioned') {\n throw conflict('Notification action already executed')\n }\n\n const actionedAt = new Date()\n const previousStatus = notification.status\n const previousActionedAt = notification.actionedAt ?? null\n const previousActionTaken = notification.actionTaken ?? null\n\n // Atomically claim the notification so only one concurrent request can run\n // the side-effecting command. The conditional UPDATE matches only while the\n // notification has not been actioned yet; a losing request updates 0 rows.\n const claimResult = (await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: 'actioned',\n actioned_at: actionedAt,\n action_taken: input.actionId,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '!=', 'actioned')\n .executeTakeFirst()) as { numUpdatedRows?: bigint | number } | undefined\n\n if (Number(claimResult?.numUpdatedRows ?? 0) === 0) {\n throw conflict('Notification action already executed')\n }\n\n // The claim is provisional: if the side-effecting command fails, the action\n // never actually completed, so release the claim to its prior state. This\n // lets the user retry the action instead of the notification being locked as\n // `actioned` forever. Only release while we still own the claim\n // (status = 'actioned'), so a concurrent winner's state is never clobbered.\n const releaseClaim = async () => {\n await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: previousStatus,\n actioned_at: previousActionedAt,\n action_taken: previousActionTaken,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '=', 'actioned')\n .executeTakeFirst()\n }\n\n let result: unknown = null\n\n if (action.commandId && commandBus && container) {\n const commandInput = {\n id: notification.sourceEntityId,\n ...input.payload,\n }\n\n // Build a CommandRuntimeContext from the notification service context\n const commandCtx = {\n container,\n auth: {\n sub: ctx.userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: ctx.organizationId ?? null,\n organizationIds: ctx.organizationId ? [ctx.organizationId] : null,\n }\n\n let commandResult: { result: unknown }\n try {\n commandResult = await commandBus.execute(action.commandId, {\n input: commandInput,\n ctx: commandCtx,\n metadata: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n resourceKind: 'notifications',\n },\n })\n } catch (err) {\n // Never let a rollback failure mask the original command error \u2014 the\n // caller needs the real failure to decide whether to retry.\n try {\n await releaseClaim()\n } catch (releaseErr) {\n debug('failed to release notification action claim', releaseErr)\n }\n throw err\n }\n\n result = commandResult.result\n }\n\n notification.status = 'actioned'\n notification.actionedAt = actionedAt\n notification.actionTaken = input.actionId\n notification.actionResult = result as Record<string, unknown>\n\n if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.ACTIONED, {\n notificationId: notification.id,\n actionId: input.actionId,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return { notification, result }\n },\n\n async getUnreadCount(ctx) {\n const em = rootEm.fork()\n return em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n })\n },\n\n async getPollData(ctx, since) {\n const em = rootEm.fork()\n const filters: Record<string, unknown> = {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n }\n\n if (since) {\n filters.createdAt = { $gt: new Date(since) }\n }\n\n const [notifications, unreadCount] = await Promise.all([\n em.find(Notification, filters, {\n orderBy: { createdAt: 'desc' },\n limit: 50,\n }),\n em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n }),\n ])\n\n const recent = notifications.map(toNotificationDto)\n const hasNew = since ? recent.length > 0 : false\n\n return {\n unreadCount,\n recent,\n hasNew,\n lastId: recent[0]?.id,\n }\n },\n\n async cleanupExpired() {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const updateResult = await db\n .updateTable('notifications' as any)\n .set({\n status: 'dismissed',\n dismissed_at: sql`now()`,\n } as any)\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n\n return Number(updateResult?.numUpdatedRows ?? 0)\n },\n\n async deleteBySource(sourceEntityType, sourceEntityId, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const deleteResult = await db\n .deleteFrom('notifications' as any)\n .where('source_entity_type' as any, '=', sourceEntityType)\n .where('source_entity_id' as any, '=', sourceEntityId)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .executeTakeFirst() as { numDeletedRows?: bigint | number } | undefined\n\n return Number(deleteResult?.numDeletedRows ?? 0)\n },\n }\n}\n\n/**\n * Helper to create notification service from a DI container.\n * Use this in API routes and commands to avoid DI resolution issues.\n */\nexport function resolveNotificationService(container: {\n resolve: (name: string) => unknown\n}): NotificationService {\n const em = container.resolve('em') as EntityManager\n const eventBus = container.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n\n // commandBus may not be registered in all contexts, so resolve it safely\n let commandBus: NotificationServiceDeps['commandBus']\n try {\n commandBus = container.resolve('commandBus') as typeof commandBus\n } catch {\n // commandBus not available - actions with commandId won't work\n commandBus = undefined\n }\n\n return createNotificationService({ em, eventBus, commandBus, container })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
3
3
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
|
+
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
5
|
+
import {
|
|
6
|
+
runCrudMutationGuardAfterSuccess,
|
|
7
|
+
validateCrudMutationGuard
|
|
8
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
4
9
|
import { getStatusSchema } from "../../data/validators.js";
|
|
5
10
|
import { paymentGatewaysTag } from "../openapi.js";
|
|
11
|
+
const gatewayTransactionResourceKind = "payment_gateways:gateway_transaction";
|
|
6
12
|
const metadata = {
|
|
7
13
|
path: "/payment_gateways/status",
|
|
8
|
-
GET: { requireAuth: true, requireFeatures: ["payment_gateways.view"] }
|
|
14
|
+
GET: { requireAuth: true, requireFeatures: ["payment_gateways.view"] },
|
|
15
|
+
POST: { requireAuth: true, requireFeatures: ["payment_gateways.manage"] }
|
|
9
16
|
};
|
|
10
17
|
async function GET(req) {
|
|
11
18
|
const auth = await getAuthFromRequest(req);
|
|
@@ -29,7 +36,73 @@ async function GET(req) {
|
|
|
29
36
|
if (!transaction) {
|
|
30
37
|
return NextResponse.json({ error: "Transaction not found" }, { status: 404 });
|
|
31
38
|
}
|
|
39
|
+
return NextResponse.json({
|
|
40
|
+
transactionId: transaction.id,
|
|
41
|
+
paymentId: transaction.paymentId,
|
|
42
|
+
providerKey: transaction.providerKey,
|
|
43
|
+
sessionId: transaction.providerSessionId,
|
|
44
|
+
status: transaction.unifiedStatus,
|
|
45
|
+
gatewayStatus: transaction.gatewayStatus,
|
|
46
|
+
amount: Number(transaction.amount),
|
|
47
|
+
amountReceived: null,
|
|
48
|
+
currencyCode: transaction.currencyCode,
|
|
49
|
+
redirectUrl: transaction.redirectUrl,
|
|
50
|
+
createdAt: transaction.createdAt.toISOString(),
|
|
51
|
+
updatedAt: transaction.updatedAt.toISOString()
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const message = err instanceof Error ? err.message : "Failed to get status";
|
|
55
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function POST(req) {
|
|
59
|
+
const auth = await getAuthFromRequest(req);
|
|
60
|
+
if (!auth?.tenantId || !auth.orgId) {
|
|
61
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
62
|
+
}
|
|
63
|
+
const payload = await readJsonSafe(req);
|
|
64
|
+
const parsed = getStatusSchema.safeParse(payload);
|
|
65
|
+
if (!parsed.success) {
|
|
66
|
+
return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 422 });
|
|
67
|
+
}
|
|
68
|
+
const container = await createRequestContainer();
|
|
69
|
+
const service = container.resolve("paymentGatewayService");
|
|
70
|
+
const scope = { organizationId: auth.orgId, tenantId: auth.tenantId };
|
|
71
|
+
const actorUserId = auth.userId ?? auth.sub;
|
|
72
|
+
const { transactionId } = parsed.data;
|
|
73
|
+
try {
|
|
74
|
+
const transaction = await service.findTransaction(transactionId, scope);
|
|
75
|
+
if (!transaction) {
|
|
76
|
+
return NextResponse.json({ error: "Transaction not found" }, { status: 404 });
|
|
77
|
+
}
|
|
78
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
79
|
+
tenantId: auth.tenantId,
|
|
80
|
+
organizationId: scope.organizationId,
|
|
81
|
+
userId: actorUserId,
|
|
82
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
83
|
+
resourceId: transactionId,
|
|
84
|
+
operation: "custom",
|
|
85
|
+
requestMethod: req.method,
|
|
86
|
+
requestHeaders: req.headers,
|
|
87
|
+
mutationPayload: parsed.data
|
|
88
|
+
});
|
|
89
|
+
if (guardResult && !guardResult.ok) {
|
|
90
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
91
|
+
}
|
|
32
92
|
const status = await service.getPaymentStatus(transactionId, scope);
|
|
93
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
94
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
95
|
+
tenantId: auth.tenantId,
|
|
96
|
+
organizationId: scope.organizationId,
|
|
97
|
+
userId: actorUserId,
|
|
98
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
99
|
+
resourceId: transactionId,
|
|
100
|
+
operation: "custom",
|
|
101
|
+
requestMethod: req.method,
|
|
102
|
+
requestHeaders: req.headers,
|
|
103
|
+
metadata: guardResult.metadata ?? null
|
|
104
|
+
});
|
|
105
|
+
}
|
|
33
106
|
return NextResponse.json({
|
|
34
107
|
transactionId: transaction.id,
|
|
35
108
|
paymentId: transaction.paymentId,
|
|
@@ -45,27 +118,39 @@ async function GET(req) {
|
|
|
45
118
|
updatedAt: transaction.updatedAt.toISOString()
|
|
46
119
|
});
|
|
47
120
|
} catch (err) {
|
|
48
|
-
const message = err instanceof Error ? err.message : "Failed to
|
|
49
|
-
return NextResponse.json({ error: message }, { status:
|
|
121
|
+
const message = err instanceof Error ? err.message : "Failed to refresh status";
|
|
122
|
+
return NextResponse.json({ error: message }, { status: 502 });
|
|
50
123
|
}
|
|
51
124
|
}
|
|
52
125
|
const openApi = {
|
|
53
126
|
tags: [paymentGatewaysTag],
|
|
54
|
-
summary: "
|
|
127
|
+
summary: "Payment transaction status",
|
|
55
128
|
methods: {
|
|
56
129
|
GET: {
|
|
57
|
-
summary: "Get transaction status",
|
|
130
|
+
summary: "Get stored transaction status",
|
|
58
131
|
tags: [paymentGatewaysTag],
|
|
59
132
|
responses: [
|
|
60
|
-
{ status: 200, description: "
|
|
133
|
+
{ status: 200, description: "Stored transaction status" },
|
|
61
134
|
{ status: 400, description: "Missing or malformed transactionId" },
|
|
62
135
|
{ status: 404, description: "Transaction not found" }
|
|
63
136
|
]
|
|
137
|
+
},
|
|
138
|
+
POST: {
|
|
139
|
+
summary: "Refresh transaction status from the provider",
|
|
140
|
+
tags: [paymentGatewaysTag],
|
|
141
|
+
responses: [
|
|
142
|
+
{ status: 200, description: "Refreshed transaction status" },
|
|
143
|
+
{ status: 401, description: "Unauthorized" },
|
|
144
|
+
{ status: 404, description: "Transaction not found" },
|
|
145
|
+
{ status: 422, description: "Missing or malformed payload" },
|
|
146
|
+
{ status: 502, description: "Gateway provider error" }
|
|
147
|
+
]
|
|
64
148
|
}
|
|
65
149
|
}
|
|
66
150
|
};
|
|
67
151
|
export {
|
|
68
152
|
GET,
|
|
153
|
+
POST,
|
|
69
154
|
metadata,
|
|
70
155
|
openApi
|
|
71
156
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/payment_gateways/api/status/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getStatusSchema } from '../../data/validators'\nimport type { PaymentGatewayService } from '../../lib/gateway-service'\nimport { paymentGatewaysTag } from '../openapi'\n\nexport const metadata = {\n path: '/payment_gateways/status',\n GET: { requireAuth: true, requireFeatures: ['payment_gateways.view'] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const transactionId = url.searchParams.get('transactionId')\n if (!transactionId) {\n return NextResponse.json({ error: 'transactionId is required' }, { status: 400 })\n }\n const parsedTransactionId = getStatusSchema.safeParse({ transactionId })\n if (!parsedTransactionId.success) {\n return NextResponse.json({ error: 'Invalid transactionId', details: parsedTransactionId.error.flatten() }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const service = container.resolve('paymentGatewayService') as PaymentGatewayService\n\n try {\n const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }\n const transaction = await service.findTransaction(transactionId, scope)\n if (!transaction) {\n return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })\n }\n\n const status = await service.getPaymentStatus(transactionId, scope)\n\n return NextResponse.json({\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n sessionId: transaction.providerSessionId,\n status: status.status,\n gatewayStatus: transaction.gatewayStatus,\n amount: status.amount,\n amountReceived: status.amountReceived,\n currencyCode: status.currencyCode,\n redirectUrl: transaction.redirectUrl,\n createdAt: transaction.createdAt.toISOString(),\n updatedAt: transaction.updatedAt.toISOString(),\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB;AAEhC,SAAS,0BAA0B;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { getStatusSchema } from '../../data/validators'\nimport type { PaymentGatewayService } from '../../lib/gateway-service'\nimport { paymentGatewaysTag } from '../openapi'\n\nconst gatewayTransactionResourceKind = 'payment_gateways:gateway_transaction'\n\nexport const metadata = {\n path: '/payment_gateways/status',\n GET: { requireAuth: true, requireFeatures: ['payment_gateways.view'] },\n POST: { requireAuth: true, requireFeatures: ['payment_gateways.manage'] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const transactionId = url.searchParams.get('transactionId')\n if (!transactionId) {\n return NextResponse.json({ error: 'transactionId is required' }, { status: 400 })\n }\n const parsedTransactionId = getStatusSchema.safeParse({ transactionId })\n if (!parsedTransactionId.success) {\n return NextResponse.json({ error: 'Invalid transactionId', details: parsedTransactionId.error.flatten() }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const service = container.resolve('paymentGatewayService') as PaymentGatewayService\n\n try {\n const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }\n const transaction = await service.findTransaction(transactionId, scope)\n if (!transaction) {\n return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n sessionId: transaction.providerSessionId,\n status: transaction.unifiedStatus,\n gatewayStatus: transaction.gatewayStatus,\n amount: Number(transaction.amount),\n amountReceived: null,\n currencyCode: transaction.currencyCode,\n redirectUrl: transaction.redirectUrl,\n createdAt: transaction.createdAt.toISOString(),\n updatedAt: transaction.updatedAt.toISOString(),\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to get status'\n return NextResponse.json({ error: message }, { status: 500 })\n }\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const payload = await readJsonSafe<unknown>(req)\n const parsed = getStatusSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const service = container.resolve('paymentGatewayService') as PaymentGatewayService\n const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }\n const actorUserId = auth.userId ?? auth.sub\n const { transactionId } = parsed.data\n\n try {\n const transaction = await service.findTransaction(transactionId, scope)\n if (!transaction) {\n return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: actorUserId,\n resourceKind: gatewayTransactionResourceKind,\n resourceId: transactionId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const status = await service.getPaymentStatus(transactionId, scope)\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: actorUserId,\n resourceKind: gatewayTransactionResourceKind,\n resourceId: transactionId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n sessionId: transaction.providerSessionId,\n status: status.status,\n gatewayStatus: transaction.gatewayStatus,\n amount: status.amount,\n amountReceived: status.amountReceived,\n currencyCode: status.currencyCode,\n redirectUrl: transaction.redirectUrl,\n createdAt: transaction.createdAt.toISOString(),\n updatedAt: transaction.updatedAt.toISOString(),\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to refresh status'\n return NextResponse.json({ error: message }, { status: 502 })\n }\n}\n\nexport const openApi = {\n tags: [paymentGatewaysTag],\n summary: 'Payment transaction status',\n methods: {\n GET: {\n summary: 'Get stored transaction status',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 200, description: 'Stored transaction status' },\n { status: 400, description: 'Missing or malformed transactionId' },\n { status: 404, description: 'Transaction not found' },\n ],\n },\n POST: {\n summary: 'Refresh transaction status from the provider',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 200, description: 'Refreshed transaction status' },\n { status: 401, description: 'Unauthorized' },\n { status: 404, description: 'Transaction not found' },\n { status: 422, description: 'Missing or malformed payload' },\n { status: 502, description: 'Gateway provider error' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -185,19 +185,33 @@ function PaymentTransactionsPage() {
|
|
|
185
185
|
const handleRefreshStatus = React.useCallback(async () => {
|
|
186
186
|
if (!selectedId) return;
|
|
187
187
|
setIsRefreshingStatus(true);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
try {
|
|
189
|
+
await runMutation({
|
|
190
|
+
operation: async () => {
|
|
191
|
+
await apiCallOrThrow("/api/payment_gateways/status", {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: { "content-type": "application/json" },
|
|
194
|
+
body: JSON.stringify({ transactionId: selectedId })
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
context: {
|
|
198
|
+
entityType: "payment_gateways:gateway_transaction",
|
|
199
|
+
entityId: selectedId
|
|
200
|
+
},
|
|
201
|
+
mutationPayload: { transactionId: selectedId }
|
|
202
|
+
});
|
|
203
|
+
await Promise.all([
|
|
204
|
+
loadRows(),
|
|
205
|
+
loadDetail(selectedId)
|
|
206
|
+
]);
|
|
207
|
+
flash(t("payment_gateways.transactions.success.refreshStatus", "Transaction status refreshed"), "success");
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const message = error instanceof Error && error.message ? error.message : t("payment_gateways.transactions.error.refreshStatus", "Failed to refresh transaction status");
|
|
210
|
+
flash(message, "error");
|
|
211
|
+
} finally {
|
|
191
212
|
setIsRefreshingStatus(false);
|
|
192
|
-
return;
|
|
193
213
|
}
|
|
194
|
-
|
|
195
|
-
loadRows(),
|
|
196
|
-
loadDetail(selectedId)
|
|
197
|
-
]);
|
|
198
|
-
flash(t("payment_gateways.transactions.success.refreshStatus", "Transaction status refreshed"), "success");
|
|
199
|
-
setIsRefreshingStatus(false);
|
|
200
|
-
}, [loadDetail, loadRows, selectedId, t]);
|
|
214
|
+
}, [loadDetail, loadRows, runMutation, selectedId, t]);
|
|
201
215
|
const handleCapturePayment = React.useCallback(async () => {
|
|
202
216
|
if (!selectedId) return;
|
|
203
217
|
setIsCapturing(true);
|