@openora/core 0.4.1-canary.37 → 0.4.1-canary.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/admin-console/__tests__/backoffice.router.test.js +67 -51
  3. package/dist/admin-console/__tests__/backoffice.router.test.js.map +1 -1
  4. package/dist/admin-console/__tests__/backoffice.service.test.js +1 -13
  5. package/dist/admin-console/__tests__/backoffice.service.test.js.map +1 -1
  6. package/dist/analytics/__tests__/analytics.contract.test.js +12 -0
  7. package/dist/analytics/__tests__/analytics.contract.test.js.map +1 -1
  8. package/dist/analytics/__tests__/financial-analytics.service.test.js +3 -3
  9. package/dist/analytics/__tests__/financial-analytics.service.test.js.map +1 -1
  10. package/dist/analytics/service/financial-analytics.service.d.ts.map +1 -1
  11. package/dist/analytics/service/financial-analytics.service.js +14 -17
  12. package/dist/analytics/service/financial-analytics.service.js.map +1 -1
  13. package/dist/analytics/service/funnel-analytics.service.d.ts.map +1 -1
  14. package/dist/analytics/service/funnel-analytics.service.js +8 -11
  15. package/dist/analytics/service/funnel-analytics.service.js.map +1 -1
  16. package/dist/audit/__tests__/audit.service.test.js +128 -251
  17. package/dist/audit/__tests__/audit.service.test.js.map +1 -1
  18. package/dist/casino/gaming/__tests__/gaming.service.test.js +83 -89
  19. package/dist/casino/gaming/__tests__/gaming.service.test.js.map +1 -1
  20. package/dist/casino/lobby/__tests__/lobby.cache.test.js +42 -33
  21. package/dist/casino/lobby/__tests__/lobby.cache.test.js.map +1 -1
  22. package/dist/cms/__tests__/cms.reads.test.js +59 -80
  23. package/dist/cms/__tests__/cms.reads.test.js.map +1 -1
  24. package/dist/cms/__tests__/cms.router.test.js +62 -20
  25. package/dist/cms/__tests__/cms.router.test.js.map +1 -1
  26. package/dist/cms/__tests__/cms.service.test.js +116 -14
  27. package/dist/cms/__tests__/cms.service.test.js.map +1 -1
  28. package/dist/common/errors/__tests__/errors.mapper.test.d.ts +2 -0
  29. package/dist/common/errors/__tests__/errors.mapper.test.d.ts.map +1 -0
  30. package/dist/common/errors/__tests__/errors.mapper.test.js +59 -0
  31. package/dist/common/errors/__tests__/errors.mapper.test.js.map +1 -0
  32. package/dist/compliance/__tests__/assert.test.d.ts +2 -0
  33. package/dist/compliance/__tests__/assert.test.d.ts.map +1 -0
  34. package/dist/compliance/__tests__/assert.test.js +34 -0
  35. package/dist/compliance/__tests__/assert.test.js.map +1 -0
  36. package/dist/compliance/__tests__/compliance.service.test.js +149 -84
  37. package/dist/compliance/__tests__/compliance.service.test.js.map +1 -1
  38. package/dist/compliance/__tests__/kyc.service.test.js +247 -155
  39. package/dist/compliance/__tests__/kyc.service.test.js.map +1 -1
  40. package/dist/compliance/__tests__/limits.contract.test.d.ts +2 -0
  41. package/dist/compliance/__tests__/limits.contract.test.d.ts.map +1 -0
  42. package/dist/compliance/__tests__/limits.contract.test.js +43 -0
  43. package/dist/compliance/__tests__/limits.contract.test.js.map +1 -0
  44. package/dist/compliance/__tests__/rg-monitoring.service.test.js +256 -101
  45. package/dist/compliance/__tests__/rg-monitoring.service.test.js.map +1 -1
  46. package/dist/compliance/__tests__/rg.contract.test.js +1 -20
  47. package/dist/compliance/__tests__/rg.contract.test.js.map +1 -1
  48. package/dist/compliance/__tests__/rg.router.test.js +113 -65
  49. package/dist/compliance/__tests__/rg.router.test.js.map +1 -1
  50. package/dist/compliance/__tests__/rg.service.test.js +277 -235
  51. package/dist/compliance/__tests__/rg.service.test.js.map +1 -1
  52. package/dist/compliance/service/rg-monitoring.service.d.ts.map +1 -1
  53. package/dist/compliance/service/rg-monitoring.service.js +11 -8
  54. package/dist/compliance/service/rg-monitoring.service.js.map +1 -1
  55. package/dist/contracts/__tests__/compose-contract.test.d.ts +2 -0
  56. package/dist/contracts/__tests__/compose-contract.test.d.ts.map +1 -0
  57. package/dist/contracts/__tests__/compose-contract.test.js +27 -0
  58. package/dist/contracts/__tests__/compose-contract.test.js.map +1 -0
  59. package/dist/contracts/adapters/__tests__/token.test.d.ts +2 -0
  60. package/dist/contracts/adapters/__tests__/token.test.d.ts.map +1 -0
  61. package/dist/contracts/adapters/__tests__/token.test.js +14 -0
  62. package/dist/contracts/adapters/__tests__/token.test.js.map +1 -0
  63. package/dist/contracts/schemas/__tests__/events.test.d.ts +2 -0
  64. package/dist/contracts/schemas/__tests__/events.test.d.ts.map +1 -0
  65. package/dist/contracts/schemas/__tests__/events.test.js +51 -0
  66. package/dist/contracts/schemas/__tests__/events.test.js.map +1 -0
  67. package/dist/contracts/schemas/__tests__/igaming-config.test.d.ts +2 -0
  68. package/dist/contracts/schemas/__tests__/igaming-config.test.d.ts.map +1 -0
  69. package/dist/contracts/schemas/__tests__/igaming-config.test.js +38 -0
  70. package/dist/contracts/schemas/__tests__/igaming-config.test.js.map +1 -0
  71. package/dist/contracts/schemas/reporting.d.ts +7 -2
  72. package/dist/contracts/schemas/reporting.d.ts.map +1 -1
  73. package/dist/contracts/schemas/reporting.js +2 -1
  74. package/dist/contracts/schemas/reporting.js.map +1 -1
  75. package/dist/engagement/chat/__tests__/chat.service.test.js +524 -364
  76. package/dist/engagement/chat/__tests__/chat.service.test.js.map +1 -1
  77. package/dist/engagement/notifications/__tests__/notifications.service.test.js +125 -68
  78. package/dist/engagement/notifications/__tests__/notifications.service.test.js.map +1 -1
  79. package/dist/iam/__tests__/iam.service.test.js +367 -596
  80. package/dist/iam/__tests__/iam.service.test.js.map +1 -1
  81. package/dist/pam/identity/__tests__/admin-user-directory.test.js +144 -68
  82. package/dist/pam/identity/__tests__/admin-user-directory.test.js.map +1 -1
  83. package/dist/pam/identity/__tests__/identity.rate-limit.test.js +33 -22
  84. package/dist/pam/identity/__tests__/identity.rate-limit.test.js.map +1 -1
  85. package/dist/pam/identity/__tests__/identity.service.test.js +217 -298
  86. package/dist/pam/identity/__tests__/identity.service.test.js.map +1 -1
  87. package/dist/pam/identity/__tests__/login-enforcement.service.test.js +97 -32
  88. package/dist/pam/identity/__tests__/login-enforcement.service.test.js.map +1 -1
  89. package/dist/pam/identity/__tests__/phone-login.service.test.js +163 -134
  90. package/dist/pam/identity/__tests__/phone-login.service.test.js.map +1 -1
  91. package/dist/pam/identity/__tests__/rg-guard.test.d.ts +2 -0
  92. package/dist/pam/identity/__tests__/rg-guard.test.d.ts.map +1 -0
  93. package/dist/pam/identity/__tests__/rg-guard.test.js +21 -0
  94. package/dist/pam/identity/__tests__/rg-guard.test.js.map +1 -0
  95. package/dist/pam/player-management/__tests__/kyc-status-writer.test.js +73 -26
  96. package/dist/pam/player-management/__tests__/kyc-status-writer.test.js.map +1 -1
  97. package/dist/pam/player-management/__tests__/player.router.test.js +63 -46
  98. package/dist/pam/player-management/__tests__/player.router.test.js.map +1 -1
  99. package/dist/pam/player-management/__tests__/player.service.test.js +207 -111
  100. package/dist/pam/player-management/__tests__/player.service.test.js.map +1 -1
  101. package/dist/pam/profile/__tests__/profile.service.test.js +89 -41
  102. package/dist/pam/profile/__tests__/profile.service.test.js.map +1 -1
  103. package/dist/pam/shared/__tests__/shared-mappers.test.d.ts +2 -0
  104. package/dist/pam/shared/__tests__/shared-mappers.test.d.ts.map +1 -0
  105. package/dist/pam/shared/__tests__/shared-mappers.test.js +55 -0
  106. package/dist/pam/shared/__tests__/shared-mappers.test.js.map +1 -0
  107. package/dist/pam/tag/__tests__/get-active-tag-keys.test.js +72 -30
  108. package/dist/pam/tag/__tests__/get-active-tag-keys.test.js.map +1 -1
  109. package/dist/pam/tag/__tests__/tag-evaluation.service.test.js +308 -235
  110. package/dist/pam/tag/__tests__/tag-evaluation.service.test.js.map +1 -1
  111. package/dist/pam/tag/__tests__/tag-rule.service.test.js +136 -140
  112. package/dist/pam/tag/__tests__/tag-rule.service.test.js.map +1 -1
  113. package/dist/pam/tag/__tests__/tag.router.test.js +76 -25
  114. package/dist/pam/tag/__tests__/tag.router.test.js.map +1 -1
  115. package/dist/pam/tag/__tests__/tag.service.test.js +224 -247
  116. package/dist/pam/tag/__tests__/tag.service.test.js.map +1 -1
  117. package/dist/server/auth/__tests__/admin-guard.test.js +107 -117
  118. package/dist/server/auth/__tests__/admin-guard.test.js.map +1 -1
  119. package/dist/server/auth/__tests__/permission-levels.test.d.ts +2 -0
  120. package/dist/server/auth/__tests__/permission-levels.test.d.ts.map +1 -0
  121. package/dist/server/auth/__tests__/permission-levels.test.js +89 -0
  122. package/dist/server/auth/__tests__/permission-levels.test.js.map +1 -0
  123. package/dist/server/auth/__tests__/session-resolver.test.js +45 -13
  124. package/dist/server/auth/__tests__/session-resolver.test.js.map +1 -1
  125. package/dist/server/db/__tests__/query-helpers.test.js +72 -1
  126. package/dist/server/db/__tests__/query-helpers.test.js.map +1 -1
  127. package/dist/server/db/outbox/__tests__/relay.test.js +74 -81
  128. package/dist/server/db/outbox/__tests__/relay.test.js.map +1 -1
  129. package/dist/server/kernel/__tests__/bullmq-job-queue.test.js +204 -154
  130. package/dist/server/kernel/__tests__/bullmq-job-queue.test.js.map +1 -1
  131. package/dist/server/kernel/__tests__/cache.test.js +26 -13
  132. package/dist/server/kernel/__tests__/cache.test.js.map +1 -1
  133. package/dist/server/kernel/__tests__/domain-error.test.d.ts +2 -0
  134. package/dist/server/kernel/__tests__/domain-error.test.d.ts.map +1 -0
  135. package/dist/server/kernel/__tests__/domain-error.test.js +35 -0
  136. package/dist/server/kernel/__tests__/domain-error.test.js.map +1 -0
  137. package/dist/server/kernel/__tests__/error-reporter.test.d.ts +2 -0
  138. package/dist/server/kernel/__tests__/error-reporter.test.d.ts.map +1 -0
  139. package/dist/server/kernel/__tests__/error-reporter.test.js +52 -0
  140. package/dist/server/kernel/__tests__/error-reporter.test.js.map +1 -0
  141. package/dist/server/kernel/__tests__/event-bus.test.js +60 -20
  142. package/dist/server/kernel/__tests__/event-bus.test.js.map +1 -1
  143. package/dist/server/kernel/__tests__/orpc-error-map.test.d.ts +2 -0
  144. package/dist/server/kernel/__tests__/orpc-error-map.test.d.ts.map +1 -0
  145. package/dist/server/kernel/__tests__/orpc-error-map.test.js +54 -0
  146. package/dist/server/kernel/__tests__/orpc-error-map.test.js.map +1 -0
  147. package/dist/server/kernel/__tests__/platform-config-loader.test.d.ts +2 -0
  148. package/dist/server/kernel/__tests__/platform-config-loader.test.d.ts.map +1 -0
  149. package/dist/server/kernel/__tests__/platform-config-loader.test.js +112 -0
  150. package/dist/server/kernel/__tests__/platform-config-loader.test.js.map +1 -0
  151. package/dist/server/kernel/__tests__/rate-limiter.test.js +22 -13
  152. package/dist/server/kernel/__tests__/rate-limiter.test.js.map +1 -1
  153. package/dist/server/kernel/__tests__/realtime-transport.test.d.ts.map +1 -0
  154. package/dist/{testing/fakes → server/kernel}/__tests__/realtime-transport.test.js.map +1 -1
  155. package/dist/server/kernel/__tests__/redis-cache.test.js +34 -34
  156. package/dist/server/kernel/__tests__/redis-cache.test.js.map +1 -1
  157. package/dist/server/kernel/__tests__/redis-rate-limiter.test.js +49 -44
  158. package/dist/server/kernel/__tests__/redis-rate-limiter.test.js.map +1 -1
  159. package/dist/server/kernel/__tests__/redis-streams-broker.test.js +141 -262
  160. package/dist/server/kernel/__tests__/redis-streams-broker.test.js.map +1 -1
  161. package/dist/server/kernel/__tests__/request-context.test.d.ts +2 -0
  162. package/dist/server/kernel/__tests__/request-context.test.d.ts.map +1 -0
  163. package/dist/server/kernel/__tests__/request-context.test.js +64 -0
  164. package/dist/server/kernel/__tests__/request-context.test.js.map +1 -0
  165. package/dist/server/kernel/__tests__/router-utils.test.d.ts +2 -0
  166. package/dist/server/kernel/__tests__/router-utils.test.d.ts.map +1 -0
  167. package/dist/server/kernel/__tests__/router-utils.test.js +80 -0
  168. package/dist/server/kernel/__tests__/router-utils.test.js.map +1 -0
  169. package/dist/server/kernel/bullmq-job-queue.d.ts.map +1 -1
  170. package/dist/server/kernel/bullmq-job-queue.js +8 -2
  171. package/dist/server/kernel/bullmq-job-queue.js.map +1 -1
  172. package/dist/server/runtime/__tests__/create-app.test.js +26 -18
  173. package/dist/server/runtime/__tests__/create-app.test.js.map +1 -1
  174. package/dist/testing/index.d.ts +2 -6
  175. package/dist/testing/index.d.ts.map +1 -1
  176. package/dist/testing/index.js +2 -10
  177. package/dist/testing/index.js.map +1 -1
  178. package/dist/testing/mock.d.ts +28 -16
  179. package/dist/testing/mock.d.ts.map +1 -1
  180. package/dist/testing/mock.js +58 -37
  181. package/dist/testing/mock.js.map +1 -1
  182. package/dist/testing/real-infra.d.ts +47 -0
  183. package/dist/testing/real-infra.d.ts.map +1 -0
  184. package/dist/testing/real-infra.js +122 -0
  185. package/dist/testing/real-infra.js.map +1 -0
  186. package/dist/wallet/__tests__/admin-reporting.test.js +201 -90
  187. package/dist/wallet/__tests__/admin-reporting.test.js.map +1 -1
  188. package/dist/wallet/__tests__/rail-for.test.d.ts +2 -0
  189. package/dist/wallet/__tests__/rail-for.test.d.ts.map +1 -0
  190. package/dist/wallet/__tests__/rail-for.test.js +22 -0
  191. package/dist/wallet/__tests__/rail-for.test.js.map +1 -0
  192. package/dist/wallet/__tests__/wallet-auto-rule.router.test.js +82 -63
  193. package/dist/wallet/__tests__/wallet-auto-rule.router.test.js.map +1 -1
  194. package/dist/wallet/__tests__/wallet-auto-withdrawal.test.js +414 -437
  195. package/dist/wallet/__tests__/wallet-auto-withdrawal.test.js.map +1 -1
  196. package/dist/wallet/__tests__/wallet-commands.service.test.js +116 -155
  197. package/dist/wallet/__tests__/wallet-commands.service.test.js.map +1 -1
  198. package/dist/wallet/__tests__/wallet-kyc-gate.test.js +63 -42
  199. package/dist/wallet/__tests__/wallet-kyc-gate.test.js.map +1 -1
  200. package/dist/wallet/__tests__/wallet-webhook.router.test.js +129 -51
  201. package/dist/wallet/__tests__/wallet-webhook.router.test.js.map +1 -1
  202. package/dist/wallet/__tests__/wallet.rate-limit.test.js +74 -21
  203. package/dist/wallet/__tests__/wallet.rate-limit.test.js.map +1 -1
  204. package/dist/wallet/__tests__/wallet.router.test.js +56 -36
  205. package/dist/wallet/__tests__/wallet.router.test.js.map +1 -1
  206. package/dist/wallet/__tests__/wallet.service.test.js +623 -887
  207. package/dist/wallet/__tests__/wallet.service.test.js.map +1 -1
  208. package/package.json +1 -2
  209. package/dist/casino/lobby/__tests__/lobby.service.test.d.ts +0 -2
  210. package/dist/casino/lobby/__tests__/lobby.service.test.d.ts.map +0 -1
  211. package/dist/casino/lobby/__tests__/lobby.service.test.js +0 -11
  212. package/dist/casino/lobby/__tests__/lobby.service.test.js.map +0 -1
  213. package/dist/contracts/schemas/__tests__/identity-schemas.test.d.ts +0 -2
  214. package/dist/contracts/schemas/__tests__/identity-schemas.test.d.ts.map +0 -1
  215. package/dist/contracts/schemas/__tests__/identity-schemas.test.js +0 -43
  216. package/dist/contracts/schemas/__tests__/identity-schemas.test.js.map +0 -1
  217. package/dist/testing/fakes/__tests__/cache.test.d.ts +0 -2
  218. package/dist/testing/fakes/__tests__/cache.test.d.ts.map +0 -1
  219. package/dist/testing/fakes/__tests__/cache.test.js +0 -89
  220. package/dist/testing/fakes/__tests__/cache.test.js.map +0 -1
  221. package/dist/testing/fakes/__tests__/job-queue.test.d.ts +0 -2
  222. package/dist/testing/fakes/__tests__/job-queue.test.d.ts.map +0 -1
  223. package/dist/testing/fakes/__tests__/job-queue.test.js +0 -135
  224. package/dist/testing/fakes/__tests__/job-queue.test.js.map +0 -1
  225. package/dist/testing/fakes/__tests__/rate-limiter.test.d.ts +0 -2
  226. package/dist/testing/fakes/__tests__/rate-limiter.test.d.ts.map +0 -1
  227. package/dist/testing/fakes/__tests__/rate-limiter.test.js +0 -83
  228. package/dist/testing/fakes/__tests__/rate-limiter.test.js.map +0 -1
  229. package/dist/testing/fakes/__tests__/realtime-authorizer.test.d.ts +0 -2
  230. package/dist/testing/fakes/__tests__/realtime-authorizer.test.d.ts.map +0 -1
  231. package/dist/testing/fakes/__tests__/realtime-authorizer.test.js +0 -19
  232. package/dist/testing/fakes/__tests__/realtime-authorizer.test.js.map +0 -1
  233. package/dist/testing/fakes/__tests__/realtime-transport.test.d.ts.map +0 -1
  234. package/dist/testing/fakes/cache.d.ts +0 -14
  235. package/dist/testing/fakes/cache.d.ts.map +0 -1
  236. package/dist/testing/fakes/cache.js +0 -56
  237. package/dist/testing/fakes/cache.js.map +0 -1
  238. package/dist/testing/fakes/event-bus.d.ts +0 -17
  239. package/dist/testing/fakes/event-bus.d.ts.map +0 -1
  240. package/dist/testing/fakes/event-bus.js +0 -30
  241. package/dist/testing/fakes/event-bus.js.map +0 -1
  242. package/dist/testing/fakes/job-queue.d.ts +0 -40
  243. package/dist/testing/fakes/job-queue.d.ts.map +0 -1
  244. package/dist/testing/fakes/job-queue.js +0 -200
  245. package/dist/testing/fakes/job-queue.js.map +0 -1
  246. package/dist/testing/fakes/rate-limiter.d.ts +0 -11
  247. package/dist/testing/fakes/rate-limiter.d.ts.map +0 -1
  248. package/dist/testing/fakes/rate-limiter.js +0 -39
  249. package/dist/testing/fakes/rate-limiter.js.map +0 -1
  250. package/dist/testing/fakes/realtime-authorizer.d.ts +0 -10
  251. package/dist/testing/fakes/realtime-authorizer.d.ts.map +0 -1
  252. package/dist/testing/fakes/realtime-authorizer.js +0 -10
  253. package/dist/testing/fakes/realtime-authorizer.js.map +0 -1
  254. package/dist/testing/fakes/realtime-transport.d.ts +0 -2
  255. package/dist/testing/fakes/realtime-transport.d.ts.map +0 -1
  256. package/dist/testing/fakes/realtime-transport.js +0 -2
  257. package/dist/testing/fakes/realtime-transport.js.map +0 -1
  258. /package/dist/{testing/fakes → server/kernel}/__tests__/realtime-transport.test.d.ts +0 -0
  259. /package/dist/{testing/fakes → server/kernel}/__tests__/realtime-transport.test.js +0 -0
@@ -1,270 +1,343 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { mock } from '../../../testing/mock.js';
1
+ import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from 'vitest';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { and, eq, isNull, sql } from 'drizzle-orm';
4
+ import { createTestDb } from '@openora/core/testing';
5
+ import { mock, makeEventBus } from '../../../testing/mock.js';
3
6
  import { TagEvaluationService } from '../service/tag-evaluation.service.js';
4
7
  import { SYSTEM_ACTOR_ID } from '../service/tag-mappers.js';
5
- import { TagAlreadyInUseError, TagAssignmentNotFoundError, } from '../service/tag.service.js';
6
- import { TagRuleNotFoundError } from '../service/tag-rule.service.js';
7
- const UID = '11111111-1111-4111-8111-111111111111';
8
- const TXID = '22222222-2222-4222-8222-222222222222';
9
- function makeRule(overrides = {}) {
10
- return {
11
- id: '00000000-0000-0000-0000-000000000001',
12
- tagId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
13
- tagKey: 'high_roller',
8
+ import { TagService } from '../service/tag.service.js';
9
+ import { TagRuleService } from '../service/tag-rule.service.js';
10
+ import { migrate } from '../migrate.js';
11
+ import { tag, tagRule, playerTag } from '../schema/index.js';
12
+ let db;
13
+ async function seedTagRow(key) {
14
+ const [row] = await db.drizzle.db.insert(tag).values({ key }).returning();
15
+ return row;
16
+ }
17
+ async function seedRule(key, overrides = {}) {
18
+ const tagRow = await seedTagRow(key);
19
+ await db.drizzle.db.insert(tagRule).values({
20
+ tagId: tagRow.id,
14
21
  isEnabled: true,
15
22
  threshold: null,
16
23
  thresholdDays: null,
17
24
  thresholdCount: null,
18
- createdAt: new Date().toISOString(),
19
- updatedAt: new Date().toISOString(),
20
25
  ...overrides,
21
- };
22
- }
23
- /**
24
- * Builds all deps. `rules` maps a tagKey to the rule row getTagRule should
25
- * return; an absent key makes getTagRule throw TagRuleNotFoundError (rule absent).
26
- */
27
- function makeServices(rules = {}) {
28
- const tag = mock({
29
- assignPlayerTag: vi.fn().mockResolvedValue(undefined),
30
- removePlayerTag: vi.fn().mockResolvedValue(undefined),
31
- });
32
- const rule = mock({
33
- getTagRule: vi.fn(async (tagKey) => {
34
- const found = rules[tagKey];
35
- if (!found) {
36
- throw new TagRuleNotFoundError(tagKey);
37
- }
38
- return found;
39
- }),
40
26
  });
27
+ return tagRow;
28
+ }
29
+ async function seedKycPendingRule(overrides = {}) {
30
+ const kycPendingTag = await seedRule('kyc_pending', overrides);
31
+ const kycRejectedTag = await seedTagRow('kyc_rejected');
32
+ return { kycPendingTag, kycRejectedTag };
33
+ }
34
+ async function seedActiveAssignment(playerId, tagRow) {
35
+ const [row] = await db.drizzle.db
36
+ .insert(playerTag)
37
+ .values({
38
+ playerId,
39
+ tagId: tagRow.id,
40
+ assignReason: 'seed fixture',
41
+ assignActor: 'manual',
42
+ assignActorUserId: null,
43
+ })
44
+ .returning();
45
+ return row;
46
+ }
47
+ async function activeTagKeys(playerId) {
48
+ const rows = await db.drizzle.db
49
+ .select({ key: tag.key })
50
+ .from(playerTag)
51
+ .innerJoin(tag, eq(playerTag.tagId, tag.id))
52
+ .where(and(eq(playerTag.playerId, playerId), isNull(playerTag.removedAt)));
53
+ return rows.map((r) => r.key);
54
+ }
55
+ async function assignmentRow(playerId, tagRow) {
56
+ const [row] = await db.drizzle.db
57
+ .select()
58
+ .from(playerTag)
59
+ .where(and(eq(playerTag.playerId, playerId), eq(playerTag.tagId, tagRow.id)));
60
+ return row;
61
+ }
62
+ function makeServices(overrides = {}) {
63
+ const events = makeEventBus();
64
+ const tagService = new TagService(db.drizzle, events);
65
+ const ruleService = new TagRuleService(db.drizzle, events);
41
66
  const walletReader = mock({
42
67
  getLifetimeDeposit: vi.fn().mockResolvedValue('0'),
43
68
  getWithdrawalCountInWindow: vi.fn().mockResolvedValue(0),
69
+ ...overrides.walletReader,
44
70
  });
45
71
  const identityReader = mock({
46
72
  getLastLoginAt: vi.fn().mockResolvedValue(null),
47
73
  getPlayerIdsInactiveSince: vi.fn().mockResolvedValue([]),
48
74
  getPlayerIdByUserId: vi.fn(async (uid) => uid),
75
+ ...overrides.identityReader,
49
76
  });
50
- const service = new TagEvaluationService(tag, rule, walletReader, identityReader);
51
- return { service, tag, rule, walletReader, identityReader };
77
+ const service = new TagEvaluationService(tagService, ruleService, walletReader, identityReader);
78
+ return { service, walletReader, identityReader };
52
79
  }
53
- function depositPayload(amount) {
54
- return { userId: UID, amount: String(amount), currency: 'USD', transactionId: TXID };
80
+ function depositPayload(userId, amount) {
81
+ return { userId, amount: String(amount), currency: 'USD', transactionId: randomUUID() };
55
82
  }
56
- function withdrawalPayload(amount) {
57
- return { userId: UID, amount: String(amount), currency: 'USD', transactionId: TXID };
83
+ function withdrawalPayload(userId, amount) {
84
+ return { userId, amount: String(amount), currency: 'USD', transactionId: randomUUID() };
58
85
  }
59
- function kycUpdatedPayload(status) {
60
- return { userId: UID, actorId: SYSTEM_ACTOR_ID, status, previousStatus: 'pending' };
86
+ function kycUpdatedPayload(userId, status) {
87
+ return { userId, actorId: SYSTEM_ACTOR_ID, status, previousStatus: 'pending' };
61
88
  }
62
- describe('TagEvaluationService', () => {
63
- describe('onDepositCompleted', () => {
64
- it('assigns large_depositor when a single deposit meets the amount threshold', async () => {
65
- const { service, tag } = makeServices({
66
- large_depositor: makeRule({ tagKey: 'large_depositor', threshold: '500' }),
67
- });
68
- await service.onDepositCompleted(depositPayload(500));
69
- expect(tag.assignPlayerTag).toHaveBeenCalledWith(expect.objectContaining({ playerId: UID, tagKey: 'large_depositor' }));
70
- });
71
- it('does not assign large_depositor when the deposit is below threshold', async () => {
72
- const { service, tag } = makeServices({
73
- large_depositor: makeRule({ tagKey: 'large_depositor', threshold: '500' }),
74
- });
75
- await service.onDepositCompleted(depositPayload(499));
76
- expect(tag.assignPlayerTag).not.toHaveBeenCalled();
77
- });
78
- it('assigns high_roller when lifetime deposits meet the threshold', async () => {
79
- const { service, tag, walletReader } = makeServices({
80
- high_roller: makeRule({ tagKey: 'high_roller', threshold: '10000' }),
81
- });
82
- walletReader.getLifetimeDeposit = vi.fn().mockResolvedValue('12000');
83
- await service.onDepositCompleted(depositPayload(100));
84
- expect(tag.assignPlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'high_roller' }));
85
- });
86
- it('removes high_roller when lifetime deposits fall below a raised threshold', async () => {
87
- const { service, tag, walletReader } = makeServices({
88
- high_roller: makeRule({ tagKey: 'high_roller', threshold: '10000' }),
89
- });
90
- walletReader.getLifetimeDeposit = vi.fn().mockResolvedValue('5000');
91
- await service.onDepositCompleted(depositPayload(100));
92
- expect(tag.removePlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'high_roller' }));
93
- expect(tag.assignPlayerTag).not.toHaveBeenCalled();
94
- });
95
- it('does nothing when the rules are disabled', async () => {
96
- const { service, tag, walletReader } = makeServices({
97
- high_roller: makeRule({ tagKey: 'high_roller', threshold: '10000', isEnabled: false }),
98
- large_depositor: makeRule({
99
- tagKey: 'large_depositor',
100
- threshold: '100',
101
- isEnabled: false,
102
- }),
103
- });
104
- await service.onDepositCompleted(depositPayload(99999));
105
- expect(tag.assignPlayerTag).not.toHaveBeenCalled();
106
- expect(tag.removePlayerTag).not.toHaveBeenCalled();
107
- expect(walletReader.getLifetimeDeposit).not.toHaveBeenCalled();
108
- });
89
+ beforeAll(async () => {
90
+ db = await createTestDb([migrate]);
91
+ });
92
+ afterAll(async () => {
93
+ await db.drop();
94
+ });
95
+ beforeEach(async () => {
96
+ await db.drizzle.db.execute(sql `TRUNCATE ${playerTag}, ${tagRule}, ${tag} RESTART IDENTITY CASCADE`);
97
+ });
98
+ describe('TagEvaluationService.onDepositCompleted (real PG)', () => {
99
+ it('assigns large_depositor when a single deposit meets the amount threshold', async () => {
100
+ const userId = randomUUID();
101
+ const { service } = makeServices();
102
+ await seedRule('large_depositor', { threshold: '500' });
103
+ await service.onDepositCompleted(depositPayload(userId, 500));
104
+ expect(await activeTagKeys(userId)).toContain('large_depositor');
109
105
  });
110
- describe('onWithdrawalCompleted', () => {
111
- it('assigns high_risk when a single withdrawal meets the amount threshold', async () => {
112
- const { service, tag } = makeServices({
113
- high_risk: makeRule({ tagKey: 'high_risk', threshold: '1000' }),
114
- });
115
- await service.onWithdrawalCompleted(withdrawalPayload(1500));
116
- expect(tag.assignPlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'high_risk' }));
117
- });
118
- it('assigns high_risk when the withdrawal count in the window meets the threshold', async () => {
119
- const { service, tag, walletReader } = makeServices({
120
- high_risk: makeRule({
121
- tagKey: 'high_risk',
122
- threshold: '1000',
123
- thresholdDays: 7,
124
- thresholdCount: 3,
125
- }),
126
- });
127
- walletReader.getWithdrawalCountInWindow = vi.fn().mockResolvedValue(3);
128
- await service.onWithdrawalCompleted(withdrawalPayload(10));
129
- expect(walletReader.getWithdrawalCountInWindow).toHaveBeenCalledWith(UID, 7);
130
- expect(tag.assignPlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'high_risk' }));
131
- });
132
- it('takes no action when neither threshold is met (risk designation requires admin clear)', async () => {
133
- const { service, tag, walletReader } = makeServices({
134
- high_risk: makeRule({
135
- tagKey: 'high_risk',
136
- threshold: '1000',
137
- thresholdDays: 7,
138
- thresholdCount: 3,
139
- }),
140
- });
141
- walletReader.getWithdrawalCountInWindow = vi.fn().mockResolvedValue(1);
142
- await service.onWithdrawalCompleted(withdrawalPayload(10));
143
- expect(tag.assignPlayerTag).not.toHaveBeenCalled();
144
- expect(tag.removePlayerTag).not.toHaveBeenCalled();
145
- });
146
- it('skips the count check when thresholdDays or thresholdCount is null', async () => {
147
- const { service, walletReader } = makeServices({
148
- high_risk: makeRule({ tagKey: 'high_risk', threshold: '1000', thresholdDays: null }),
149
- });
150
- await service.onWithdrawalCompleted(withdrawalPayload(10));
151
- expect(walletReader.getWithdrawalCountInWindow).not.toHaveBeenCalled();
152
- });
106
+ it('does not assign large_depositor when the deposit is below threshold', async () => {
107
+ const userId = randomUUID();
108
+ const { service } = makeServices();
109
+ await seedRule('large_depositor', { threshold: '500' });
110
+ await service.onDepositCompleted(depositPayload(userId, 499));
111
+ expect(await activeTagKeys(userId)).not.toContain('large_depositor');
153
112
  });
154
- describe('onUserLogin', () => {
155
- it('tries to remove the inactive tag', async () => {
156
- const { service, tag } = makeServices();
157
- await service.onUserLogin({ userId: UID });
158
- expect(tag.removePlayerTag).toHaveBeenCalledWith(expect.objectContaining({ playerId: UID, tagKey: 'inactive', removalActor: 'scheduled' }));
113
+ it('assigns high_roller when lifetime deposits meet the threshold', async () => {
114
+ const userId = randomUUID();
115
+ const { service } = makeServices({
116
+ walletReader: { getLifetimeDeposit: vi.fn().mockResolvedValue('12000') },
159
117
  });
118
+ await seedRule('high_roller', { threshold: '10000' });
119
+ await service.onDepositCompleted(depositPayload(userId, 100));
120
+ expect(await activeTagKeys(userId)).toContain('high_roller');
160
121
  });
161
- describe('onKycSubmitted', () => {
162
- const kycRule = { kyc_pending: makeRule({ tagKey: 'kyc_pending' }) };
163
- it('assigns kyc_pending when the rule is enabled', async () => {
164
- const { service, tag } = makeServices(kycRule);
165
- await service.onKycSubmitted({ userId: UID, referenceId: 'ref-1', provider: 'sumsub' });
166
- expect(tag.assignPlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'kyc_pending' }));
167
- });
168
- it('also tries to remove kyc_rejected on the resubmission path', async () => {
169
- const { service, tag } = makeServices(kycRule);
170
- await service.onKycSubmitted({ userId: UID, referenceId: 'ref-1', provider: 'sumsub' });
171
- expect(tag.removePlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'kyc_rejected' }));
172
- });
173
- it('does nothing when kyc_pending rule is disabled', async () => {
174
- const { service, tag } = makeServices({
175
- kyc_pending: makeRule({ tagKey: 'kyc_pending', isEnabled: false }),
176
- });
177
- await service.onKycSubmitted({ userId: UID, referenceId: 'ref-1', provider: 'sumsub' });
178
- expect(tag.assignPlayerTag).not.toHaveBeenCalled();
179
- });
180
- it('is idempotent when kyc_pending is already assigned (at-least-once delivery)', async () => {
181
- const { service, tag } = makeServices(kycRule);
182
- tag.assignPlayerTag = vi.fn().mockRejectedValue(new TagAlreadyInUseError());
183
- await expect(service.onKycSubmitted({ userId: UID, referenceId: 'ref-1', provider: 'sumsub' })).resolves.toBeUndefined();
122
+ it('removes an active high_roller assignment when lifetime deposits fall below a raised threshold', async () => {
123
+ const userId = randomUUID();
124
+ const { service } = makeServices({
125
+ walletReader: { getLifetimeDeposit: vi.fn().mockResolvedValue('5000') },
184
126
  });
127
+ const highRollerTag = await seedRule('high_roller', { threshold: '10000' });
128
+ await seedActiveAssignment(userId, highRollerTag);
129
+ await service.onDepositCompleted(depositPayload(userId, 100));
130
+ expect(await activeTagKeys(userId)).not.toContain('high_roller');
131
+ const row = await assignmentRow(userId, highRollerTag);
132
+ expect(row?.removedAt).toBeInstanceOf(Date);
133
+ expect(row).toMatchObject({ removalActor: 'scheduled', removalActorUserId: SYSTEM_ACTOR_ID });
185
134
  });
186
- describe('onKycStatusUpdated', () => {
187
- const kycRule = { kyc_pending: makeRule({ tagKey: 'kyc_pending' }) };
188
- it('removes kyc_pending on verified', async () => {
189
- const { service, tag } = makeServices(kycRule);
190
- await service.onKycStatusUpdated(kycUpdatedPayload('verified'));
191
- expect(tag.removePlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'kyc_pending' }));
192
- expect(tag.assignPlayerTag).not.toHaveBeenCalled();
193
- });
194
- it('removes kyc_pending and assigns kyc_rejected on rejected', async () => {
195
- const { service, tag } = makeServices(kycRule);
196
- await service.onKycStatusUpdated(kycUpdatedPayload('rejected'));
197
- expect(tag.removePlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'kyc_pending' }));
198
- expect(tag.assignPlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'kyc_rejected' }));
199
- });
200
- it('assigns kyc_pending on resubmission_requested', async () => {
201
- const { service, tag } = makeServices(kycRule);
202
- await service.onKycStatusUpdated(kycUpdatedPayload('resubmission_requested'));
203
- expect(tag.assignPlayerTag).toHaveBeenCalledWith(expect.objectContaining({ tagKey: 'kyc_pending' }));
204
- });
205
- it('does nothing when kyc_pending rule is disabled', async () => {
206
- const { service, tag } = makeServices({
207
- kyc_pending: makeRule({ tagKey: 'kyc_pending', isEnabled: false }),
208
- });
209
- await service.onKycStatusUpdated(kycUpdatedPayload('verified'));
210
- expect(tag.removePlayerTag).not.toHaveBeenCalled();
211
- });
212
- it('is idempotent when verified fires twice (tags already removed)', async () => {
213
- const { service, tag } = makeServices(kycRule);
214
- tag.removePlayerTag = vi.fn().mockRejectedValue(new TagAssignmentNotFoundError(UID));
215
- await expect(service.onKycStatusUpdated(kycUpdatedPayload('verified'))).resolves.toBeUndefined();
216
- });
217
- it('is idempotent when rejected fires twice (kyc_pending gone, kyc_rejected already present)', async () => {
218
- const { service, tag } = makeServices(kycRule);
219
- tag.removePlayerTag = vi.fn().mockRejectedValue(new TagAssignmentNotFoundError(UID));
220
- tag.assignPlayerTag = vi.fn().mockRejectedValue(new TagAlreadyInUseError());
221
- await expect(service.onKycStatusUpdated(kycUpdatedPayload('rejected'))).resolves.toBeUndefined();
222
- });
223
- it('is idempotent when resubmission_requested fires twice (kyc_pending already assigned)', async () => {
224
- const { service, tag } = makeServices(kycRule);
225
- tag.assignPlayerTag = vi.fn().mockRejectedValue(new TagAlreadyInUseError());
226
- await expect(service.onKycStatusUpdated(kycUpdatedPayload('resubmission_requested'))).resolves.toBeUndefined();
227
- });
135
+ it('does nothing when the rules are disabled', async () => {
136
+ const userId = randomUUID();
137
+ const { service, walletReader } = makeServices();
138
+ await seedRule('high_roller', { threshold: '10000', isEnabled: false });
139
+ await seedRule('large_depositor', { threshold: '100', isEnabled: false });
140
+ await service.onDepositCompleted(depositPayload(userId, 99999));
141
+ expect(await activeTagKeys(userId)).toEqual([]);
142
+ expect(walletReader.getLifetimeDeposit).not.toHaveBeenCalled();
228
143
  });
229
- describe('runDailyEvaluation', () => {
230
- it('assigns inactive to each player returned by the identity reader', async () => {
231
- const { service, tag, identityReader } = makeServices({
232
- inactive: makeRule({ tagKey: 'inactive', thresholdDays: 30 }),
233
- });
234
- identityReader.getPlayerIdsInactiveSince = vi.fn().mockResolvedValue(['p1', 'p2']);
235
- await service.runDailyEvaluation();
236
- expect(tag.assignPlayerTag).toHaveBeenCalledTimes(2);
237
- expect(tag.assignPlayerTag).toHaveBeenCalledWith(expect.objectContaining({ playerId: 'p1', tagKey: 'inactive' }));
238
- expect(tag.assignPlayerTag).toHaveBeenCalledWith(expect.objectContaining({ playerId: 'p2', tagKey: 'inactive' }));
239
- });
240
- it('returns early when the rule is disabled', async () => {
241
- const { service, identityReader } = makeServices({
242
- inactive: makeRule({ tagKey: 'inactive', thresholdDays: 30, isEnabled: false }),
243
- });
244
- await service.runDailyEvaluation();
245
- expect(identityReader.getPlayerIdsInactiveSince).not.toHaveBeenCalled();
246
- });
247
- it('returns early when thresholdDays is null', async () => {
248
- const { service, identityReader } = makeServices({
249
- inactive: makeRule({ tagKey: 'inactive', thresholdDays: null }),
250
- });
251
- await service.runDailyEvaluation();
252
- expect(identityReader.getPlayerIdsInactiveSince).not.toHaveBeenCalled();
144
+ });
145
+ describe('TagEvaluationService.onWithdrawalCompleted (real PG)', () => {
146
+ it('assigns high_risk when a single withdrawal meets the amount threshold', async () => {
147
+ const userId = randomUUID();
148
+ const { service } = makeServices();
149
+ await seedRule('high_risk', { threshold: '1000' });
150
+ await service.onWithdrawalCompleted(withdrawalPayload(userId, 1500));
151
+ expect(await activeTagKeys(userId)).toContain('high_risk');
152
+ });
153
+ it('assigns high_risk when the withdrawal count in the window meets the threshold', async () => {
154
+ const userId = randomUUID();
155
+ const { service, walletReader } = makeServices({
156
+ walletReader: { getWithdrawalCountInWindow: vi.fn().mockResolvedValue(3) },
253
157
  });
158
+ await seedRule('high_risk', { threshold: '1000', thresholdDays: 7, thresholdCount: 3 });
159
+ await service.onWithdrawalCompleted(withdrawalPayload(userId, 10));
160
+ expect(walletReader.getWithdrawalCountInWindow).toHaveBeenCalledWith(userId, 7);
161
+ expect(await activeTagKeys(userId)).toContain('high_risk');
254
162
  });
255
- describe('idempotency', () => {
256
- it('tryAssignTag swallows TagAlreadyInUseError', async () => {
257
- const { service, tag } = makeServices({
258
- large_depositor: makeRule({ tagKey: 'large_depositor', threshold: '500' }),
259
- });
260
- tag.assignPlayerTag = vi.fn().mockRejectedValue(new TagAlreadyInUseError());
261
- await expect(service.onDepositCompleted(depositPayload(500))).resolves.toBeUndefined();
163
+ it('takes no action when neither threshold is met (risk designation requires admin clear)', async () => {
164
+ const userId = randomUUID();
165
+ const { service } = makeServices({
166
+ walletReader: { getWithdrawalCountInWindow: vi.fn().mockResolvedValue(1) },
262
167
  });
263
- it('tryRemoveTag swallows TagAssignmentNotFoundError', async () => {
264
- const { service, tag } = makeServices();
265
- tag.removePlayerTag = vi.fn().mockRejectedValue(new TagAssignmentNotFoundError(UID));
266
- await expect(service.onUserLogin({ userId: UID })).resolves.toBeUndefined();
168
+ await seedRule('high_risk', { threshold: '1000', thresholdDays: 7, thresholdCount: 3 });
169
+ await service.onWithdrawalCompleted(withdrawalPayload(userId, 10));
170
+ expect(await activeTagKeys(userId)).toEqual([]);
171
+ });
172
+ it('skips the count check when thresholdDays or thresholdCount is null', async () => {
173
+ const userId = randomUUID();
174
+ const { service, walletReader } = makeServices();
175
+ await seedRule('high_risk', { threshold: '1000', thresholdDays: null });
176
+ await service.onWithdrawalCompleted(withdrawalPayload(userId, 10));
177
+ expect(walletReader.getWithdrawalCountInWindow).not.toHaveBeenCalled();
178
+ });
179
+ });
180
+ describe('TagEvaluationService.onUserLogin (real PG)', () => {
181
+ it('removes an active inactive-tag assignment when the player logs in', async () => {
182
+ const userId = randomUUID();
183
+ const { service } = makeServices();
184
+ const inactiveTag = await seedTagRow('inactive');
185
+ await seedActiveAssignment(userId, inactiveTag);
186
+ await service.onUserLogin({ userId });
187
+ expect(await activeTagKeys(userId)).not.toContain('inactive');
188
+ const row = await assignmentRow(userId, inactiveTag);
189
+ expect(row).toMatchObject({ removalActor: 'scheduled', removalActorUserId: SYSTEM_ACTOR_ID });
190
+ });
191
+ });
192
+ describe('TagEvaluationService.onKycSubmitted (real PG)', () => {
193
+ it('assigns kyc_pending when the rule is enabled', async () => {
194
+ const userId = randomUUID();
195
+ const { service } = makeServices();
196
+ await seedKycPendingRule();
197
+ await service.onKycSubmitted({ userId, referenceId: 'ref-1', provider: 'sumsub' });
198
+ expect(await activeTagKeys(userId)).toContain('kyc_pending');
199
+ });
200
+ it('removes an existing kyc_rejected assignment on resubmission', async () => {
201
+ const userId = randomUUID();
202
+ const { service } = makeServices();
203
+ const { kycRejectedTag } = await seedKycPendingRule();
204
+ await seedActiveAssignment(userId, kycRejectedTag);
205
+ await service.onKycSubmitted({ userId, referenceId: 'ref-1', provider: 'sumsub' });
206
+ expect(await activeTagKeys(userId)).toContain('kyc_pending');
207
+ expect(await activeTagKeys(userId)).not.toContain('kyc_rejected');
208
+ });
209
+ it('does nothing when kyc_pending rule is disabled', async () => {
210
+ const userId = randomUUID();
211
+ const { service } = makeServices();
212
+ await seedRule('kyc_pending', { isEnabled: false });
213
+ await service.onKycSubmitted({ userId, referenceId: 'ref-1', provider: 'sumsub' });
214
+ expect(await activeTagKeys(userId)).toEqual([]);
215
+ });
216
+ it('is idempotent when kyc_pending is already assigned (at-least-once delivery)', async () => {
217
+ const userId = randomUUID();
218
+ const { service } = makeServices();
219
+ const { kycPendingTag } = await seedKycPendingRule();
220
+ await seedActiveAssignment(userId, kycPendingTag);
221
+ await expect(service.onKycSubmitted({ userId, referenceId: 'ref-1', provider: 'sumsub' })).resolves.toBeUndefined();
222
+ const rows = await db.drizzle.db
223
+ .select()
224
+ .from(playerTag)
225
+ .where(and(eq(playerTag.playerId, userId), eq(playerTag.tagId, kycPendingTag.id)));
226
+ expect(rows).toHaveLength(1);
227
+ });
228
+ });
229
+ describe('TagEvaluationService.onKycStatusUpdated (real PG)', () => {
230
+ it('removes kyc_pending on verified', async () => {
231
+ const userId = randomUUID();
232
+ const { service } = makeServices();
233
+ const { kycPendingTag } = await seedKycPendingRule();
234
+ await seedActiveAssignment(userId, kycPendingTag);
235
+ await service.onKycStatusUpdated(kycUpdatedPayload(userId, 'verified'));
236
+ expect(await activeTagKeys(userId)).toEqual([]);
237
+ });
238
+ it('removes kyc_pending and assigns kyc_rejected on rejected', async () => {
239
+ const userId = randomUUID();
240
+ const { service } = makeServices();
241
+ const { kycPendingTag } = await seedKycPendingRule();
242
+ await seedActiveAssignment(userId, kycPendingTag);
243
+ await service.onKycStatusUpdated(kycUpdatedPayload(userId, 'rejected'));
244
+ expect(await activeTagKeys(userId)).toEqual(['kyc_rejected']);
245
+ });
246
+ it('assigns kyc_pending on resubmission_requested', async () => {
247
+ const userId = randomUUID();
248
+ const { service } = makeServices();
249
+ await seedKycPendingRule();
250
+ await service.onKycStatusUpdated(kycUpdatedPayload(userId, 'resubmission_requested'));
251
+ expect(await activeTagKeys(userId)).toContain('kyc_pending');
252
+ });
253
+ it('does nothing when kyc_pending rule is disabled', async () => {
254
+ const userId = randomUUID();
255
+ const { service } = makeServices();
256
+ await seedRule('kyc_pending', { isEnabled: false });
257
+ await service.onKycStatusUpdated(kycUpdatedPayload(userId, 'verified'));
258
+ expect(await activeTagKeys(userId)).toEqual([]);
259
+ });
260
+ it('is idempotent when verified fires twice (tags already removed)', async () => {
261
+ const userId = randomUUID();
262
+ const { service } = makeServices();
263
+ await seedKycPendingRule();
264
+ await expect(service.onKycStatusUpdated(kycUpdatedPayload(userId, 'verified'))).resolves.toBeUndefined();
265
+ expect(await activeTagKeys(userId)).toEqual([]);
266
+ });
267
+ it('is idempotent when rejected fires twice (kyc_pending gone, kyc_rejected already present)', async () => {
268
+ const userId = randomUUID();
269
+ const { service } = makeServices();
270
+ const { kycRejectedTag } = await seedKycPendingRule();
271
+ await seedActiveAssignment(userId, kycRejectedTag);
272
+ await expect(service.onKycStatusUpdated(kycUpdatedPayload(userId, 'rejected'))).resolves.toBeUndefined();
273
+ const rows = await db.drizzle.db
274
+ .select()
275
+ .from(playerTag)
276
+ .where(and(eq(playerTag.playerId, userId), eq(playerTag.tagId, kycRejectedTag.id)));
277
+ expect(rows).toHaveLength(1);
278
+ });
279
+ it('is idempotent when resubmission_requested fires twice (kyc_pending already assigned)', async () => {
280
+ const userId = randomUUID();
281
+ const { service } = makeServices();
282
+ const { kycPendingTag } = await seedKycPendingRule();
283
+ await seedActiveAssignment(userId, kycPendingTag);
284
+ await expect(service.onKycStatusUpdated(kycUpdatedPayload(userId, 'resubmission_requested'))).resolves.toBeUndefined();
285
+ const rows = await db.drizzle.db
286
+ .select()
287
+ .from(playerTag)
288
+ .where(and(eq(playerTag.playerId, userId), eq(playerTag.tagId, kycPendingTag.id)));
289
+ expect(rows).toHaveLength(1);
290
+ });
291
+ });
292
+ describe('TagEvaluationService.runDailyEvaluation (real PG)', () => {
293
+ it('assigns inactive to each player returned by the identity reader, exactly once each', async () => {
294
+ const playerA = randomUUID();
295
+ const playerB = randomUUID();
296
+ const inactiveTag = await seedRule('inactive', { thresholdDays: 30 });
297
+ const { service } = makeServices({
298
+ identityReader: { getPlayerIdsInactiveSince: vi.fn().mockResolvedValue([playerA, playerB]) },
267
299
  });
300
+ await service.runDailyEvaluation();
301
+ expect(await activeTagKeys(playerA)).toContain('inactive');
302
+ expect(await activeTagKeys(playerB)).toContain('inactive');
303
+ const rows = await db.drizzle.db
304
+ .select()
305
+ .from(playerTag)
306
+ .where(eq(playerTag.tagId, inactiveTag.id));
307
+ expect(rows).toHaveLength(2);
308
+ });
309
+ it('returns early when the rule is disabled', async () => {
310
+ await seedRule('inactive', { thresholdDays: 30, isEnabled: false });
311
+ const { service, identityReader } = makeServices();
312
+ await service.runDailyEvaluation();
313
+ expect(identityReader.getPlayerIdsInactiveSince).not.toHaveBeenCalled();
314
+ });
315
+ it('returns early when thresholdDays is null', async () => {
316
+ await seedRule('inactive', { thresholdDays: null });
317
+ const { service, identityReader } = makeServices();
318
+ await service.runDailyEvaluation();
319
+ expect(identityReader.getPlayerIdsInactiveSince).not.toHaveBeenCalled();
320
+ });
321
+ });
322
+ describe('TagEvaluationService idempotency (real PG)', () => {
323
+ it('swallows TagAlreadyInUseError when the tag is already active', async () => {
324
+ const userId = randomUUID();
325
+ const { service } = makeServices();
326
+ const largeDepositorTag = await seedRule('large_depositor', { threshold: '500' });
327
+ await seedActiveAssignment(userId, largeDepositorTag);
328
+ await expect(service.onDepositCompleted(depositPayload(userId, 500))).resolves.toBeUndefined();
329
+ const rows = await db.drizzle.db
330
+ .select()
331
+ .from(playerTag)
332
+ .where(eq(playerTag.tagId, largeDepositorTag.id));
333
+ expect(rows).toHaveLength(1);
334
+ });
335
+ it('swallows TagAssignmentNotFoundError when nothing is assigned to remove', async () => {
336
+ const userId = randomUUID();
337
+ const { service } = makeServices();
338
+ await seedTagRow('inactive');
339
+ await expect(service.onUserLogin({ userId })).resolves.toBeUndefined();
340
+ expect(await activeTagKeys(userId)).toEqual([]);
268
341
  });
269
342
  });
270
343
  //# sourceMappingURL=tag-evaluation.service.test.js.map