@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,284 +1,326 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { domainEventSchemas } from '@openora/core/contracts';
3
- import { mock, mockDb } from '../../testing/mock.js';
1
+ import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from 'vitest';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { eq, sql } from 'drizzle-orm';
4
+ import { createTestDb } from '@openora/core/testing';
5
+ import { mock, makeEventBus } from '../../testing/mock.js';
6
+ import { migrate } from '../migrate.js';
4
7
  import { userLimit, rgExclusion } from '../schema/index.js';
5
8
  import { RgService, ActiveExclusionError, PermanentExclusionLiftError, ExclusionPeriodNotElapsedError, ExclusionNotFoundError, } from '../service/rg.service.js';
6
- // Routing Drizzle stub. `select().from(t)` resolves the next value from that table's
7
- // queue (so the pre-write check and the enforcement recompute get distinct rows);
8
- // `transaction(cb)` runs inline; `insert`/`update` `.returning()` yield `returning`.
9
- function routingDb(cfg) {
10
- const queues = new Map([
11
- [rgExclusion, [...(cfg.rgExclusion ?? [])]],
12
- [userLimit, [...(cfg.userLimit ?? [])]],
13
- ]);
14
- function selectChain() {
15
- let table;
16
- const c = {
17
- from: (t) => {
18
- table = t;
19
- return c;
20
- },
21
- innerJoin: () => c,
22
- where: () => c,
23
- orderBy: () => c,
24
- limit: () => c,
25
- offset: () => c,
26
- then: (resolve) => {
27
- const q = queues.get(table);
28
- resolve(q && q.length > 0 ? q.shift() : []);
29
- },
30
- };
31
- return c;
32
- }
33
- const returning = () => Promise.resolve(cfg.returning ?? []);
34
- const db = {
35
- transaction: (cb) => cb(db),
36
- select: () => selectChain(),
37
- insert: () => {
38
- const c = {
39
- values: () => c,
40
- onConflictDoUpdate: () => c,
41
- returning,
42
- then: (r) => r(undefined),
43
- };
44
- return c;
45
- },
46
- update: () => {
47
- const c = {
48
- set: () => c,
49
- where: () => c,
50
- returning,
51
- then: (r) => r(undefined),
52
- };
53
- return c;
54
- },
55
- };
56
- return mockDb(db);
57
- }
58
- function makeEvents() {
59
- return { emit: vi.fn(), on: vi.fn() };
60
- }
61
- function makeEnforcement() {
9
+ let db;
10
+ function makeNotifier(email = 'player@example.com') {
62
11
  return {
63
- block: vi.fn().mockResolvedValue(undefined),
64
- unblock: vi.fn().mockResolvedValue(undefined),
12
+ email: mock({ send: vi.fn(async () => undefined) }),
13
+ directory: mock({
14
+ lookupPlayers: vi.fn(async (ids) => ids.map((userId) => mock({ userId, email, language: 'en' }))),
15
+ }),
16
+ templateRenderer: mock({
17
+ render: vi.fn(async () => ({ subject: 'subject', body: 'body' })),
18
+ }),
65
19
  };
66
20
  }
67
- function newSvc(db, enforcement = makeEnforcement(), events = makeEvents()) {
21
+ function makeService(notifier) {
22
+ const events = makeEventBus();
23
+ const enforcement = mock({
24
+ block: vi.fn(async () => undefined),
25
+ unblock: vi.fn(async () => undefined),
26
+ });
68
27
  const svc = new RgService({
69
- drizzle: db,
70
- events: mock(events),
28
+ drizzle: db.drizzle,
29
+ events,
71
30
  loginEnforcement: enforcement,
72
- email: null,
73
- directory: null,
31
+ email: notifier?.email ?? null,
32
+ directory: notifier?.directory ?? null,
33
+ templateRenderer: notifier?.templateRenderer ?? null,
74
34
  });
75
35
  return { svc, events, enforcement };
76
36
  }
77
- function exclusionRow(overrides = {}) {
78
- const now = new Date();
79
- return {
80
- id: 'excl-1',
81
- userId: 'user-1',
37
+ async function seedExclusion(overrides) {
38
+ const [row] = await db.drizzle.db
39
+ .insert(rgExclusion)
40
+ .values({
41
+ userId: randomUUID(),
82
42
  kind: 'self_exclusion',
83
43
  status: 'active',
84
44
  reason: 'gambling concern',
85
45
  isPermanent: false,
86
- startsAt: now,
87
- expiresAt: null,
88
- liftedAt: null,
89
- liftedReason: null,
90
- liftedBy: null,
91
- createdBy: 'admin-1',
92
- createdAt: now,
93
- updatedAt: now,
46
+ createdBy: randomUUID(),
94
47
  ...overrides,
95
- };
48
+ })
49
+ .returning();
50
+ return row;
96
51
  }
97
- const USER = 'user-1';
98
- const ADMIN = 'admin-1';
99
- const USER_UUID = '11111111-1111-4111-8111-111111111111';
100
- const ADMIN_UUID = '22222222-2222-4222-8222-222222222222';
101
- const EXCLUSION_UUID = '33333333-3333-4333-8333-333333333333';
102
- const future = () => new Date(Date.now() + 200 * 24 * 3600_000);
103
- describe('RgService.setPlayerLimit', () => {
104
- it('emits rg.limit.set with the prior amount and returns the DTO', async () => {
105
- const row = {
106
- id: 'lim-1',
107
- userId: USER,
108
- type: 'deposit',
109
- amount: '100',
110
- minutes: null,
111
- period: 'daily',
112
- createdAt: new Date(),
113
- };
114
- const { svc, events } = newSvc(routingDb({ userLimit: [[{ amount: '50', minutes: null }]], returning: [row] }));
115
- const dto = await svc.setPlayerLimit(USER, { userId: USER, type: 'deposit', amount: '100', minutes: null, period: 'daily' }, ADMIN);
116
- expect(dto.id).toBe('lim-1');
117
- expect(events.emit).toHaveBeenCalledWith('rg.limit.set', expect.objectContaining({
118
- userId: USER,
119
- actorId: ADMIN,
120
- amount: '100',
121
- previousAmount: '50',
122
- }));
52
+ async function exclusionsOf(userId) {
53
+ return db.drizzle.db.select().from(rgExclusion).where(eq(rgExclusion.userId, userId));
54
+ }
55
+ const HOURS = 3600_000;
56
+ const future = () => new Date(Date.now() + 200 * 24 * HOURS);
57
+ const past = () => new Date(Date.now() - 1000);
58
+ beforeAll(async () => {
59
+ db = await createTestDb([migrate]);
60
+ });
61
+ afterAll(async () => {
62
+ await db.drop();
63
+ });
64
+ beforeEach(async () => {
65
+ await db.drizzle.db.execute(sql `TRUNCATE ${rgExclusion}, ${userLimit} RESTART IDENTITY CASCADE`);
66
+ });
67
+ describe('RgService.setPlayerLimit (real PG)', () => {
68
+ it('persists a first limit and reports no prior amount', async () => {
69
+ const { svc, events } = makeService();
70
+ const userId = randomUUID();
71
+ const actorId = randomUUID();
72
+ const dto = await svc.setPlayerLimit(userId, { userId, type: 'deposit', amount: '100', minutes: null, period: 'daily' }, actorId);
73
+ expect(Number(dto.amount)).toBe(100);
74
+ expect(events.emit).toHaveBeenCalledWith('rg.limit.set', expect.objectContaining({ userId, actorId, amount: '100', previousAmount: null }));
75
+ });
76
+ it('upserts on the same type and period, carrying the prior amount into the event', async () => {
77
+ const { svc, events } = makeService();
78
+ const userId = randomUUID();
79
+ const actorId = randomUUID();
80
+ await svc.setPlayerLimit(userId, { userId, type: 'deposit', amount: '50', minutes: null, period: 'daily' }, actorId);
81
+ await svc.setPlayerLimit(userId, { userId, type: 'deposit', amount: '100', minutes: null, period: 'daily' }, actorId);
82
+ const rows = await db.drizzle.db.select().from(userLimit).where(eq(userLimit.userId, userId));
83
+ expect(rows).toHaveLength(1);
84
+ expect(Number(rows[0]?.amount)).toBe(100);
85
+ expect(events.emit).toHaveBeenLastCalledWith('rg.limit.set', expect.objectContaining({ previousAmount: '50.00' }));
86
+ });
87
+ it('keeps a different period as its own row', async () => {
88
+ const { svc } = makeService();
89
+ const userId = randomUUID();
90
+ const actorId = randomUUID();
91
+ await svc.setPlayerLimit(userId, { userId, type: 'deposit', amount: '50', minutes: null, period: 'daily' }, actorId);
92
+ await svc.setPlayerLimit(userId, { userId, type: 'deposit', amount: '500', minutes: null, period: 'monthly' }, actorId);
93
+ const rows = await db.drizzle.db.select().from(userLimit).where(eq(userLimit.userId, userId));
94
+ expect(rows).toHaveLength(2);
95
+ });
96
+ it('stores minutes and no amount for a session limit', async () => {
97
+ const { svc } = makeService();
98
+ const userId = randomUUID();
99
+ const dto = await svc.setPlayerLimit(userId, { userId, type: 'session', amount: null, minutes: 60, period: 'session' }, randomUUID());
100
+ expect(dto).toMatchObject({ minutes: 60, amount: null });
101
+ });
102
+ it('emails the player when a mail port, directory, and template renderer are bound', async () => {
103
+ const notifier = makeNotifier();
104
+ const { svc } = makeService(notifier);
105
+ const userId = randomUUID();
106
+ await svc.setPlayerLimit(userId, { userId, type: 'deposit', amount: '100', minutes: null, period: 'daily' }, randomUUID());
107
+ expect(notifier.templateRenderer.render).toHaveBeenCalled();
108
+ expect(notifier.email.send).toHaveBeenCalledWith(expect.objectContaining({ to: 'player@example.com', subject: 'subject', body: 'body' }));
109
+ });
110
+ it('swallows a notification failure once the limit has committed', async () => {
111
+ const notifier = makeNotifier();
112
+ vi.mocked(notifier.email.send).mockRejectedValueOnce(new Error('smtp down'));
113
+ const { svc } = makeService(notifier);
114
+ const userId = randomUUID();
115
+ await expect(svc.setPlayerLimit(userId, { userId, type: 'deposit', amount: '100', minutes: null, period: 'daily' }, randomUUID())).resolves.toMatchObject({ period: 'daily' });
116
+ const rows = await db.drizzle.db.select().from(userLimit).where(eq(userLimit.userId, userId));
117
+ expect(rows).toHaveLength(1);
123
118
  });
124
119
  });
125
- describe('RgService.activateCoolingOff', () => {
126
- it('blocks login until the expiry and emits the activation event', async () => {
127
- const row = exclusionRow({ kind: 'cooling_off', expiresAt: future() });
128
- const db = routingDb({
129
- rgExclusion: [[], [{ kind: 'cooling_off', expiresAt: future() }]],
130
- returning: [row],
131
- });
132
- const { svc, events, enforcement } = newSvc(db);
133
- await svc.activateCoolingOff(USER, { userId: USER, durationHours: 24, reason: 'break' }, ADMIN);
134
- expect(enforcement.block).toHaveBeenCalledWith(USER, { until: expect.any(Date) });
120
+ describe('RgService.activateCoolingOff (real PG)', () => {
121
+ it('stores an active row and blocks login until its expiry', async () => {
122
+ const { svc, events, enforcement } = makeService();
123
+ const userId = randomUUID();
124
+ await svc.activateCoolingOff(userId, { userId, durationHours: 24, reason: 'break' }, randomUUID());
125
+ const [row] = await exclusionsOf(userId);
126
+ expect(row).toMatchObject({ kind: 'cooling_off', status: 'active', isPermanent: false });
127
+ expect(enforcement.block).toHaveBeenCalledWith(userId, { until: expect.any(Date) });
135
128
  expect(enforcement.unblock).not.toHaveBeenCalled();
136
- expect(events.emit).toHaveBeenCalledWith('rg.cooling_off.activated', expect.objectContaining({ userId: USER }));
129
+ expect(events.emit).toHaveBeenCalledWith('rg.cooling_off.activated', expect.objectContaining({ userId }));
137
130
  });
138
- it('rejects when an active cooling-off already exists', async () => {
139
- const { svc, enforcement } = newSvc(routingDb({ rgExclusion: [[{ id: 'x' }]] }));
140
- await expect(svc.activateCoolingOff(USER, { userId: USER, durationHours: 24, reason: 'break' }, ADMIN)).rejects.toBeInstanceOf(ActiveExclusionError);
131
+ it('rejects a second cooling-off while one is still running', async () => {
132
+ const { svc, enforcement } = makeService();
133
+ const userId = randomUUID();
134
+ await seedExclusion({ userId, kind: 'cooling_off', expiresAt: future() });
135
+ await expect(svc.activateCoolingOff(userId, { userId, durationHours: 24, reason: 'break' }, randomUUID())).rejects.toBeInstanceOf(ActiveExclusionError);
141
136
  expect(enforcement.block).not.toHaveBeenCalled();
142
137
  });
143
- // GROUP 1: a cooling-off on a self-excluded player must NOT downgrade the hard block.
138
+ it('expires a lapsed cooling-off and lets a fresh one through', async () => {
139
+ const { svc } = makeService();
140
+ const userId = randomUUID();
141
+ const lapsed = await seedExclusion({ userId, kind: 'cooling_off', expiresAt: past() });
142
+ await svc.activateCoolingOff(userId, { userId, durationHours: 24, reason: 'again' }, randomUUID());
143
+ const rows = await exclusionsOf(userId);
144
+ expect(rows).toHaveLength(2);
145
+ expect(rows.find((r) => r.id === lapsed.id)?.status).toBe('expired');
146
+ expect(rows.filter((r) => r.status === 'active')).toHaveLength(1);
147
+ });
144
148
  it('keeps an indefinite block when a self-exclusion is still active', async () => {
145
- const row = exclusionRow({ kind: 'cooling_off', expiresAt: future() });
146
- const db = routingDb({
147
- rgExclusion: [
148
- [],
149
- [
150
- { kind: 'self_exclusion', expiresAt: null },
151
- { kind: 'cooling_off', expiresAt: future() },
152
- ],
153
- ],
154
- returning: [row],
155
- });
156
- const { svc, enforcement } = newSvc(db);
157
- await svc.activateCoolingOff(USER, { userId: USER, durationHours: 24, reason: 'break' }, ADMIN);
158
- expect(enforcement.block).toHaveBeenCalledWith(USER, { until: null });
149
+ const { svc, enforcement } = makeService();
150
+ const userId = randomUUID();
151
+ await seedExclusion({ userId, kind: 'self_exclusion', isPermanent: true, expiresAt: null });
152
+ await svc.activateCoolingOff(userId, { userId, durationHours: 24, reason: 'break' }, randomUUID());
153
+ expect(enforcement.block).toHaveBeenCalledWith(userId, { until: null });
159
154
  });
160
155
  });
161
- describe('RgService.activateSelfExclusion', () => {
162
- it('blocks indefinitely (until null) for a permanent exclusion', async () => {
163
- const row = exclusionRow({ isPermanent: true, expiresAt: null });
164
- const db = routingDb({
165
- rgExclusion: [[], [{ kind: 'self_exclusion', expiresAt: null }]],
166
- returning: [row],
167
- });
168
- const { svc, events, enforcement } = newSvc(db);
169
- await svc.activateSelfExclusion(USER, { userId: USER, isPermanent: true, reason: 'stop', confirm: true }, ADMIN);
170
- expect(enforcement.block).toHaveBeenCalledWith(USER, { until: null });
156
+ describe('RgService.activateSelfExclusion (real PG)', () => {
157
+ it('stores a permanent exclusion with no expiry and blocks indefinitely', async () => {
158
+ const { svc, events, enforcement } = makeService();
159
+ const userId = randomUUID();
160
+ await svc.activateSelfExclusion(userId, { userId, isPermanent: true, reason: 'stop', confirm: true }, randomUUID());
161
+ const [row] = await exclusionsOf(userId);
162
+ expect(row).toMatchObject({ kind: 'self_exclusion', isPermanent: true, expiresAt: null });
163
+ expect(enforcement.block).toHaveBeenCalledWith(userId, { until: null });
171
164
  expect(events.emit).toHaveBeenCalledWith('rg.self_exclusion.activated', expect.objectContaining({ isPermanent: true, durationMonths: null, expiresAt: null }));
172
165
  });
173
- it('carries the chosen term and a matching expiry for a fixed-term exclusion', async () => {
174
- const expiresAt = future();
175
- const row = exclusionRow({
176
- id: EXCLUSION_UUID,
177
- userId: USER_UUID,
178
- isPermanent: false,
179
- expiresAt,
180
- });
181
- const db = routingDb({
182
- rgExclusion: [[], [{ kind: 'self_exclusion', expiresAt }]],
183
- returning: [row],
184
- });
185
- const { svc, events, enforcement } = newSvc(db);
186
- await svc.activateSelfExclusion(USER_UUID, { userId: USER_UUID, isPermanent: false, durationMonths: 6, reason: 'stop', confirm: true }, ADMIN_UUID);
187
- expect(enforcement.block).toHaveBeenCalledWith(USER_UUID, { until: null });
188
- const [topic, payload] = events.emit.mock.calls[0] ?? [];
189
- expect(topic).toBe('rg.self_exclusion.activated');
190
- expect(domainEventSchemas['rg.self_exclusion.activated'].parse(payload)).toMatchObject({
166
+ it('stores a fixed-term expiry, keeps the indefinite block, and emits the chosen term', async () => {
167
+ const { svc, events, enforcement } = makeService();
168
+ const userId = randomUUID();
169
+ const actorId = randomUUID();
170
+ await svc.activateSelfExclusion(userId, { userId, isPermanent: false, durationMonths: 6, reason: 'break', confirm: true }, actorId);
171
+ const [row] = await exclusionsOf(userId);
172
+ expect(row?.expiresAt).toBeInstanceOf(Date);
173
+ expect(enforcement.block).toHaveBeenCalledWith(userId, { until: null });
174
+ expect(events.emit).toHaveBeenCalledWith('rg.self_exclusion.activated', expect.objectContaining({
175
+ userId,
176
+ actorId,
191
177
  isPermanent: false,
192
178
  durationMonths: 6,
193
- reason: 'stop',
194
- actorId: ADMIN_UUID,
195
- userId: USER_UUID,
196
- });
179
+ reason: 'break',
180
+ }));
181
+ });
182
+ it('rejects a second active self-exclusion', async () => {
183
+ const { svc } = makeService();
184
+ const userId = randomUUID();
185
+ await seedExclusion({ userId, kind: 'self_exclusion', isPermanent: true });
186
+ await expect(svc.activateSelfExclusion(userId, { userId, isPermanent: true, reason: 'again', confirm: true }, randomUUID())).rejects.toBeInstanceOf(ActiveExclusionError);
197
187
  });
198
188
  });
199
- describe('RgService.liftSelfExclusion', () => {
200
- it('rejects lifting a permanent self-exclusion', async () => {
201
- const db = routingDb({ rgExclusion: [[exclusionRow({ isPermanent: true, expiresAt: null })]] });
202
- const { svc, enforcement } = newSvc(db);
203
- await expect(svc.liftSelfExclusion(USER, { userId: USER, reason: 'ok', confirm: true }, ADMIN)).rejects.toBeInstanceOf(PermanentExclusionLiftError);
189
+ describe('RgService.liftSelfExclusion (real PG)', () => {
190
+ it('refuses to lift a permanent self-exclusion', async () => {
191
+ const { svc, enforcement } = makeService();
192
+ const userId = randomUUID();
193
+ await seedExclusion({ userId, isPermanent: true, expiresAt: null });
194
+ await expect(svc.liftSelfExclusion(userId, { userId, reason: 'ok', confirm: true }, randomUUID())).rejects.toBeInstanceOf(PermanentExclusionLiftError);
204
195
  expect(enforcement.unblock).not.toHaveBeenCalled();
205
196
  });
206
- it('rejects lifting before the minimum period elapses', async () => {
207
- const db = routingDb({
208
- rgExclusion: [[exclusionRow({ isPermanent: false, expiresAt: future() })]],
209
- });
210
- const { svc } = newSvc(db);
211
- await expect(svc.liftSelfExclusion(USER, { userId: USER, reason: 'ok', confirm: true }, ADMIN)).rejects.toBeInstanceOf(ExclusionPeriodNotElapsedError);
197
+ it('refuses to lift before the minimum period has elapsed', async () => {
198
+ const { svc } = makeService();
199
+ const userId = randomUUID();
200
+ await seedExclusion({ userId, isPermanent: false, expiresAt: future() });
201
+ await expect(svc.liftSelfExclusion(userId, { userId, reason: 'ok', confirm: true }, randomUUID())).rejects.toBeInstanceOf(ExclusionPeriodNotElapsedError);
212
202
  });
213
- it('rejects when there is no active self-exclusion', async () => {
214
- const { svc } = newSvc(routingDb({ rgExclusion: [[]] }));
215
- await expect(svc.liftSelfExclusion(USER, { userId: USER, reason: 'ok', confirm: true }, ADMIN)).rejects.toBeInstanceOf(ExclusionNotFoundError);
203
+ it('throws when the player has no active self-exclusion', async () => {
204
+ const { svc } = makeService();
205
+ await expect(svc.liftSelfExclusion(randomUUID(), { userId: randomUUID(), reason: 'ok', confirm: true }, randomUUID())).rejects.toBeInstanceOf(ExclusionNotFoundError);
216
206
  });
217
- it('lifts after the minimum period and unblocks when nothing else is active', async () => {
218
- const past = new Date(Date.now() - 1000);
219
- const existing = exclusionRow({ isPermanent: false, expiresAt: past });
220
- const lifted = { ...existing, status: 'lifted' };
221
- const db = routingDb({ rgExclusion: [[existing], []], returning: [lifted] });
222
- const { svc, events, enforcement } = newSvc(db);
223
- await svc.liftSelfExclusion(USER, { userId: USER, reason: 'ok', confirm: true }, ADMIN);
224
- expect(enforcement.unblock).toHaveBeenCalledWith(USER);
225
- expect(events.emit).toHaveBeenCalledWith('rg.self_exclusion.lifted', expect.objectContaining({ userId: USER }));
207
+ it('lifts an elapsed exclusion, records the lift metadata, and unblocks', async () => {
208
+ const { svc, events, enforcement } = makeService();
209
+ const userId = randomUUID();
210
+ const actorId = randomUUID();
211
+ const existing = await seedExclusion({ userId, isPermanent: false, expiresAt: past() });
212
+ await svc.liftSelfExclusion(userId, { userId, reason: 'recovered', confirm: true }, actorId);
213
+ const [row] = await exclusionsOf(userId);
214
+ expect(row).toMatchObject({
215
+ id: existing.id,
216
+ status: 'lifted',
217
+ liftedReason: 'recovered',
218
+ liftedBy: actorId,
219
+ });
220
+ expect(row?.liftedAt).toBeInstanceOf(Date);
221
+ expect(enforcement.unblock).toHaveBeenCalledWith(userId);
222
+ expect(events.emit).toHaveBeenCalledWith('rg.self_exclusion.lifted', expect.objectContaining({ userId, actorId }));
226
223
  });
227
- // GROUP 1: lifting the self-exclusion must not clear an unrelated active cooling-off.
228
224
  it('recomputes to the remaining cooling-off block instead of unblocking', async () => {
229
- const past = new Date(Date.now() - 1000);
230
- const existing = exclusionRow({ isPermanent: false, expiresAt: past });
231
- const db = routingDb({
232
- rgExclusion: [[existing], [{ kind: 'cooling_off', expiresAt: future() }]],
233
- returning: [{ ...existing, status: 'lifted' }],
234
- });
235
- const { svc, enforcement } = newSvc(db);
236
- await svc.liftSelfExclusion(USER, { userId: USER, reason: 'ok', confirm: true }, ADMIN);
237
- expect(enforcement.block).toHaveBeenCalledWith(USER, { until: expect.any(Date) });
225
+ const { svc, enforcement } = makeService();
226
+ const userId = randomUUID();
227
+ await seedExclusion({ userId, isPermanent: false, expiresAt: past() });
228
+ await seedExclusion({ userId, kind: 'cooling_off', expiresAt: future() });
229
+ await svc.liftSelfExclusion(userId, { userId, reason: 'ok', confirm: true }, randomUUID());
230
+ expect(enforcement.block).toHaveBeenCalledWith(userId, { until: expect.any(Date) });
238
231
  expect(enforcement.unblock).not.toHaveBeenCalled();
239
232
  });
233
+ it('lets a fresh self-exclusion start once the previous one was lifted', async () => {
234
+ const { svc } = makeService();
235
+ const userId = randomUUID();
236
+ await seedExclusion({ userId, isPermanent: false, expiresAt: past() });
237
+ await svc.liftSelfExclusion(userId, { userId, reason: 'ok', confirm: true }, randomUUID());
238
+ await svc.activateSelfExclusion(userId, { userId, isPermanent: true, reason: 'relapse', confirm: true }, randomUUID());
239
+ const rows = await exclusionsOf(userId);
240
+ expect(rows.filter((r) => r.status === 'active')).toHaveLength(1);
241
+ });
240
242
  });
241
- describe('RgService.liftCoolingOff', () => {
242
- const coolingOffRow = (overrides = {}) => exclusionRow({ kind: 'cooling_off', expiresAt: future(), ...overrides });
243
- it('rejects when the player has no active cooling-off', async () => {
244
- const { svc } = newSvc(routingDb({ rgExclusion: [[]] }));
245
- await expect(svc.liftCoolingOff(USER, { userId: USER, reason: 'ok' }, ADMIN)).rejects.toThrow(ExclusionNotFoundError);
243
+ describe('RgService.liftCoolingOff (real PG)', () => {
244
+ it('throws when the player has no active cooling-off', async () => {
245
+ const { svc } = makeService();
246
+ const userId = randomUUID();
247
+ await expect(svc.liftCoolingOff(userId, { userId, reason: 'ok' }, randomUUID())).rejects.toBeInstanceOf(ExclusionNotFoundError);
246
248
  });
247
- it('lifts before expiry and unblocks when nothing else is active', async () => {
248
- const existing = coolingOffRow();
249
- const db = routingDb({
250
- rgExclusion: [[existing], []],
251
- returning: [{ ...existing, status: 'lifted' }],
249
+ it('lifts an active cooling-off, records the lift metadata, and unblocks', async () => {
250
+ const { svc, events, enforcement } = makeService();
251
+ const userId = randomUUID();
252
+ const actorId = randomUUID();
253
+ const existing = await seedExclusion({ userId, kind: 'cooling_off', expiresAt: future() });
254
+ await svc.liftCoolingOff(userId, { userId, reason: 'support ticket 42' }, actorId);
255
+ const [row] = await exclusionsOf(userId);
256
+ expect(row).toMatchObject({
257
+ id: existing.id,
258
+ kind: 'cooling_off',
259
+ status: 'lifted',
260
+ liftedReason: 'support ticket 42',
261
+ liftedBy: actorId,
252
262
  });
253
- const { svc, events, enforcement } = newSvc(db);
254
- const lifted = await svc.liftCoolingOff(USER, { userId: USER, reason: 'raised in error' }, ADMIN);
255
- expect(lifted.status).toBe('lifted');
256
- expect(enforcement.unblock).toHaveBeenCalledWith(USER);
257
- expect(events.emit).toHaveBeenCalledWith('rg.cooling_off.lifted', expect.objectContaining({ userId: USER, actorId: ADMIN, reason: 'raised in error' }));
263
+ expect(enforcement.unblock).toHaveBeenCalledWith(userId);
264
+ expect(events.emit).toHaveBeenCalledWith('rg.cooling_off.lifted', expect.objectContaining({ userId, actorId, reason: 'support ticket 42' }));
258
265
  });
259
266
  it('keeps an indefinite block when the player is also self-excluded', async () => {
260
- const existing = coolingOffRow();
261
- const db = routingDb({
262
- rgExclusion: [[existing], [{ kind: 'self_exclusion', expiresAt: null }]],
263
- returning: [{ ...existing, status: 'lifted' }],
264
- });
265
- const { svc, enforcement } = newSvc(db);
266
- await svc.liftCoolingOff(USER, { userId: USER, reason: 'ok' }, ADMIN);
267
- expect(enforcement.block).toHaveBeenCalledWith(USER, { until: null });
267
+ const { svc, enforcement } = makeService();
268
+ const userId = randomUUID();
269
+ await seedExclusion({ userId, kind: 'cooling_off', expiresAt: future() });
270
+ await seedExclusion({ userId, kind: 'self_exclusion', isPermanent: true, expiresAt: null });
271
+ await svc.liftCoolingOff(userId, { userId, reason: 'ok' }, randomUUID());
272
+ expect(enforcement.block).toHaveBeenCalledWith(userId, { until: null });
268
273
  expect(enforcement.unblock).not.toHaveBeenCalled();
269
274
  });
270
- it('records the actor and reason on the lifted row', async () => {
271
- const existing = coolingOffRow();
272
- const db = routingDb({
273
- rgExclusion: [[existing], []],
274
- returning: [
275
- { ...existing, status: 'lifted', liftedBy: ADMIN, liftedReason: 'support ticket 42' },
276
- ],
277
- });
278
- const { svc } = newSvc(db);
279
- const lifted = await svc.liftCoolingOff(USER, { userId: USER, reason: 'support ticket 42' }, ADMIN);
280
- expect(lifted.liftedBy).toBe(ADMIN);
281
- expect(lifted.liftedReason).toBe('support ticket 42');
275
+ });
276
+ describe('RgService.getRgSection (real PG)', () => {
277
+ it('returns the player limits alongside the active exclusions', async () => {
278
+ const { svc } = makeService();
279
+ const userId = randomUUID();
280
+ await svc.setPlayerLimit(userId, { userId, type: 'deposit', amount: '100', minutes: null, period: 'daily' }, randomUUID());
281
+ await seedExclusion({ userId, kind: 'cooling_off', expiresAt: future() });
282
+ await seedExclusion({ userId, kind: 'self_exclusion', isPermanent: true });
283
+ const section = await svc.getRgSection(userId);
284
+ expect(section.limits).toHaveLength(1);
285
+ expect(section.coolingOff).toMatchObject({ kind: 'cooling_off' });
286
+ expect(section.selfExclusion).toMatchObject({ kind: 'self_exclusion' });
287
+ });
288
+ it('treats a lapsed cooling-off as inactive even before the sweep runs', async () => {
289
+ const { svc } = makeService();
290
+ const userId = randomUUID();
291
+ await seedExclusion({ userId, kind: 'cooling_off', expiresAt: past() });
292
+ const section = await svc.getRgSection(userId);
293
+ expect(section.coolingOff).toBeNull();
294
+ });
295
+ it('returns empty state for a player with nothing on file', async () => {
296
+ const { svc } = makeService();
297
+ const section = await svc.getRgSection(randomUUID());
298
+ expect(section).toEqual({ limits: [], coolingOff: null, selfExclusion: null });
299
+ });
300
+ });
301
+ describe('RgService.expireLapsedCoolingOffs (real PG)', () => {
302
+ it('expires lapsed rows and unblocks the affected players', async () => {
303
+ const { svc, enforcement } = makeService();
304
+ const lapsedUser = randomUUID();
305
+ const runningUser = randomUUID();
306
+ await seedExclusion({ userId: lapsedUser, kind: 'cooling_off', expiresAt: past() });
307
+ await seedExclusion({ userId: runningUser, kind: 'cooling_off', expiresAt: future() });
308
+ await svc.expireLapsedCoolingOffs();
309
+ const [lapsed] = await exclusionsOf(lapsedUser);
310
+ const [running] = await exclusionsOf(runningUser);
311
+ expect(lapsed?.status).toBe('expired');
312
+ expect(running?.status).toBe('active');
313
+ expect(enforcement.unblock).toHaveBeenCalledWith(lapsedUser);
314
+ expect(enforcement.unblock).not.toHaveBeenCalledWith(runningUser);
315
+ });
316
+ it('keeps the indefinite block when the player is also self-excluded', async () => {
317
+ const { svc, enforcement } = makeService();
318
+ const userId = randomUUID();
319
+ await seedExclusion({ userId, kind: 'cooling_off', expiresAt: past() });
320
+ await seedExclusion({ userId, kind: 'self_exclusion', isPermanent: true });
321
+ await svc.expireLapsedCoolingOffs();
322
+ expect(enforcement.block).toHaveBeenCalledWith(userId, { until: null });
323
+ expect(enforcement.unblock).not.toHaveBeenCalled();
282
324
  });
283
325
  });
284
326
  //# sourceMappingURL=rg.service.test.js.map