@openora/core 0.4.1-canary.112 → 0.4.1-canary.113
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/dist/.tsbuildinfo +1 -1
- package/dist/admin-console/plugin.d.ts +6 -0
- package/dist/admin-console/plugin.d.ts.map +1 -1
- package/dist/analytics/plugin.d.ts +6 -0
- package/dist/analytics/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.d.ts +6 -0
- package/dist/audit/plugin.d.ts.map +1 -1
- package/dist/casino/gaming/plugin.d.ts +6 -0
- package/dist/casino/gaming/plugin.d.ts.map +1 -1
- package/dist/casino/lobby/plugin.d.ts +6 -0
- package/dist/casino/lobby/plugin.d.ts.map +1 -1
- package/dist/cms/plugin.d.ts +6 -0
- package/dist/cms/plugin.d.ts.map +1 -1
- package/dist/compliance/plugin.d.ts +6 -0
- package/dist/compliance/plugin.d.ts.map +1 -1
- package/dist/contracts/schemas/platform-config.d.ts +19 -0
- package/dist/contracts/schemas/platform-config.d.ts.map +1 -1
- package/dist/contracts/schemas/platform-config.js +17 -0
- package/dist/contracts/schemas/platform-config.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat.service.int.test.js +117 -0
- package/dist/engagement/chat/__tests__/chat.service.int.test.js.map +1 -1
- package/dist/engagement/chat/plugin.d.ts +6 -0
- package/dist/engagement/chat/plugin.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat.service.d.ts +2 -2
- package/dist/engagement/chat/service/chat.service.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat.service.js +61 -1
- package/dist/engagement/chat/service/chat.service.js.map +1 -1
- package/dist/engagement/chat-commands/plugin.d.ts +6 -0
- package/dist/engagement/chat-commands/plugin.d.ts.map +1 -1
- package/dist/engagement/notifications/__tests__/notification-event-map.test.d.ts +2 -0
- package/dist/engagement/notifications/__tests__/notification-event-map.test.d.ts.map +1 -0
- package/dist/engagement/notifications/__tests__/notification-event-map.test.js +235 -0
- package/dist/engagement/notifications/__tests__/notification-event-map.test.js.map +1 -0
- package/dist/engagement/notifications/__tests__/notifications.router.int.test.d.ts +2 -0
- package/dist/engagement/notifications/__tests__/notifications.router.int.test.d.ts.map +1 -0
- package/dist/engagement/notifications/__tests__/notifications.router.int.test.js +142 -0
- package/dist/engagement/notifications/__tests__/notifications.router.int.test.js.map +1 -0
- package/dist/engagement/notifications/__tests__/notifications.service.int.test.js +113 -7
- package/dist/engagement/notifications/__tests__/notifications.service.int.test.js.map +1 -1
- package/dist/engagement/notifications/contract/index.d.ts +98 -17
- package/dist/engagement/notifications/contract/index.d.ts.map +1 -1
- package/dist/engagement/notifications/contract/index.js +34 -4
- package/dist/engagement/notifications/contract/index.js.map +1 -1
- package/dist/engagement/notifications/drizzle/migrations/0001_classy_pixie.sql +1 -0
- package/dist/engagement/notifications/drizzle/migrations/0002_open_madame_web.sql +2 -0
- package/dist/engagement/notifications/drizzle/migrations/meta/0001_snapshot.json +98 -0
- package/dist/engagement/notifications/drizzle/migrations/meta/0002_snapshot.json +119 -0
- package/dist/engagement/notifications/drizzle/migrations/meta/_journal.json +14 -0
- package/dist/engagement/notifications/plugin.d.ts +20 -3
- package/dist/engagement/notifications/plugin.d.ts.map +1 -1
- package/dist/engagement/notifications/plugin.js +219 -75
- package/dist/engagement/notifications/plugin.js.map +1 -1
- package/dist/engagement/notifications/react/notifications.d.ts +20 -0
- package/dist/engagement/notifications/react/notifications.d.ts.map +1 -0
- package/dist/engagement/notifications/react/notifications.js +39 -0
- package/dist/engagement/notifications/react/notifications.js.map +1 -0
- package/dist/engagement/notifications/router/index.d.ts +70 -17
- package/dist/engagement/notifications/router/index.d.ts.map +1 -1
- package/dist/engagement/notifications/router/index.js +37 -16
- package/dist/engagement/notifications/router/index.js.map +1 -1
- package/dist/engagement/notifications/schema/index.d.ts +36 -0
- package/dist/engagement/notifications/schema/index.d.ts.map +1 -1
- package/dist/engagement/notifications/schema/index.js +7 -2
- package/dist/engagement/notifications/schema/index.js.map +1 -1
- package/dist/engagement/notifications/service/notifications.service.d.ts +27 -19
- package/dist/engagement/notifications/service/notifications.service.d.ts.map +1 -1
- package/dist/engagement/notifications/service/notifications.service.js +41 -11
- package/dist/engagement/notifications/service/notifications.service.js.map +1 -1
- package/dist/engagement/react.d.ts +2 -0
- package/dist/engagement/react.d.ts.map +1 -0
- package/dist/engagement/react.js +3 -0
- package/dist/engagement/react.js.map +1 -0
- package/dist/engagement/social/plugin.d.ts +6 -0
- package/dist/engagement/social/plugin.d.ts.map +1 -1
- package/dist/iam/plugin.d.ts +6 -0
- package/dist/iam/plugin.d.ts.map +1 -1
- package/dist/pam/identity/plugin.d.ts +6 -0
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/player-management/plugin.d.ts +6 -0
- package/dist/pam/player-management/plugin.d.ts.map +1 -1
- package/dist/pam/player-note/plugin.d.ts +6 -0
- package/dist/pam/player-note/plugin.d.ts.map +1 -1
- package/dist/pam/profile/plugin.d.ts +6 -0
- package/dist/pam/profile/plugin.d.ts.map +1 -1
- package/dist/pam/tag/plugin.d.ts +6 -0
- package/dist/pam/tag/plugin.d.ts.map +1 -1
- package/dist/server/runtime/core-token-catalog.d.ts +6 -0
- package/dist/server/runtime/core-token-catalog.d.ts.map +1 -1
- package/dist/wallet/plugin.d.ts +6 -0
- package/dist/wallet/plugin.d.ts.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "fa9988e3-5040-4f86-b921-4320fa27eeaa",
|
|
3
|
+
"prevId": "c5045022-6772-41f6-b8cc-54dc6f58645d",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.notification": {
|
|
8
|
+
"name": "notification",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "uuid",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true,
|
|
16
|
+
"default": "gen_random_uuid()"
|
|
17
|
+
},
|
|
18
|
+
"user_id": {
|
|
19
|
+
"name": "user_id",
|
|
20
|
+
"type": "uuid",
|
|
21
|
+
"primaryKey": false,
|
|
22
|
+
"notNull": true
|
|
23
|
+
},
|
|
24
|
+
"type": {
|
|
25
|
+
"name": "type",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true
|
|
29
|
+
},
|
|
30
|
+
"title": {
|
|
31
|
+
"name": "title",
|
|
32
|
+
"type": "text",
|
|
33
|
+
"primaryKey": false,
|
|
34
|
+
"notNull": true
|
|
35
|
+
},
|
|
36
|
+
"body": {
|
|
37
|
+
"name": "body",
|
|
38
|
+
"type": "text",
|
|
39
|
+
"primaryKey": false,
|
|
40
|
+
"notNull": true
|
|
41
|
+
},
|
|
42
|
+
"data": {
|
|
43
|
+
"name": "data",
|
|
44
|
+
"type": "jsonb",
|
|
45
|
+
"primaryKey": false,
|
|
46
|
+
"notNull": false
|
|
47
|
+
},
|
|
48
|
+
"read_at": {
|
|
49
|
+
"name": "read_at",
|
|
50
|
+
"type": "timestamp with time zone",
|
|
51
|
+
"primaryKey": false,
|
|
52
|
+
"notNull": false
|
|
53
|
+
},
|
|
54
|
+
"created_at": {
|
|
55
|
+
"name": "created_at",
|
|
56
|
+
"type": "timestamp with time zone",
|
|
57
|
+
"primaryKey": false,
|
|
58
|
+
"notNull": true,
|
|
59
|
+
"default": "now()"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"indexes": {
|
|
63
|
+
"notification_user_id_idx": {
|
|
64
|
+
"name": "notification_user_id_idx",
|
|
65
|
+
"columns": [
|
|
66
|
+
{
|
|
67
|
+
"expression": "user_id",
|
|
68
|
+
"isExpression": false,
|
|
69
|
+
"asc": true,
|
|
70
|
+
"nulls": "last"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"isUnique": false,
|
|
74
|
+
"concurrently": false,
|
|
75
|
+
"method": "btree",
|
|
76
|
+
"with": {}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"foreignKeys": {},
|
|
80
|
+
"compositePrimaryKeys": {},
|
|
81
|
+
"uniqueConstraints": {},
|
|
82
|
+
"policies": {},
|
|
83
|
+
"checkConstraints": {},
|
|
84
|
+
"isRLSEnabled": false
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"enums": {},
|
|
88
|
+
"schemas": {},
|
|
89
|
+
"sequences": {},
|
|
90
|
+
"roles": {},
|
|
91
|
+
"policies": {},
|
|
92
|
+
"views": {},
|
|
93
|
+
"_meta": {
|
|
94
|
+
"columns": {},
|
|
95
|
+
"schemas": {},
|
|
96
|
+
"tables": {}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "a7e44828-dab8-4c21-ad56-ea8e67992ea5",
|
|
3
|
+
"prevId": "fa9988e3-5040-4f86-b921-4320fa27eeaa",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.notification": {
|
|
8
|
+
"name": "notification",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "uuid",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true,
|
|
16
|
+
"default": "gen_random_uuid()"
|
|
17
|
+
},
|
|
18
|
+
"user_id": {
|
|
19
|
+
"name": "user_id",
|
|
20
|
+
"type": "uuid",
|
|
21
|
+
"primaryKey": false,
|
|
22
|
+
"notNull": true
|
|
23
|
+
},
|
|
24
|
+
"type": {
|
|
25
|
+
"name": "type",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true
|
|
29
|
+
},
|
|
30
|
+
"title": {
|
|
31
|
+
"name": "title",
|
|
32
|
+
"type": "text",
|
|
33
|
+
"primaryKey": false,
|
|
34
|
+
"notNull": true
|
|
35
|
+
},
|
|
36
|
+
"body": {
|
|
37
|
+
"name": "body",
|
|
38
|
+
"type": "text",
|
|
39
|
+
"primaryKey": false,
|
|
40
|
+
"notNull": true
|
|
41
|
+
},
|
|
42
|
+
"data": {
|
|
43
|
+
"name": "data",
|
|
44
|
+
"type": "jsonb",
|
|
45
|
+
"primaryKey": false,
|
|
46
|
+
"notNull": false
|
|
47
|
+
},
|
|
48
|
+
"read_at": {
|
|
49
|
+
"name": "read_at",
|
|
50
|
+
"type": "timestamp with time zone",
|
|
51
|
+
"primaryKey": false,
|
|
52
|
+
"notNull": false
|
|
53
|
+
},
|
|
54
|
+
"created_at": {
|
|
55
|
+
"name": "created_at",
|
|
56
|
+
"type": "timestamp with time zone",
|
|
57
|
+
"primaryKey": false,
|
|
58
|
+
"notNull": true,
|
|
59
|
+
"default": "now()"
|
|
60
|
+
},
|
|
61
|
+
"event_id": {
|
|
62
|
+
"name": "event_id",
|
|
63
|
+
"type": "uuid",
|
|
64
|
+
"primaryKey": false,
|
|
65
|
+
"notNull": false
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"indexes": {
|
|
69
|
+
"notification_user_id_idx": {
|
|
70
|
+
"name": "notification_user_id_idx",
|
|
71
|
+
"columns": [
|
|
72
|
+
{
|
|
73
|
+
"expression": "user_id",
|
|
74
|
+
"isExpression": false,
|
|
75
|
+
"asc": true,
|
|
76
|
+
"nulls": "last"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"isUnique": false,
|
|
80
|
+
"concurrently": false,
|
|
81
|
+
"method": "btree",
|
|
82
|
+
"with": {}
|
|
83
|
+
},
|
|
84
|
+
"notification_event_id_idx": {
|
|
85
|
+
"name": "notification_event_id_idx",
|
|
86
|
+
"columns": [
|
|
87
|
+
{
|
|
88
|
+
"expression": "event_id",
|
|
89
|
+
"isExpression": false,
|
|
90
|
+
"asc": true,
|
|
91
|
+
"nulls": "last"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"isUnique": true,
|
|
95
|
+
"concurrently": false,
|
|
96
|
+
"method": "btree",
|
|
97
|
+
"with": {}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"foreignKeys": {},
|
|
101
|
+
"compositePrimaryKeys": {},
|
|
102
|
+
"uniqueConstraints": {},
|
|
103
|
+
"policies": {},
|
|
104
|
+
"checkConstraints": {},
|
|
105
|
+
"isRLSEnabled": false
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"enums": {},
|
|
109
|
+
"schemas": {},
|
|
110
|
+
"sequences": {},
|
|
111
|
+
"roles": {},
|
|
112
|
+
"policies": {},
|
|
113
|
+
"views": {},
|
|
114
|
+
"_meta": {
|
|
115
|
+
"columns": {},
|
|
116
|
+
"schemas": {},
|
|
117
|
+
"tables": {}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
"when": 1783555224659,
|
|
9
9
|
"tag": "0000_sparkling_kulan_gath",
|
|
10
10
|
"breakpoints": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idx": 1,
|
|
14
|
+
"version": "7",
|
|
15
|
+
"when": 1787595667142,
|
|
16
|
+
"tag": "0001_classy_pixie",
|
|
17
|
+
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "7",
|
|
22
|
+
"when": 1787911988004,
|
|
23
|
+
"tag": "0002_open_madame_web",
|
|
24
|
+
"breakpoints": true
|
|
11
25
|
}
|
|
12
26
|
]
|
|
13
27
|
}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import { type AdminUserDirectory, type JobQueueAdapter, type NotificationDeliveryAdapter } from '@openora/core/contracts';
|
|
1
|
+
import { type AdminUserDirectory, type DomainEventName, type JobQueueAdapter, type NotificationDeliveryAdapter, type RealtimeTransport } from '@openora/core/contracts';
|
|
2
|
+
import { type CreateNotificationInput } from './contract/index.js';
|
|
3
|
+
type NotificationMapEntry = {
|
|
4
|
+
event: DomainEventName;
|
|
5
|
+
sendEmail: boolean;
|
|
6
|
+
handle: (payload: unknown) => CreateNotificationInput | null;
|
|
7
|
+
};
|
|
8
|
+
export declare const notificationEventMap: NotificationMapEntry[];
|
|
9
|
+
export declare function buildKycResubmissionNotification(payload: {
|
|
10
|
+
userId: string;
|
|
11
|
+
reason: string | null;
|
|
12
|
+
}): CreateNotificationInput;
|
|
2
13
|
declare const _default: {
|
|
3
14
|
readonly id: "notifications";
|
|
4
15
|
readonly dependsOn: readonly ["identity"];
|
|
@@ -16,7 +27,7 @@ declare const _default: {
|
|
|
16
27
|
CHAT_BLOCK_WRITER: import("@openora/core/contracts").Token<import("@openora/core/contracts").ChatBlockWriter>;
|
|
17
28
|
CHAT_MODERATION: import("@openora/core/contracts").Token<import("@openora/core/contracts").ChatModeration, string>;
|
|
18
29
|
CHAT_REALTIME_CLIENT_AUTHORIZER: import("@openora/core/contracts").Token<import("@openora/core/contracts").RealtimeClientAuthorizer>;
|
|
19
|
-
CHAT_REALTIME_TRANSPORT: import("@openora/core/contracts").Token<
|
|
30
|
+
CHAT_REALTIME_TRANSPORT: import("@openora/core/contracts").Token<RealtimeTransport>;
|
|
20
31
|
CHAT_ROOM_ACCESS: import("@openora/core/contracts").Token<import("@openora/core/contracts").ChatRoomAccess>;
|
|
21
32
|
CHAT_SYSTEM_WRITER: import("@openora/core/contracts").Token<import("@openora/core/contracts").ChatSystemWriter, string>;
|
|
22
33
|
DRIZZLE: import("@openora/core/contracts").Token<import("@openora/core/server").DrizzleService>;
|
|
@@ -118,6 +129,12 @@ declare const _default: {
|
|
|
118
129
|
termsVersion: string;
|
|
119
130
|
requireEmailVerification: boolean;
|
|
120
131
|
} | undefined;
|
|
132
|
+
notifications?: {
|
|
133
|
+
retention?: {
|
|
134
|
+
days: number;
|
|
135
|
+
cron: string;
|
|
136
|
+
} | undefined;
|
|
137
|
+
} | undefined;
|
|
121
138
|
supportedLanguages?: string[] | undefined;
|
|
122
139
|
}, string>;
|
|
123
140
|
PLAYER_ACTIVITY_TRACKER: import("@openora/core/contracts").Token<import("@openora/core/contracts").PlayerActivityTracker>;
|
|
@@ -126,7 +143,7 @@ declare const _default: {
|
|
|
126
143
|
PLAY_ELIGIBILITY: import("@openora/core/contracts").Token<import("@openora/core/contracts").PlayEligibilityPort>;
|
|
127
144
|
RATE_LIMITER: import("@openora/core/contracts").Token<import("@openora/core/contracts").RateLimiterAdapter<`register:${string}` | `login:${string}` | `enable2fa:${string}` | `verify2fa:${string}` | `disable2fa:${string}` | `pwreset-req:${string}` | `pwreset-verify:${string}` | `pwreset:${string}` | `change-password:${string}` | `email-verify:${string}` | `verify-email:${string}` | `wallet-mutation:${string}` | `wallet-webhook:${string}` | `chat-room-join:${string}` | `chat-send:${string}` | `report-access-denied:${string}`>>;
|
|
128
145
|
REALTIME_CLIENT_AUTHORIZER: import("@openora/core/contracts").Token<import("@openora/core/contracts").RealtimeClientAuthorizer>;
|
|
129
|
-
REALTIME_TRANSPORT: import("@openora/core/contracts").Token<
|
|
146
|
+
REALTIME_TRANSPORT: import("@openora/core/contracts").Token<RealtimeTransport>;
|
|
130
147
|
RNG_ADAPTER: import("@openora/core/contracts").Token<import("@openora/core/contracts").RngAdapter>;
|
|
131
148
|
SEND_EMAIL: import("@openora/core/contracts").Token<import("@openora/core/contracts").SendEmailPort>;
|
|
132
149
|
SESSION_COMMANDS: import("@openora/core/contracts").Token<import("@openora/core/contracts").SessionCommands>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/engagement/notifications/plugin.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/engagement/notifications/plugin.ts"],"names":[],"mappings":"AACA,OAAO,EASL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAEpB,KAAK,eAAe,EACpB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAC;AAUjC,OAAO,EAAiC,KAAK,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAgClG,KAAK,oBAAoB,GAAG;IAC1B,KAAK,EAAE,eAAe,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,uBAAuB,GAAG,IAAI,CAAC;CAC9D,CAAC;AAqCF,eAAO,MAAM,oBAAoB,EAAE,oBAAoB,EAuItD,CAAC;AAEF,wBAAgB,gCAAgC,CAAC,OAAO,EAAE;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,GAAG,uBAAuB,CAQ1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAED,wBAmL8C"}
|
|
@@ -1,18 +1,155 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
|
-
import { ADMIN_USER_DIRECTORY, JOB_QUEUE, NOTIFICATION_DELIVERY_ADAPTER, UuidSchema, domainEventSchemas, queue, } from '@openora/core/contracts';
|
|
2
|
+
import { ADMIN_USER_DIRECTORY, JOB_QUEUE, NOTIFICATION_DELIVERY_ADAPTER, PLATFORM_CONFIG, REALTIME_TRANSPORT, UuidSchema, domainEventSchemas, queue, } from '@openora/core/contracts';
|
|
3
3
|
import { createLogger, EVENT_BUS, DRIZZLE } from '@openora/core/server';
|
|
4
4
|
import { MockNotificationDeliveryAdapter } from './adapters/mock/mock-notification-adapter.js';
|
|
5
|
-
import { createNotificationsRouter } from './router/index.js';
|
|
5
|
+
import { createNotificationsRouter, notificationsChannel, toNotificationDto, } from './router/index.js';
|
|
6
6
|
import { NotificationsService } from './service/notifications.service.js';
|
|
7
|
+
import { CreateNotificationInputSchema } from './contract/index.js';
|
|
7
8
|
const KYC_RESUBMISSION_NOTIFY_QUEUE = queue('kyc-resubmission-notify');
|
|
9
|
+
const NOTIFICATIONS_RETENTION_PURGE_QUEUE = queue('notifications-retention-purge');
|
|
10
|
+
const NOTIFICATIONS_DISPATCH_QUEUE = queue('notifications-dispatch');
|
|
11
|
+
const DEFAULT_NOTIFICATIONS_RETENTION_DAYS = 30;
|
|
12
|
+
const DEFAULT_NOTIFICATIONS_RETENTION_CRON = '0 3 * * *';
|
|
8
13
|
const KycResubmissionNotifyJobSchema = z.object({
|
|
9
14
|
userId: UuidSchema,
|
|
10
15
|
reason: z.string().nullable(),
|
|
11
16
|
});
|
|
17
|
+
const NotificationDispatchJobSchema = z.object({
|
|
18
|
+
event: z.string(),
|
|
19
|
+
input: CreateNotificationInputSchema,
|
|
20
|
+
sendEmail: z.boolean(),
|
|
21
|
+
});
|
|
22
|
+
function formatMoneyAmount(amount) {
|
|
23
|
+
const match = /^(-)?(\d+)(?:\.(\d+))?$/.exec(amount);
|
|
24
|
+
if (!match) {
|
|
25
|
+
return amount;
|
|
26
|
+
}
|
|
27
|
+
const [, sign, integerPart, fractionPart] = match;
|
|
28
|
+
const groupedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
29
|
+
const trimmedFraction = fractionPart ? fractionPart.replace(/0+$/, '') : '';
|
|
30
|
+
const magnitude = trimmedFraction ? `${groupedInteger}.${trimmedFraction}` : groupedInteger;
|
|
31
|
+
return sign ? `${sign}${magnitude}` : magnitude;
|
|
32
|
+
}
|
|
33
|
+
// Drops null/undefined entity refs (eg a nullable roomId) so `data` only ever carries
|
|
34
|
+
// fields the source event actually had a value for; an all-null payload becomes `null`.
|
|
35
|
+
function compactData(fields) {
|
|
36
|
+
const entries = Object.entries(fields).filter((entry) => entry[1] !== null && entry[1] !== undefined);
|
|
37
|
+
return entries.length > 0 ? Object.fromEntries(entries) : null;
|
|
38
|
+
}
|
|
39
|
+
// A plain array literal would widen every entry's `event` to DomainEventName and lose the
|
|
40
|
+
// payload type tied to it; this generic keeps K bound per entry so buildNotification stays
|
|
41
|
+
// checked against that one event's actual payload shape.
|
|
42
|
+
function mapEvent(event, buildNotification, options) {
|
|
43
|
+
return {
|
|
44
|
+
event,
|
|
45
|
+
sendEmail: options.sendEmail,
|
|
46
|
+
handle: (payload) => {
|
|
47
|
+
const parsed = domainEventSchemas[event].safeParse(payload);
|
|
48
|
+
if (!parsed.success) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// Library boundary: TS cannot narrow a domainEventSchemas lookup keyed by this
|
|
52
|
+
// function's own generic K to that one event's payload type; the pairing holds
|
|
53
|
+
// by domainEventSchemas' own definition (DomainEventPayload<K> is inferred from it).
|
|
54
|
+
return buildNotification(parsed.data);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export const notificationEventMap = [
|
|
59
|
+
mapEvent('wallet.withdrawal.approved', (p) => ({
|
|
60
|
+
userId: p.userId,
|
|
61
|
+
type: 'withdrawal.approved',
|
|
62
|
+
title: 'Withdrawal approved',
|
|
63
|
+
body: `Your withdrawal of ${formatMoneyAmount(p.amount)} ${p.currency} has been approved and is being processed.`,
|
|
64
|
+
data: { transactionId: p.transactionId },
|
|
65
|
+
}), { sendEmail: true }),
|
|
66
|
+
mapEvent('wallet.withdrawal.rejected', (p) => ({
|
|
67
|
+
userId: p.userId,
|
|
68
|
+
type: 'withdrawal.rejected',
|
|
69
|
+
title: 'Withdrawal rejected',
|
|
70
|
+
body: `Your withdrawal of ${formatMoneyAmount(p.amount)} ${p.currency} was rejected and the funds were returned to your balance.${p.reason ? ` Reason: ${p.reason}.` : ''}`,
|
|
71
|
+
data: { transactionId: p.transactionId },
|
|
72
|
+
}), { sendEmail: true }),
|
|
73
|
+
mapEvent('wallet.withdrawal.requested', (p) => ({
|
|
74
|
+
userId: p.userId,
|
|
75
|
+
type: 'withdrawal.requested',
|
|
76
|
+
title: 'Withdrawal requested',
|
|
77
|
+
body: `Your withdrawal request of ${formatMoneyAmount(p.amount)} ${p.currency} has been received and is pending review.`,
|
|
78
|
+
data: { transactionId: p.transactionId },
|
|
79
|
+
}), { sendEmail: false }),
|
|
80
|
+
mapEvent('wallet.withdrawal.completed', (p) => ({
|
|
81
|
+
userId: p.userId,
|
|
82
|
+
type: 'withdrawal.completed',
|
|
83
|
+
title: 'Withdrawal completed',
|
|
84
|
+
body: `Your withdrawal of ${formatMoneyAmount(p.amount)} ${p.currency} has been completed.`,
|
|
85
|
+
data: { transactionId: p.transactionId },
|
|
86
|
+
}), { sendEmail: false }),
|
|
87
|
+
mapEvent('wallet.withdrawal.failed', (p) => ({
|
|
88
|
+
userId: p.userId,
|
|
89
|
+
type: 'withdrawal.failed',
|
|
90
|
+
title: 'Withdrawal failed',
|
|
91
|
+
body: `Your withdrawal of ${formatMoneyAmount(p.amount)} ${p.currency} failed and the funds were returned to your balance.`,
|
|
92
|
+
data: { transactionId: p.transactionId },
|
|
93
|
+
}), { sendEmail: false }),
|
|
94
|
+
mapEvent('wallet.deposit.completed', (p) => ({
|
|
95
|
+
userId: p.userId,
|
|
96
|
+
type: 'deposit.completed',
|
|
97
|
+
title: 'Deposit completed',
|
|
98
|
+
body: `Your deposit of ${formatMoneyAmount(p.amount)} ${p.currency} has been completed.`,
|
|
99
|
+
data: { transactionId: p.transactionId },
|
|
100
|
+
}), { sendEmail: false }),
|
|
101
|
+
mapEvent('wallet.manual_adjustment.created', (p) => ({
|
|
102
|
+
userId: p.userId,
|
|
103
|
+
type: 'balance.adjusted',
|
|
104
|
+
title: 'Balance adjusted',
|
|
105
|
+
body: `Your balance was ${p.direction === 'credit' ? 'credited' : 'debited'} ${formatMoneyAmount(p.amount)} ${p.currency}. Reason: ${p.reason}.`,
|
|
106
|
+
data: { transactionId: p.transactionId },
|
|
107
|
+
}), { sendEmail: false }),
|
|
108
|
+
mapEvent('wallet.bonus_rollover.completed', (p) => ({
|
|
109
|
+
userId: p.userId,
|
|
110
|
+
type: 'wallet.bonus_rollover.completed',
|
|
111
|
+
title: 'Bonus unlocked',
|
|
112
|
+
body: `Your ${formatMoneyAmount(p.creditedAmount)} ${p.currency} bonus credit has cleared its rollover requirement and is now fully withdrawable.`,
|
|
113
|
+
data: null,
|
|
114
|
+
}), { sendEmail: false }),
|
|
115
|
+
mapEvent('chat.user.mentioned', (p) => ({
|
|
116
|
+
userId: p.mentionedUserId,
|
|
117
|
+
type: 'chat.mention',
|
|
118
|
+
title: 'You were mentioned',
|
|
119
|
+
body: 'You were mentioned in a chat message.',
|
|
120
|
+
data: compactData({ roomId: p.roomId, messageId: p.messageId }),
|
|
121
|
+
}), { sendEmail: false }),
|
|
122
|
+
// Pre-existing types: in-app only, unchanged from before the declarative-map refactor.
|
|
123
|
+
mapEvent('social.friend_request.sent', (p) => ({
|
|
124
|
+
userId: p.addresseeId,
|
|
125
|
+
type: 'social.friend_request.received',
|
|
126
|
+
title: 'New friend request',
|
|
127
|
+
body: `${p.requesterUsername} sent you a friend request.`,
|
|
128
|
+
data: { requesterId: p.requesterId },
|
|
129
|
+
}), { sendEmail: false }),
|
|
130
|
+
mapEvent('social.friend_request.accepted', (p) => ({
|
|
131
|
+
userId: p.requesterId,
|
|
132
|
+
type: 'social.friend_request.accepted',
|
|
133
|
+
title: 'Friend request accepted',
|
|
134
|
+
body: `${p.accepterUsername} accepted your friend request.`,
|
|
135
|
+
// accepterId, not requesterId: the recipient of THIS notification is the
|
|
136
|
+
// requester, so the linkable "other party" is whoever just accepted.
|
|
137
|
+
data: { accepterId: p.accepterId },
|
|
138
|
+
}), { sendEmail: false }),
|
|
139
|
+
];
|
|
140
|
+
export function buildKycResubmissionNotification(payload) {
|
|
141
|
+
return {
|
|
142
|
+
userId: payload.userId,
|
|
143
|
+
type: 'kyc.resubmission_requested',
|
|
144
|
+
title: 'Document resubmission required',
|
|
145
|
+
body: `An admin has requested you resubmit your verification documents.${payload.reason ? ` Reason: ${payload.reason}.` : ''}`,
|
|
146
|
+
data: null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
12
149
|
export default {
|
|
13
150
|
id: 'notifications',
|
|
14
151
|
// ADMIN_USER_DIRECTORY (owned by identity) resolves the player's email for the
|
|
15
|
-
//
|
|
152
|
+
// delivery emails; pin load order so a split still finds the port. See ADR-0017.
|
|
16
153
|
dependsOn: ['identity'],
|
|
17
154
|
register(ctx) {
|
|
18
155
|
ctx.provide(NOTIFICATION_DELIVERY_ADAPTER, () => new MockNotificationDeliveryAdapter());
|
|
@@ -23,6 +160,8 @@ export default {
|
|
|
23
160
|
let deliveryRef = null;
|
|
24
161
|
let directoryRef = null;
|
|
25
162
|
let jobQueueRef = null;
|
|
163
|
+
let realtimeRef = null;
|
|
164
|
+
let retentionDaysRef = DEFAULT_NOTIFICATIONS_RETENTION_DAYS;
|
|
26
165
|
// Best-effort email alongside the in-app notification; a missing user or delivery
|
|
27
166
|
// failure is logged, never thrown - the in-app notification already landed.
|
|
28
167
|
const sendEmail = (userId, title, body) => {
|
|
@@ -41,69 +180,34 @@ export default {
|
|
|
41
180
|
})
|
|
42
181
|
.catch((err) => logger.error({ err }, 'notification email delivery failed'));
|
|
43
182
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!parsed.success || !svcRef) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const p = parsed.data;
|
|
50
|
-
const title = 'Withdrawal approved';
|
|
51
|
-
const body = `Your withdrawal of ${p.amount} ${p.currency} has been approved and is being processed.`;
|
|
52
|
-
svcRef
|
|
53
|
-
.create({ userId: p.userId, type: 'withdrawal.approved', title, body })
|
|
54
|
-
.catch((err) => logger.error({ err }, 'withdrawal.approved notification failed'));
|
|
55
|
-
sendEmail(p.userId, title, body);
|
|
56
|
-
});
|
|
57
|
-
ctx.events.on('wallet.withdrawal.rejected', (payload) => {
|
|
58
|
-
const parsed = domainEventSchemas['wallet.withdrawal.rejected'].safeParse(payload);
|
|
59
|
-
if (!parsed.success || !svcRef) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const p = parsed.data;
|
|
63
|
-
const reason = p.reason ? ` Reason: ${p.reason}.` : '';
|
|
64
|
-
const title = 'Withdrawal rejected';
|
|
65
|
-
const body = `Your withdrawal of ${p.amount} ${p.currency} was rejected and the funds were returned to your balance.${reason}`;
|
|
66
|
-
svcRef
|
|
67
|
-
.create({ userId: p.userId, type: 'withdrawal.rejected', title, body })
|
|
68
|
-
.catch((err) => logger.error({ err }, 'withdrawal.rejected notification failed'));
|
|
69
|
-
sendEmail(p.userId, title, body);
|
|
70
|
-
});
|
|
71
|
-
ctx.events.on('wallet.bonus_rollover.completed', (payload) => {
|
|
72
|
-
const parsed = domainEventSchemas['wallet.bonus_rollover.completed'].safeParse(payload);
|
|
73
|
-
if (!parsed.success || !svcRef) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const p = parsed.data;
|
|
77
|
-
const title = 'Bonus unlocked';
|
|
78
|
-
const body = `Your ${p.creditedAmount} ${p.currency} bonus credit has cleared its rollover requirement and is now fully withdrawable.`;
|
|
79
|
-
svcRef
|
|
80
|
-
.create({ userId: p.userId, type: 'wallet.bonus_rollover.completed', title, body })
|
|
81
|
-
.catch((err) => logger.error({ err }, 'wallet.bonus_rollover.completed notification failed'));
|
|
82
|
-
});
|
|
83
|
-
ctx.events.on('social.friend_request.sent', (payload) => {
|
|
84
|
-
const parsed = domainEventSchemas['social.friend_request.sent'].safeParse(payload);
|
|
85
|
-
if (!parsed.success || !svcRef) {
|
|
183
|
+
const publishNotification = (record) => {
|
|
184
|
+
if (!realtimeRef) {
|
|
86
185
|
return;
|
|
87
186
|
}
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
svcRef
|
|
92
|
-
.create({ userId: p.addresseeId, type: 'social.friend_request.received', title, body })
|
|
93
|
-
.catch((err) => logger.error({ err }, 'social.friend_request.sent notification failed'));
|
|
94
|
-
});
|
|
95
|
-
ctx.events.on('social.friend_request.accepted', (payload) => {
|
|
96
|
-
const parsed = domainEventSchemas['social.friend_request.accepted'].safeParse(payload);
|
|
97
|
-
if (!parsed.success || !svcRef) {
|
|
98
|
-
return;
|
|
187
|
+
const dto = toNotificationDto(record);
|
|
188
|
+
if (dto) {
|
|
189
|
+
void Promise.resolve(realtimeRef.publish(notificationsChannel(record.userId), dto)).catch((err) => logger.error({ err }, 'notification realtime publish failed'));
|
|
99
190
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
191
|
+
};
|
|
192
|
+
for (const entry of notificationEventMap) {
|
|
193
|
+
ctx.events.on(entry.event, (payload, envelope) => {
|
|
194
|
+
const input = entry.handle(payload);
|
|
195
|
+
if (!input || !jobQueueRef || !envelope) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
jobQueueRef
|
|
199
|
+
.enqueue(NOTIFICATIONS_DISPATCH_QUEUE, {
|
|
200
|
+
event: entry.event,
|
|
201
|
+
input: { ...input, eventId: envelope.eventId },
|
|
202
|
+
sendEmail: entry.sendEmail,
|
|
203
|
+
}, {
|
|
204
|
+
idempotencyKey: `notifications-dispatch:${envelope.eventId}`,
|
|
205
|
+
attempts: 5,
|
|
206
|
+
backoff: { type: 'exponential', delayMs: 1000 },
|
|
207
|
+
})
|
|
208
|
+
.catch((err) => logger.error({ err, event: entry.event }, 'notification dispatch enqueue failed'));
|
|
209
|
+
});
|
|
210
|
+
}
|
|
107
211
|
ctx.events.on('compliance.kyc.updated', (payload, envelope) => {
|
|
108
212
|
const parsed = domainEventSchemas['compliance.kyc.updated'].safeParse(payload);
|
|
109
213
|
if (!parsed.success || !jobQueueRef) {
|
|
@@ -113,8 +217,12 @@ export default {
|
|
|
113
217
|
if (p.status !== 'resubmission_requested' || p.source !== 'manual') {
|
|
114
218
|
return;
|
|
115
219
|
}
|
|
220
|
+
if (!envelope?.eventId) {
|
|
221
|
+
logger.warn({ userId: p.userId }, 'kyc-resubmission-notify enqueue skipped: missing eventId');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
116
224
|
jobQueueRef
|
|
117
|
-
.enqueue(KYC_RESUBMISSION_NOTIFY_QUEUE, { userId: p.userId, reason: p.reason }, { idempotencyKey: `kyc-resubmission-notify:${envelope
|
|
225
|
+
.enqueue(KYC_RESUBMISSION_NOTIFY_QUEUE, { userId: p.userId, reason: p.reason }, { idempotencyKey: `kyc-resubmission-notify:${envelope.eventId}` })
|
|
118
226
|
.catch((err) => logger.error({ err }, 'kyc-resubmission-notify enqueue failed'));
|
|
119
227
|
});
|
|
120
228
|
ctx.jobs.worker({
|
|
@@ -124,16 +232,44 @@ export default {
|
|
|
124
232
|
if (!svcRef) {
|
|
125
233
|
return;
|
|
126
234
|
}
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
235
|
+
const input = buildKycResubmissionNotification(payload);
|
|
236
|
+
const record = await svcRef.create(input);
|
|
237
|
+
if (!record) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
publishNotification(record);
|
|
241
|
+
sendEmail(input.userId, input.title, input.body);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
ctx.jobs.worker({
|
|
245
|
+
queue: NOTIFICATIONS_DISPATCH_QUEUE,
|
|
246
|
+
schema: NotificationDispatchJobSchema,
|
|
247
|
+
handler: async ({ payload }) => {
|
|
248
|
+
if (!svcRef) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const record = await svcRef.create(payload.input);
|
|
252
|
+
if (!record) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
publishNotification(record);
|
|
256
|
+
if (payload.sendEmail) {
|
|
257
|
+
sendEmail(record.userId, record.title, record.body);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
onDeadLetter: (jobCtx, error) => {
|
|
261
|
+
logger.error({ err: error, event: jobCtx.payload.event, jobId: jobCtx.id }, 'notification dispatch exhausted retries');
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
ctx.jobs.worker({
|
|
265
|
+
queue: NOTIFICATIONS_RETENTION_PURGE_QUEUE,
|
|
266
|
+
schema: z.object({}),
|
|
267
|
+
handler: async () => {
|
|
268
|
+
if (!svcRef) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const { count } = await svcRef.purgeExpired(retentionDaysRef);
|
|
272
|
+
logger.info({ count }, 'notification retention purge complete');
|
|
137
273
|
},
|
|
138
274
|
});
|
|
139
275
|
ctx.routers.add('notifications', (c) => {
|
|
@@ -142,7 +278,15 @@ export default {
|
|
|
142
278
|
deliveryRef = c.get(NOTIFICATION_DELIVERY_ADAPTER);
|
|
143
279
|
directoryRef = c.get(ADMIN_USER_DIRECTORY);
|
|
144
280
|
jobQueueRef = c.get(JOB_QUEUE);
|
|
145
|
-
|
|
281
|
+
realtimeRef = c.get(REALTIME_TRANSPORT);
|
|
282
|
+
const platformConfig = c.has(PLATFORM_CONFIG) ? c.get(PLATFORM_CONFIG) : undefined;
|
|
283
|
+
retentionDaysRef =
|
|
284
|
+
platformConfig?.notifications?.retention?.days ?? DEFAULT_NOTIFICATIONS_RETENTION_DAYS;
|
|
285
|
+
const purgeCron = platformConfig?.notifications?.retention?.cron ?? DEFAULT_NOTIFICATIONS_RETENTION_CRON;
|
|
286
|
+
// Off-peak, one hour after tag's daily-evaluation (0 2 * * *), so the in-process
|
|
287
|
+
// driver doesn't run both sweeps at once in dev/test.
|
|
288
|
+
void jobQueueRef.schedule(NOTIFICATIONS_RETENTION_PURGE_QUEUE, 'notifications.retention-purge.daily', {}, { cron: purgeCron });
|
|
289
|
+
return createNotificationsRouter({ notifications: svc, realtime: realtimeRef });
|
|
146
290
|
});
|
|
147
291
|
},
|
|
148
292
|
};
|