@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/admin-console/__tests__/backoffice.router.test.js +67 -51
- package/dist/admin-console/__tests__/backoffice.router.test.js.map +1 -1
- package/dist/admin-console/__tests__/backoffice.service.test.js +1 -13
- package/dist/admin-console/__tests__/backoffice.service.test.js.map +1 -1
- package/dist/analytics/__tests__/analytics.contract.test.js +12 -0
- package/dist/analytics/__tests__/analytics.contract.test.js.map +1 -1
- package/dist/analytics/__tests__/financial-analytics.service.test.js +3 -3
- package/dist/analytics/__tests__/financial-analytics.service.test.js.map +1 -1
- package/dist/analytics/service/financial-analytics.service.d.ts.map +1 -1
- package/dist/analytics/service/financial-analytics.service.js +14 -17
- package/dist/analytics/service/financial-analytics.service.js.map +1 -1
- package/dist/analytics/service/funnel-analytics.service.d.ts.map +1 -1
- package/dist/analytics/service/funnel-analytics.service.js +8 -11
- package/dist/analytics/service/funnel-analytics.service.js.map +1 -1
- package/dist/audit/__tests__/audit.service.test.js +128 -251
- package/dist/audit/__tests__/audit.service.test.js.map +1 -1
- package/dist/casino/gaming/__tests__/gaming.service.test.js +83 -89
- package/dist/casino/gaming/__tests__/gaming.service.test.js.map +1 -1
- package/dist/casino/lobby/__tests__/lobby.cache.test.js +42 -33
- package/dist/casino/lobby/__tests__/lobby.cache.test.js.map +1 -1
- package/dist/cms/__tests__/cms.reads.test.js +59 -80
- package/dist/cms/__tests__/cms.reads.test.js.map +1 -1
- package/dist/cms/__tests__/cms.router.test.js +62 -20
- package/dist/cms/__tests__/cms.router.test.js.map +1 -1
- package/dist/cms/__tests__/cms.service.test.js +116 -14
- package/dist/cms/__tests__/cms.service.test.js.map +1 -1
- package/dist/common/errors/__tests__/errors.mapper.test.d.ts +2 -0
- package/dist/common/errors/__tests__/errors.mapper.test.d.ts.map +1 -0
- package/dist/common/errors/__tests__/errors.mapper.test.js +59 -0
- package/dist/common/errors/__tests__/errors.mapper.test.js.map +1 -0
- package/dist/compliance/__tests__/assert.test.d.ts +2 -0
- package/dist/compliance/__tests__/assert.test.d.ts.map +1 -0
- package/dist/compliance/__tests__/assert.test.js +34 -0
- package/dist/compliance/__tests__/assert.test.js.map +1 -0
- package/dist/compliance/__tests__/compliance.service.test.js +149 -84
- package/dist/compliance/__tests__/compliance.service.test.js.map +1 -1
- package/dist/compliance/__tests__/kyc.service.test.js +247 -155
- package/dist/compliance/__tests__/kyc.service.test.js.map +1 -1
- package/dist/compliance/__tests__/limits.contract.test.d.ts +2 -0
- package/dist/compliance/__tests__/limits.contract.test.d.ts.map +1 -0
- package/dist/compliance/__tests__/limits.contract.test.js +43 -0
- package/dist/compliance/__tests__/limits.contract.test.js.map +1 -0
- package/dist/compliance/__tests__/rg-monitoring.service.test.js +256 -101
- package/dist/compliance/__tests__/rg-monitoring.service.test.js.map +1 -1
- package/dist/compliance/__tests__/rg.contract.test.js +1 -20
- package/dist/compliance/__tests__/rg.contract.test.js.map +1 -1
- package/dist/compliance/__tests__/rg.router.test.js +113 -65
- package/dist/compliance/__tests__/rg.router.test.js.map +1 -1
- package/dist/compliance/__tests__/rg.service.test.js +277 -235
- package/dist/compliance/__tests__/rg.service.test.js.map +1 -1
- package/dist/compliance/service/rg-monitoring.service.d.ts.map +1 -1
- package/dist/compliance/service/rg-monitoring.service.js +11 -8
- package/dist/compliance/service/rg-monitoring.service.js.map +1 -1
- package/dist/contracts/__tests__/compose-contract.test.d.ts +2 -0
- package/dist/contracts/__tests__/compose-contract.test.d.ts.map +1 -0
- package/dist/contracts/__tests__/compose-contract.test.js +27 -0
- package/dist/contracts/__tests__/compose-contract.test.js.map +1 -0
- package/dist/contracts/adapters/__tests__/token.test.d.ts +2 -0
- package/dist/contracts/adapters/__tests__/token.test.d.ts.map +1 -0
- package/dist/contracts/adapters/__tests__/token.test.js +14 -0
- package/dist/contracts/adapters/__tests__/token.test.js.map +1 -0
- package/dist/contracts/schemas/__tests__/events.test.d.ts +2 -0
- package/dist/contracts/schemas/__tests__/events.test.d.ts.map +1 -0
- package/dist/contracts/schemas/__tests__/events.test.js +51 -0
- package/dist/contracts/schemas/__tests__/events.test.js.map +1 -0
- package/dist/contracts/schemas/__tests__/igaming-config.test.d.ts +2 -0
- package/dist/contracts/schemas/__tests__/igaming-config.test.d.ts.map +1 -0
- package/dist/contracts/schemas/__tests__/igaming-config.test.js +38 -0
- package/dist/contracts/schemas/__tests__/igaming-config.test.js.map +1 -0
- package/dist/contracts/schemas/reporting.d.ts +7 -2
- package/dist/contracts/schemas/reporting.d.ts.map +1 -1
- package/dist/contracts/schemas/reporting.js +2 -1
- package/dist/contracts/schemas/reporting.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat.service.test.js +524 -364
- package/dist/engagement/chat/__tests__/chat.service.test.js.map +1 -1
- package/dist/engagement/notifications/__tests__/notifications.service.test.js +125 -68
- package/dist/engagement/notifications/__tests__/notifications.service.test.js.map +1 -1
- package/dist/iam/__tests__/iam.service.test.js +367 -596
- package/dist/iam/__tests__/iam.service.test.js.map +1 -1
- package/dist/pam/identity/__tests__/admin-user-directory.test.js +144 -68
- package/dist/pam/identity/__tests__/admin-user-directory.test.js.map +1 -1
- package/dist/pam/identity/__tests__/identity.rate-limit.test.js +33 -22
- package/dist/pam/identity/__tests__/identity.rate-limit.test.js.map +1 -1
- package/dist/pam/identity/__tests__/identity.service.test.js +217 -298
- package/dist/pam/identity/__tests__/identity.service.test.js.map +1 -1
- package/dist/pam/identity/__tests__/login-enforcement.service.test.js +97 -32
- package/dist/pam/identity/__tests__/login-enforcement.service.test.js.map +1 -1
- package/dist/pam/identity/__tests__/phone-login.service.test.js +163 -134
- package/dist/pam/identity/__tests__/phone-login.service.test.js.map +1 -1
- package/dist/pam/identity/__tests__/rg-guard.test.d.ts +2 -0
- package/dist/pam/identity/__tests__/rg-guard.test.d.ts.map +1 -0
- package/dist/pam/identity/__tests__/rg-guard.test.js +21 -0
- package/dist/pam/identity/__tests__/rg-guard.test.js.map +1 -0
- package/dist/pam/player-management/__tests__/kyc-status-writer.test.js +73 -26
- package/dist/pam/player-management/__tests__/kyc-status-writer.test.js.map +1 -1
- package/dist/pam/player-management/__tests__/player.router.test.js +63 -46
- package/dist/pam/player-management/__tests__/player.router.test.js.map +1 -1
- package/dist/pam/player-management/__tests__/player.service.test.js +207 -111
- package/dist/pam/player-management/__tests__/player.service.test.js.map +1 -1
- package/dist/pam/profile/__tests__/profile.service.test.js +89 -41
- package/dist/pam/profile/__tests__/profile.service.test.js.map +1 -1
- package/dist/pam/shared/__tests__/shared-mappers.test.d.ts +2 -0
- package/dist/pam/shared/__tests__/shared-mappers.test.d.ts.map +1 -0
- package/dist/pam/shared/__tests__/shared-mappers.test.js +55 -0
- package/dist/pam/shared/__tests__/shared-mappers.test.js.map +1 -0
- package/dist/pam/tag/__tests__/get-active-tag-keys.test.js +72 -30
- package/dist/pam/tag/__tests__/get-active-tag-keys.test.js.map +1 -1
- package/dist/pam/tag/__tests__/tag-evaluation.service.test.js +308 -235
- package/dist/pam/tag/__tests__/tag-evaluation.service.test.js.map +1 -1
- package/dist/pam/tag/__tests__/tag-rule.service.test.js +136 -140
- package/dist/pam/tag/__tests__/tag-rule.service.test.js.map +1 -1
- package/dist/pam/tag/__tests__/tag.router.test.js +76 -25
- package/dist/pam/tag/__tests__/tag.router.test.js.map +1 -1
- package/dist/pam/tag/__tests__/tag.service.test.js +224 -247
- package/dist/pam/tag/__tests__/tag.service.test.js.map +1 -1
- package/dist/server/auth/__tests__/admin-guard.test.js +107 -117
- package/dist/server/auth/__tests__/admin-guard.test.js.map +1 -1
- package/dist/server/auth/__tests__/permission-levels.test.d.ts +2 -0
- package/dist/server/auth/__tests__/permission-levels.test.d.ts.map +1 -0
- package/dist/server/auth/__tests__/permission-levels.test.js +89 -0
- package/dist/server/auth/__tests__/permission-levels.test.js.map +1 -0
- package/dist/server/auth/__tests__/session-resolver.test.js +45 -13
- package/dist/server/auth/__tests__/session-resolver.test.js.map +1 -1
- package/dist/server/db/__tests__/query-helpers.test.js +72 -1
- package/dist/server/db/__tests__/query-helpers.test.js.map +1 -1
- package/dist/server/db/outbox/__tests__/relay.test.js +74 -81
- package/dist/server/db/outbox/__tests__/relay.test.js.map +1 -1
- package/dist/server/kernel/__tests__/bullmq-job-queue.test.js +204 -154
- package/dist/server/kernel/__tests__/bullmq-job-queue.test.js.map +1 -1
- package/dist/server/kernel/__tests__/cache.test.js +26 -13
- package/dist/server/kernel/__tests__/cache.test.js.map +1 -1
- package/dist/server/kernel/__tests__/domain-error.test.d.ts +2 -0
- package/dist/server/kernel/__tests__/domain-error.test.d.ts.map +1 -0
- package/dist/server/kernel/__tests__/domain-error.test.js +35 -0
- package/dist/server/kernel/__tests__/domain-error.test.js.map +1 -0
- package/dist/server/kernel/__tests__/error-reporter.test.d.ts +2 -0
- package/dist/server/kernel/__tests__/error-reporter.test.d.ts.map +1 -0
- package/dist/server/kernel/__tests__/error-reporter.test.js +52 -0
- package/dist/server/kernel/__tests__/error-reporter.test.js.map +1 -0
- package/dist/server/kernel/__tests__/event-bus.test.js +60 -20
- package/dist/server/kernel/__tests__/event-bus.test.js.map +1 -1
- package/dist/server/kernel/__tests__/orpc-error-map.test.d.ts +2 -0
- package/dist/server/kernel/__tests__/orpc-error-map.test.d.ts.map +1 -0
- package/dist/server/kernel/__tests__/orpc-error-map.test.js +54 -0
- package/dist/server/kernel/__tests__/orpc-error-map.test.js.map +1 -0
- package/dist/server/kernel/__tests__/platform-config-loader.test.d.ts +2 -0
- package/dist/server/kernel/__tests__/platform-config-loader.test.d.ts.map +1 -0
- package/dist/server/kernel/__tests__/platform-config-loader.test.js +112 -0
- package/dist/server/kernel/__tests__/platform-config-loader.test.js.map +1 -0
- package/dist/server/kernel/__tests__/rate-limiter.test.js +22 -13
- package/dist/server/kernel/__tests__/rate-limiter.test.js.map +1 -1
- package/dist/server/kernel/__tests__/realtime-transport.test.d.ts.map +1 -0
- package/dist/{testing/fakes → server/kernel}/__tests__/realtime-transport.test.js.map +1 -1
- package/dist/server/kernel/__tests__/redis-cache.test.js +34 -34
- package/dist/server/kernel/__tests__/redis-cache.test.js.map +1 -1
- package/dist/server/kernel/__tests__/redis-rate-limiter.test.js +49 -44
- package/dist/server/kernel/__tests__/redis-rate-limiter.test.js.map +1 -1
- package/dist/server/kernel/__tests__/redis-streams-broker.test.js +141 -262
- package/dist/server/kernel/__tests__/redis-streams-broker.test.js.map +1 -1
- package/dist/server/kernel/__tests__/request-context.test.d.ts +2 -0
- package/dist/server/kernel/__tests__/request-context.test.d.ts.map +1 -0
- package/dist/server/kernel/__tests__/request-context.test.js +64 -0
- package/dist/server/kernel/__tests__/request-context.test.js.map +1 -0
- package/dist/server/kernel/__tests__/router-utils.test.d.ts +2 -0
- package/dist/server/kernel/__tests__/router-utils.test.d.ts.map +1 -0
- package/dist/server/kernel/__tests__/router-utils.test.js +80 -0
- package/dist/server/kernel/__tests__/router-utils.test.js.map +1 -0
- package/dist/server/kernel/bullmq-job-queue.d.ts.map +1 -1
- package/dist/server/kernel/bullmq-job-queue.js +8 -2
- package/dist/server/kernel/bullmq-job-queue.js.map +1 -1
- package/dist/server/runtime/__tests__/create-app.test.js +26 -18
- package/dist/server/runtime/__tests__/create-app.test.js.map +1 -1
- package/dist/testing/index.d.ts +2 -6
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -10
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +28 -16
- package/dist/testing/mock.d.ts.map +1 -1
- package/dist/testing/mock.js +58 -37
- package/dist/testing/mock.js.map +1 -1
- package/dist/testing/real-infra.d.ts +47 -0
- package/dist/testing/real-infra.d.ts.map +1 -0
- package/dist/testing/real-infra.js +122 -0
- package/dist/testing/real-infra.js.map +1 -0
- package/dist/wallet/__tests__/admin-reporting.test.js +201 -90
- package/dist/wallet/__tests__/admin-reporting.test.js.map +1 -1
- package/dist/wallet/__tests__/rail-for.test.d.ts +2 -0
- package/dist/wallet/__tests__/rail-for.test.d.ts.map +1 -0
- package/dist/wallet/__tests__/rail-for.test.js +22 -0
- package/dist/wallet/__tests__/rail-for.test.js.map +1 -0
- package/dist/wallet/__tests__/wallet-auto-rule.router.test.js +82 -63
- package/dist/wallet/__tests__/wallet-auto-rule.router.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-auto-withdrawal.test.js +414 -437
- package/dist/wallet/__tests__/wallet-auto-withdrawal.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-commands.service.test.js +116 -155
- package/dist/wallet/__tests__/wallet-commands.service.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-kyc-gate.test.js +63 -42
- package/dist/wallet/__tests__/wallet-kyc-gate.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-webhook.router.test.js +129 -51
- package/dist/wallet/__tests__/wallet-webhook.router.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.rate-limit.test.js +74 -21
- package/dist/wallet/__tests__/wallet.rate-limit.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.router.test.js +56 -36
- package/dist/wallet/__tests__/wallet.router.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.service.test.js +623 -887
- package/dist/wallet/__tests__/wallet.service.test.js.map +1 -1
- package/package.json +1 -2
- package/dist/casino/lobby/__tests__/lobby.service.test.d.ts +0 -2
- package/dist/casino/lobby/__tests__/lobby.service.test.d.ts.map +0 -1
- package/dist/casino/lobby/__tests__/lobby.service.test.js +0 -11
- package/dist/casino/lobby/__tests__/lobby.service.test.js.map +0 -1
- package/dist/contracts/schemas/__tests__/identity-schemas.test.d.ts +0 -2
- package/dist/contracts/schemas/__tests__/identity-schemas.test.d.ts.map +0 -1
- package/dist/contracts/schemas/__tests__/identity-schemas.test.js +0 -43
- package/dist/contracts/schemas/__tests__/identity-schemas.test.js.map +0 -1
- package/dist/testing/fakes/__tests__/cache.test.d.ts +0 -2
- package/dist/testing/fakes/__tests__/cache.test.d.ts.map +0 -1
- package/dist/testing/fakes/__tests__/cache.test.js +0 -89
- package/dist/testing/fakes/__tests__/cache.test.js.map +0 -1
- package/dist/testing/fakes/__tests__/job-queue.test.d.ts +0 -2
- package/dist/testing/fakes/__tests__/job-queue.test.d.ts.map +0 -1
- package/dist/testing/fakes/__tests__/job-queue.test.js +0 -135
- package/dist/testing/fakes/__tests__/job-queue.test.js.map +0 -1
- package/dist/testing/fakes/__tests__/rate-limiter.test.d.ts +0 -2
- package/dist/testing/fakes/__tests__/rate-limiter.test.d.ts.map +0 -1
- package/dist/testing/fakes/__tests__/rate-limiter.test.js +0 -83
- package/dist/testing/fakes/__tests__/rate-limiter.test.js.map +0 -1
- package/dist/testing/fakes/__tests__/realtime-authorizer.test.d.ts +0 -2
- package/dist/testing/fakes/__tests__/realtime-authorizer.test.d.ts.map +0 -1
- package/dist/testing/fakes/__tests__/realtime-authorizer.test.js +0 -19
- package/dist/testing/fakes/__tests__/realtime-authorizer.test.js.map +0 -1
- package/dist/testing/fakes/__tests__/realtime-transport.test.d.ts.map +0 -1
- package/dist/testing/fakes/cache.d.ts +0 -14
- package/dist/testing/fakes/cache.d.ts.map +0 -1
- package/dist/testing/fakes/cache.js +0 -56
- package/dist/testing/fakes/cache.js.map +0 -1
- package/dist/testing/fakes/event-bus.d.ts +0 -17
- package/dist/testing/fakes/event-bus.d.ts.map +0 -1
- package/dist/testing/fakes/event-bus.js +0 -30
- package/dist/testing/fakes/event-bus.js.map +0 -1
- package/dist/testing/fakes/job-queue.d.ts +0 -40
- package/dist/testing/fakes/job-queue.d.ts.map +0 -1
- package/dist/testing/fakes/job-queue.js +0 -200
- package/dist/testing/fakes/job-queue.js.map +0 -1
- package/dist/testing/fakes/rate-limiter.d.ts +0 -11
- package/dist/testing/fakes/rate-limiter.d.ts.map +0 -1
- package/dist/testing/fakes/rate-limiter.js +0 -39
- package/dist/testing/fakes/rate-limiter.js.map +0 -1
- package/dist/testing/fakes/realtime-authorizer.d.ts +0 -10
- package/dist/testing/fakes/realtime-authorizer.d.ts.map +0 -1
- package/dist/testing/fakes/realtime-authorizer.js +0 -10
- package/dist/testing/fakes/realtime-authorizer.js.map +0 -1
- package/dist/testing/fakes/realtime-transport.d.ts +0 -2
- package/dist/testing/fakes/realtime-transport.d.ts.map +0 -1
- package/dist/testing/fakes/realtime-transport.js +0 -2
- package/dist/testing/fakes/realtime-transport.js.map +0 -1
- /package/dist/{testing/fakes → server/kernel}/__tests__/realtime-transport.test.d.ts +0 -0
- /package/dist/{testing/fakes → server/kernel}/__tests__/realtime-transport.test.js +0 -0
|
@@ -1,581 +1,558 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
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, NO_CLIENT_META, makeAuditWriter } from '../../testing/mock.js';
|
|
6
|
+
import { migrate } from '../migrate.js';
|
|
7
|
+
import { wallet, walletTransaction, autoWithdrawalRule } from '../schema/index.js';
|
|
3
8
|
import { WalletService } from '../service/wallet.service.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
rail: 'fiat',
|
|
13
|
-
...over,
|
|
14
|
-
});
|
|
15
|
-
// select/returning rows the initial hold transaction always consumes (no idempotency key).
|
|
16
|
-
const holdSelect = (wallet = WALLET_ROW) => [wallet];
|
|
17
|
-
const holdReturning = (tx = insertRow()) => [[tx], [{ id: 'w-1' }]];
|
|
18
|
-
// select/returning rows the flipToProcessing + settleApproved path consumes on a successful settle.
|
|
19
|
-
const finalizeSelect = () => [
|
|
20
|
-
[
|
|
21
|
-
{
|
|
22
|
-
id: 'tx-1',
|
|
23
|
-
walletId: 'w-1',
|
|
24
|
-
type: 'withdrawal',
|
|
25
|
-
status: 'pending',
|
|
26
|
-
amount: '40',
|
|
27
|
-
currency: 'USD',
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
[{ userId: 'u-1' }],
|
|
31
|
-
[],
|
|
32
|
-
];
|
|
33
|
-
const finalizeReturning = () => [
|
|
34
|
-
[
|
|
35
|
-
{
|
|
36
|
-
id: 'tx-1',
|
|
37
|
-
walletId: 'w-1',
|
|
38
|
-
type: 'withdrawal',
|
|
39
|
-
status: 'processing',
|
|
40
|
-
amount: '40',
|
|
41
|
-
currency: 'USD',
|
|
42
|
-
rail: 'fiat',
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
];
|
|
46
|
-
function makeSvc(opts) {
|
|
47
|
-
const events = makeEvents();
|
|
48
|
-
const payment = makePayment();
|
|
49
|
-
const audit = makeAudit();
|
|
50
|
-
const platformConfig = {
|
|
51
|
-
...(opts.kyc ? { kyc: opts.kyc } : {}),
|
|
52
|
-
...(opts.autoWithdrawal
|
|
53
|
-
? { autoWithdrawal: { enabled: true, excludeRiskFlags: [], ...opts.autoWithdrawal } }
|
|
54
|
-
: {}),
|
|
9
|
+
let db;
|
|
10
|
+
function makeService({ autoWithdrawal, kycStatus = 'verified', directoryThrows = false, riskTags = [], } = {}) {
|
|
11
|
+
const events = makeEventBus();
|
|
12
|
+
const psp = {
|
|
13
|
+
processWithdrawal: vi.fn(async () => ({
|
|
14
|
+
externalId: randomUUID(),
|
|
15
|
+
status: 'completed',
|
|
16
|
+
})),
|
|
55
17
|
};
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
18
|
+
const audit = makeAuditWriter();
|
|
19
|
+
const directory = mock({
|
|
20
|
+
lookupPlayers: vi.fn(async (ids) => {
|
|
21
|
+
if (directoryThrows) {
|
|
22
|
+
throw new Error('directory down');
|
|
23
|
+
}
|
|
24
|
+
return kycStatus
|
|
25
|
+
? ids.map((userId) => mock({ userId, username: 'player', kycStatus }))
|
|
26
|
+
: [];
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
const platformConfig = mock(autoWithdrawal
|
|
30
|
+
? { autoWithdrawal: { enabled: true, excludeRiskFlags: [], ...autoWithdrawal } }
|
|
31
|
+
: {});
|
|
32
|
+
const svc = new WalletService({
|
|
33
|
+
drizzle: db.drizzle,
|
|
34
|
+
events: events,
|
|
35
|
+
payment: mock(psp),
|
|
36
|
+
audit: mock(audit),
|
|
37
|
+
directory,
|
|
63
38
|
platformConfig,
|
|
64
|
-
|
|
65
|
-
|
|
39
|
+
...(riskTags === 'unbound'
|
|
40
|
+
? {}
|
|
41
|
+
: {
|
|
42
|
+
riskTags: mock({
|
|
43
|
+
getActiveTagKeys: vi.fn(async (ids) => new Map(ids.map((id) => [id, riskTags]))),
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
return { svc, events, psp, audit };
|
|
48
|
+
}
|
|
49
|
+
async function seedWallet(overrides = {}) {
|
|
50
|
+
const [row] = await db.drizzle.db
|
|
51
|
+
.insert(wallet)
|
|
52
|
+
.values({ userId: randomUUID(), balance: '100000', currency: 'USD', ...overrides })
|
|
53
|
+
.returning();
|
|
54
|
+
return row;
|
|
55
|
+
}
|
|
56
|
+
async function txById(id) {
|
|
57
|
+
const [row] = await db.drizzle.db
|
|
58
|
+
.select()
|
|
59
|
+
.from(walletTransaction)
|
|
60
|
+
.where(eq(walletTransaction.id, id));
|
|
61
|
+
return row;
|
|
66
62
|
}
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
db = await createTestDb([migrate]);
|
|
69
65
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
],
|
|
81
|
-
returning: [...holdReturning(), ...finalizeReturning()],
|
|
82
|
-
});
|
|
83
|
-
const setSpy = readPrivate(dz.db, 'set');
|
|
84
|
-
const { svc, payment, audit } = makeSvc({
|
|
85
|
-
drizzle: dz,
|
|
86
|
-
directory: verifiedDirectory(),
|
|
87
|
-
autoWithdrawal: { fiatThreshold: '1000' },
|
|
88
|
-
});
|
|
66
|
+
afterAll(async () => {
|
|
67
|
+
await db.drop();
|
|
68
|
+
});
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
await db.drizzle.db.execute(sql `TRUNCATE ${walletTransaction}, ${autoWithdrawalRule}, ${wallet} RESTART IDENTITY CASCADE`);
|
|
71
|
+
});
|
|
72
|
+
describe('WalletService.withdraw auto-approval (real PG)', () => {
|
|
73
|
+
it('auto-approves a fiat withdrawal that clears every gate, marking the system as reviewer', async () => {
|
|
74
|
+
const { svc, psp, audit, events } = makeService({ autoWithdrawal: { fiatThreshold: '1000' } });
|
|
75
|
+
const w = await seedWallet();
|
|
89
76
|
const result = await svc.withdraw({
|
|
90
|
-
userId:
|
|
77
|
+
userId: w.userId,
|
|
91
78
|
amount: '40',
|
|
92
79
|
currency: 'USD',
|
|
93
80
|
...NO_CLIENT_META,
|
|
94
81
|
});
|
|
95
|
-
expect(result).
|
|
96
|
-
expect(
|
|
97
|
-
expect(
|
|
98
|
-
|
|
82
|
+
expect(result.status).toBe('completed');
|
|
83
|
+
expect(psp.processWithdrawal).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(await txById(result.transactionId)).toMatchObject({
|
|
85
|
+
status: 'completed',
|
|
86
|
+
reviewedBy: null,
|
|
87
|
+
reviewReason: 'auto-approved',
|
|
88
|
+
providerName: 'psp',
|
|
89
|
+
});
|
|
90
|
+
expect(events.emit.mock.calls.map(([topic]) => topic)).toEqual([
|
|
91
|
+
'wallet.withdrawal.requested',
|
|
92
|
+
'wallet.withdrawal.completed',
|
|
93
|
+
]);
|
|
99
94
|
expect(audit.record).toHaveBeenCalledWith(expect.objectContaining({
|
|
100
95
|
actorType: 'system',
|
|
101
96
|
action: 'wallet.withdrawal.auto_approved',
|
|
102
97
|
resourceType: 'wallet_transaction',
|
|
103
|
-
resourceId:
|
|
98
|
+
resourceId: result.transactionId,
|
|
104
99
|
after: expect.objectContaining({
|
|
105
100
|
threshold: '1000',
|
|
106
101
|
thresholdSource: 'global',
|
|
107
102
|
kycStatus: 'verified',
|
|
108
|
-
|
|
103
|
+
cumulativeCountUsed: 0,
|
|
109
104
|
}),
|
|
110
105
|
}));
|
|
111
106
|
});
|
|
112
|
-
it('
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
select: [
|
|
117
|
-
...holdSelect().map((r) => [r]),
|
|
118
|
-
[], // resolveAutoThreshold -> global fallback
|
|
119
|
-
[{ n: 1 }], // velocity
|
|
120
|
-
[{ total: '0', n: 0 }], // caps
|
|
121
|
-
...finalizeSelect(),
|
|
122
|
-
],
|
|
123
|
-
returning: [...holdReturning(), ...finalizeReturning()],
|
|
124
|
-
});
|
|
125
|
-
const setSpy = readPrivate(dz.db, 'set');
|
|
126
|
-
const executeSpy = readPrivate(dz.db, 'execute');
|
|
127
|
-
const { svc } = makeSvc({
|
|
128
|
-
drizzle: dz,
|
|
129
|
-
directory: verifiedDirectory(),
|
|
130
|
-
autoWithdrawal: { fiatThreshold: '1000', dailyCapCount: 1 },
|
|
131
|
-
});
|
|
132
|
-
await svc.withdraw({ userId: 'u-1', amount: '40', currency: 'USD', ...NO_CLIENT_META });
|
|
133
|
-
expect(executeSpy).toHaveBeenCalledTimes(1);
|
|
134
|
-
const lockOrder = executeSpy.mock.invocationCallOrder.at(0);
|
|
135
|
-
if (lockOrder === undefined) {
|
|
136
|
-
throw new Error('expected executeSpy to have been called');
|
|
137
|
-
}
|
|
138
|
-
// The lock must precede the flip `.set()` that writes the auto-approved marker (and the cap read between them).
|
|
139
|
-
const flipCallIndex = setSpy.mock.calls.findIndex(([arg]) => arg?.reviewReason === 'auto-approved');
|
|
140
|
-
expect(flipCallIndex).toBeGreaterThanOrEqual(0);
|
|
141
|
-
const flipOrder = setSpy.mock.invocationCallOrder.at(flipCallIndex);
|
|
142
|
-
if (flipOrder === undefined) {
|
|
143
|
-
throw new Error('expected the flip call to have been recorded');
|
|
144
|
-
}
|
|
145
|
-
expect(lockOrder).toBeLessThan(flipOrder);
|
|
146
|
-
});
|
|
147
|
-
it('reverts the flip to pending when the audit write fails, instead of leaving it stuck processing', async () => {
|
|
148
|
-
const dz = makeDrizzle({
|
|
149
|
-
select: [
|
|
150
|
-
...holdSelect().map((r) => [r]),
|
|
151
|
-
[], // resolveAutoThreshold -> global fallback
|
|
152
|
-
[{ n: 1 }], // velocity
|
|
153
|
-
[{ total: '0', n: 0 }], // caps
|
|
154
|
-
[
|
|
155
|
-
{
|
|
156
|
-
id: 'tx-1',
|
|
157
|
-
walletId: 'w-1',
|
|
158
|
-
type: 'withdrawal',
|
|
159
|
-
status: 'pending',
|
|
160
|
-
amount: '40',
|
|
161
|
-
currency: 'USD',
|
|
162
|
-
},
|
|
163
|
-
], // flipToProcessing's FOR UPDATE select
|
|
164
|
-
],
|
|
165
|
-
returning: [
|
|
166
|
-
...holdReturning(),
|
|
167
|
-
[
|
|
168
|
-
{
|
|
169
|
-
id: 'tx-1',
|
|
170
|
-
walletId: 'w-1',
|
|
171
|
-
status: 'processing',
|
|
172
|
-
amount: '40',
|
|
173
|
-
currency: 'USD',
|
|
174
|
-
rail: 'fiat',
|
|
175
|
-
},
|
|
176
|
-
], // flipToProcessing's update...returning
|
|
177
|
-
],
|
|
178
|
-
});
|
|
179
|
-
const setSpy = readPrivate(dz.db, 'set');
|
|
180
|
-
const { svc, audit, payment } = makeSvc({
|
|
181
|
-
drizzle: dz,
|
|
182
|
-
directory: verifiedDirectory(),
|
|
183
|
-
autoWithdrawal: { fiatThreshold: '1000' },
|
|
184
|
-
});
|
|
185
|
-
audit.record.mockRejectedValueOnce(new Error('audit down'));
|
|
107
|
+
it('reverts the flip to pending when the audit write fails instead of stranding it processing', async () => {
|
|
108
|
+
const { svc, audit, psp } = makeService({ autoWithdrawal: { fiatThreshold: '1000' } });
|
|
109
|
+
audit.record.mockRejectedValueOnce(new Error('audit sink down'));
|
|
110
|
+
const w = await seedWallet();
|
|
186
111
|
const result = await svc.withdraw({
|
|
187
|
-
userId:
|
|
112
|
+
userId: w.userId,
|
|
188
113
|
amount: '40',
|
|
189
114
|
currency: 'USD',
|
|
190
115
|
...NO_CLIENT_META,
|
|
191
116
|
});
|
|
192
|
-
expect(result).
|
|
193
|
-
expect(
|
|
194
|
-
expect(
|
|
195
|
-
expect(setSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
117
|
+
expect(result.status).toBe('pending');
|
|
118
|
+
expect(psp.processWithdrawal).not.toHaveBeenCalled();
|
|
119
|
+
expect(await txById(result.transactionId)).toMatchObject({
|
|
196
120
|
status: 'pending',
|
|
197
121
|
reviewedBy: null,
|
|
198
122
|
reviewedAt: null,
|
|
199
123
|
reviewReason: null,
|
|
200
|
-
}));
|
|
201
|
-
});
|
|
202
|
-
it('stays pending when KYC is not passing even though kyc.gateWithdrawals is false', async () => {
|
|
203
|
-
const dz = makeDrizzle({
|
|
204
|
-
select: [...holdSelect().map((r) => [r]), []],
|
|
205
|
-
returning: [...holdReturning()],
|
|
206
|
-
});
|
|
207
|
-
const { svc, payment } = makeSvc({
|
|
208
|
-
drizzle: dz,
|
|
209
|
-
directory: {
|
|
210
|
-
lookupPlayers: vi.fn().mockResolvedValue([{ userId: 'u-1', kycStatus: 'pending' }]),
|
|
211
|
-
},
|
|
212
|
-
kyc: { gateWithdrawals: false },
|
|
213
|
-
autoWithdrawal: { fiatThreshold: '1000' },
|
|
214
124
|
});
|
|
125
|
+
});
|
|
126
|
+
it('holds the funds even when auto-approval declines', async () => {
|
|
127
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '10' } });
|
|
128
|
+
const w = await seedWallet({ balance: '100' });
|
|
215
129
|
const result = await svc.withdraw({
|
|
216
|
-
userId:
|
|
130
|
+
userId: w.userId,
|
|
217
131
|
amount: '40',
|
|
218
132
|
currency: 'USD',
|
|
219
133
|
...NO_CLIENT_META,
|
|
220
134
|
});
|
|
221
135
|
expect(result.status).toBe('pending');
|
|
222
|
-
|
|
136
|
+
const [row] = await db.drizzle.db.select().from(wallet).where(eq(wallet.id, w.id));
|
|
137
|
+
expect(Number(row?.balance)).toBe(60);
|
|
223
138
|
});
|
|
224
|
-
it('stays pending when the
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
riskTags: {
|
|
233
|
-
getActiveTagKeys: vi.fn().mockResolvedValue(new Map([['u-1', ['high_risk']]])),
|
|
234
|
-
},
|
|
235
|
-
autoWithdrawal: { fiatThreshold: '1000', excludeRiskFlags: ['high_risk'] },
|
|
139
|
+
it('stays pending when the amount exceeds the configured threshold', async () => {
|
|
140
|
+
const { svc, psp } = makeService({ autoWithdrawal: { fiatThreshold: '10' } });
|
|
141
|
+
const w = await seedWallet();
|
|
142
|
+
const result = await svc.withdraw({
|
|
143
|
+
userId: w.userId,
|
|
144
|
+
amount: '40',
|
|
145
|
+
currency: 'USD',
|
|
146
|
+
...NO_CLIENT_META,
|
|
236
147
|
});
|
|
148
|
+
expect(result.status).toBe('pending');
|
|
149
|
+
expect(psp.processWithdrawal).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
it('stays pending when no threshold is configured at all', async () => {
|
|
152
|
+
const { svc } = makeService({ autoWithdrawal: {} });
|
|
153
|
+
const w = await seedWallet();
|
|
237
154
|
const result = await svc.withdraw({
|
|
238
|
-
userId:
|
|
155
|
+
userId: w.userId,
|
|
239
156
|
amount: '40',
|
|
240
157
|
currency: 'USD',
|
|
241
158
|
...NO_CLIENT_META,
|
|
242
159
|
});
|
|
243
160
|
expect(result.status).toBe('pending');
|
|
244
|
-
expect(payment.processWithdrawal).not.toHaveBeenCalled();
|
|
245
161
|
});
|
|
246
|
-
it('stays pending
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
162
|
+
it('stays pending when auto-withdrawal is not enabled', async () => {
|
|
163
|
+
const { svc, audit } = makeService();
|
|
164
|
+
const w = await seedWallet();
|
|
165
|
+
const result = await svc.withdraw({
|
|
166
|
+
userId: w.userId,
|
|
167
|
+
amount: '40',
|
|
168
|
+
currency: 'USD',
|
|
169
|
+
...NO_CLIENT_META,
|
|
251
170
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
171
|
+
expect(result.status).toBe('pending');
|
|
172
|
+
expect(audit.record).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
it('stays pending when KYC is not passing even though gateWithdrawals is off', async () => {
|
|
175
|
+
const { svc } = makeService({
|
|
255
176
|
autoWithdrawal: { fiatThreshold: '1000' },
|
|
177
|
+
kycStatus: 'pending',
|
|
256
178
|
});
|
|
179
|
+
const w = await seedWallet();
|
|
257
180
|
const result = await svc.withdraw({
|
|
258
|
-
userId:
|
|
181
|
+
userId: w.userId,
|
|
259
182
|
amount: '40',
|
|
260
183
|
currency: 'USD',
|
|
261
184
|
...NO_CLIENT_META,
|
|
262
185
|
});
|
|
263
186
|
expect(result.status).toBe('pending');
|
|
264
|
-
expect(payment.processWithdrawal).not.toHaveBeenCalled();
|
|
265
187
|
});
|
|
266
|
-
it('
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
271
|
-
const { svc, payment } = makeSvc({
|
|
272
|
-
drizzle: dz,
|
|
273
|
-
directory: verifiedDirectory(),
|
|
274
|
-
autoWithdrawal: { fiatThreshold: '1000', dailyCapCount: 1 },
|
|
188
|
+
it('fails closed to pending when the directory throws during the KYC check', async () => {
|
|
189
|
+
const { svc } = makeService({
|
|
190
|
+
autoWithdrawal: { fiatThreshold: '1000' },
|
|
191
|
+
directoryThrows: true,
|
|
275
192
|
});
|
|
193
|
+
const w = await seedWallet();
|
|
276
194
|
const result = await svc.withdraw({
|
|
277
|
-
userId:
|
|
195
|
+
userId: w.userId,
|
|
278
196
|
amount: '40',
|
|
279
197
|
currency: 'USD',
|
|
280
198
|
...NO_CLIENT_META,
|
|
281
199
|
});
|
|
282
200
|
expect(result.status).toBe('pending');
|
|
283
|
-
expect(payment.processWithdrawal).not.toHaveBeenCalled();
|
|
284
201
|
});
|
|
285
|
-
it('stays pending when
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
});
|
|
290
|
-
const { svc, payment } = makeSvc({
|
|
291
|
-
drizzle: dz,
|
|
292
|
-
directory: verifiedDirectory(),
|
|
293
|
-
autoWithdrawal: { fiatThreshold: '1000', dailyCapAmount: '100' },
|
|
202
|
+
it('stays pending when the player carries an excluded risk flag', async () => {
|
|
203
|
+
const { svc } = makeService({
|
|
204
|
+
autoWithdrawal: { fiatThreshold: '1000', excludeRiskFlags: ['bonus_abuser'] },
|
|
205
|
+
riskTags: ['bonus_abuser'],
|
|
294
206
|
});
|
|
207
|
+
const w = await seedWallet();
|
|
295
208
|
const result = await svc.withdraw({
|
|
296
|
-
userId:
|
|
209
|
+
userId: w.userId,
|
|
297
210
|
amount: '40',
|
|
298
211
|
currency: 'USD',
|
|
299
212
|
...NO_CLIENT_META,
|
|
300
213
|
});
|
|
301
214
|
expect(result.status).toBe('pending');
|
|
302
|
-
expect(payment.processWithdrawal).not.toHaveBeenCalled();
|
|
303
215
|
});
|
|
304
|
-
it('
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
216
|
+
it('auto-approves when the player carries only unrelated tags', async () => {
|
|
217
|
+
const { svc } = makeService({
|
|
218
|
+
autoWithdrawal: { fiatThreshold: '1000', excludeRiskFlags: ['bonus_abuser'] },
|
|
219
|
+
riskTags: ['vip'],
|
|
308
220
|
});
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
221
|
+
const w = await seedWallet();
|
|
222
|
+
const result = await svc.withdraw({
|
|
223
|
+
userId: w.userId,
|
|
224
|
+
amount: '40',
|
|
225
|
+
currency: 'USD',
|
|
226
|
+
...NO_CLIENT_META,
|
|
227
|
+
});
|
|
228
|
+
expect(result.status).toBe('completed');
|
|
229
|
+
});
|
|
230
|
+
it('fails closed to pending when risk flags are configured but the tags port is unbound', async () => {
|
|
231
|
+
const { svc } = makeService({
|
|
232
|
+
autoWithdrawal: { fiatThreshold: '1000', excludeRiskFlags: ['bonus_abuser'] },
|
|
233
|
+
riskTags: 'unbound',
|
|
313
234
|
});
|
|
235
|
+
const w = await seedWallet();
|
|
314
236
|
const result = await svc.withdraw({
|
|
315
|
-
userId:
|
|
237
|
+
userId: w.userId,
|
|
316
238
|
amount: '40',
|
|
317
239
|
currency: 'USD',
|
|
318
240
|
...NO_CLIENT_META,
|
|
319
241
|
});
|
|
320
242
|
expect(result.status).toBe('pending');
|
|
321
|
-
expect(payment.processWithdrawal).not.toHaveBeenCalled();
|
|
322
243
|
});
|
|
323
|
-
it('
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
autoWithdrawal: { fiatThreshold: '1000' },
|
|
244
|
+
it('stays pending on the large_amount heuristic regardless of the threshold', async () => {
|
|
245
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '100000' } });
|
|
246
|
+
const w = await seedWallet();
|
|
247
|
+
const result = await svc.withdraw({
|
|
248
|
+
userId: w.userId,
|
|
249
|
+
amount: '5000',
|
|
250
|
+
currency: 'USD',
|
|
251
|
+
...NO_CLIENT_META,
|
|
332
252
|
});
|
|
253
|
+
expect(result.status).toBe('pending');
|
|
254
|
+
});
|
|
255
|
+
it('stays pending once the wallet hits the high_frequency velocity flag', async () => {
|
|
256
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '1000' } });
|
|
257
|
+
const w = await seedWallet();
|
|
258
|
+
await db.drizzle.db.insert(walletTransaction).values([
|
|
259
|
+
{ walletId: w.id, type: 'withdrawal', amount: '1', currency: 'USD', status: 'completed' },
|
|
260
|
+
{ walletId: w.id, type: 'withdrawal', amount: '1', currency: 'USD', status: 'completed' },
|
|
261
|
+
]);
|
|
333
262
|
const result = await svc.withdraw({
|
|
334
|
-
userId:
|
|
263
|
+
userId: w.userId,
|
|
335
264
|
amount: '40',
|
|
336
265
|
currency: 'USD',
|
|
337
266
|
...NO_CLIENT_META,
|
|
338
267
|
});
|
|
339
268
|
expect(result.status).toBe('pending');
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
269
|
+
});
|
|
270
|
+
it('ignores withdrawals outside the velocity window', async () => {
|
|
271
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '1000' } });
|
|
272
|
+
const w = await seedWallet();
|
|
273
|
+
const outsideWindow = new Date(Date.now() - 48 * 60 * 60 * 1000);
|
|
274
|
+
await db.drizzle.db.insert(walletTransaction).values([
|
|
275
|
+
{
|
|
276
|
+
walletId: w.id,
|
|
277
|
+
type: 'withdrawal',
|
|
278
|
+
amount: '1',
|
|
279
|
+
currency: 'USD',
|
|
280
|
+
status: 'completed',
|
|
281
|
+
createdAt: outsideWindow,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
walletId: w.id,
|
|
285
|
+
type: 'withdrawal',
|
|
286
|
+
amount: '1',
|
|
287
|
+
currency: 'USD',
|
|
288
|
+
status: 'completed',
|
|
289
|
+
createdAt: outsideWindow,
|
|
290
|
+
},
|
|
291
|
+
]);
|
|
292
|
+
const result = await svc.withdraw({
|
|
293
|
+
userId: w.userId,
|
|
294
|
+
amount: '40',
|
|
295
|
+
currency: 'USD',
|
|
296
|
+
...NO_CLIENT_META,
|
|
297
|
+
});
|
|
298
|
+
expect(result.status).toBe('completed');
|
|
299
|
+
});
|
|
300
|
+
it('stays pending when a configured daily count cap would be exceeded', async () => {
|
|
301
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '1000', dailyCapCount: 1 } });
|
|
302
|
+
const w = await seedWallet();
|
|
303
|
+
await db.drizzle.db.insert(walletTransaction).values({
|
|
304
|
+
walletId: w.id,
|
|
305
|
+
type: 'withdrawal',
|
|
306
|
+
amount: '10',
|
|
307
|
+
currency: 'USD',
|
|
308
|
+
status: 'completed',
|
|
309
|
+
reviewReason: 'auto-approved',
|
|
355
310
|
});
|
|
356
311
|
const result = await svc.withdraw({
|
|
357
|
-
userId:
|
|
358
|
-
amount: '
|
|
359
|
-
currency: '
|
|
360
|
-
destinationAddress: 'bc1qtest',
|
|
312
|
+
userId: w.userId,
|
|
313
|
+
amount: '40',
|
|
314
|
+
currency: 'USD',
|
|
361
315
|
...NO_CLIENT_META,
|
|
362
316
|
});
|
|
363
317
|
expect(result.status).toBe('pending');
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
id: 'tx-1',
|
|
378
|
-
walletId: 'w-1',
|
|
379
|
-
type: 'withdrawal',
|
|
380
|
-
status: 'pending',
|
|
381
|
-
amount: '0.5',
|
|
382
|
-
currency: 'BTC',
|
|
383
|
-
},
|
|
384
|
-
],
|
|
385
|
-
[{ userId: 'u-1' }],
|
|
386
|
-
[],
|
|
387
|
-
],
|
|
388
|
-
returning: [
|
|
389
|
-
...holdReturning(insertRow({ currency: 'BTC', rail: 'crypto', amount: '0.5' })),
|
|
390
|
-
[
|
|
391
|
-
{
|
|
392
|
-
id: 'tx-1',
|
|
393
|
-
walletId: 'w-1',
|
|
394
|
-
status: 'processing',
|
|
395
|
-
amount: '0.5',
|
|
396
|
-
currency: 'BTC',
|
|
397
|
-
rail: 'crypto',
|
|
398
|
-
},
|
|
399
|
-
],
|
|
400
|
-
],
|
|
401
|
-
});
|
|
402
|
-
const { svc, payment, audit } = makeSvc({
|
|
403
|
-
drizzle: dz,
|
|
404
|
-
directory: verifiedDirectory(),
|
|
405
|
-
autoWithdrawal: { cryptoThreshold: '1' },
|
|
318
|
+
});
|
|
319
|
+
it('stays pending when a configured daily amount cap would be exceeded', async () => {
|
|
320
|
+
const { svc } = makeService({
|
|
321
|
+
autoWithdrawal: { fiatThreshold: '1000', dailyCapAmount: '50' },
|
|
322
|
+
});
|
|
323
|
+
const w = await seedWallet();
|
|
324
|
+
await db.drizzle.db.insert(walletTransaction).values({
|
|
325
|
+
walletId: w.id,
|
|
326
|
+
type: 'withdrawal',
|
|
327
|
+
amount: '20',
|
|
328
|
+
currency: 'USD',
|
|
329
|
+
status: 'completed',
|
|
330
|
+
reviewReason: 'auto-approved',
|
|
406
331
|
});
|
|
407
332
|
const result = await svc.withdraw({
|
|
408
|
-
userId:
|
|
409
|
-
amount: '
|
|
410
|
-
currency: '
|
|
411
|
-
destinationAddress: 'bc1qtest',
|
|
333
|
+
userId: w.userId,
|
|
334
|
+
amount: '40',
|
|
335
|
+
currency: 'USD',
|
|
412
336
|
...NO_CLIENT_META,
|
|
413
337
|
});
|
|
414
|
-
expect(result).
|
|
415
|
-
expect(payment.processWithdrawal).toHaveBeenCalledTimes(1);
|
|
416
|
-
expect(audit.record).toHaveBeenCalledWith(expect.objectContaining({
|
|
417
|
-
after: expect.objectContaining({ threshold: '1', thresholdSource: 'global' }),
|
|
418
|
-
}));
|
|
338
|
+
expect(result.status).toBe('pending');
|
|
419
339
|
});
|
|
420
|
-
it('
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
340
|
+
it('counts only auto-approved payouts towards the daily cap', async () => {
|
|
341
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '1000', dailyCapCount: 1 } });
|
|
342
|
+
const w = await seedWallet();
|
|
343
|
+
await db.drizzle.db.insert(walletTransaction).values({
|
|
344
|
+
walletId: w.id,
|
|
345
|
+
type: 'withdrawal',
|
|
346
|
+
amount: '10',
|
|
347
|
+
currency: 'USD',
|
|
348
|
+
status: 'completed',
|
|
349
|
+
reviewReason: 'manual sign-off',
|
|
425
350
|
});
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
351
|
+
const result = await svc.withdraw({
|
|
352
|
+
userId: w.userId,
|
|
353
|
+
amount: '40',
|
|
354
|
+
currency: 'USD',
|
|
355
|
+
...NO_CLIENT_META,
|
|
430
356
|
});
|
|
357
|
+
expect(result.status).toBe('completed');
|
|
358
|
+
});
|
|
359
|
+
it('serializes concurrent withdrawals so a count cap of one cannot be bypassed', async () => {
|
|
360
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '1000', dailyCapCount: 1 } });
|
|
361
|
+
const w = await seedWallet();
|
|
362
|
+
const results = await Promise.all([
|
|
363
|
+
svc.withdraw({ userId: w.userId, amount: '40', currency: 'USD', ...NO_CLIENT_META }),
|
|
364
|
+
svc.withdraw({ userId: w.userId, amount: '41', currency: 'USD', ...NO_CLIENT_META }),
|
|
365
|
+
]);
|
|
366
|
+
expect(results.filter((r) => r.status === 'completed')).toHaveLength(1);
|
|
367
|
+
expect(results.filter((r) => r.status === 'pending')).toHaveLength(1);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
describe('WalletService.withdraw auto-approval - crypto rail (real PG)', () => {
|
|
371
|
+
it('stays pending when no cryptoThreshold is configured, even with a fiat one', async () => {
|
|
372
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '1000' } });
|
|
373
|
+
const w = await seedWallet({ currency: 'BTC', balance: '10' });
|
|
431
374
|
const result = await svc.withdraw({
|
|
432
|
-
userId:
|
|
433
|
-
amount: '
|
|
375
|
+
userId: w.userId,
|
|
376
|
+
amount: '1',
|
|
434
377
|
currency: 'BTC',
|
|
435
|
-
destinationAddress: '
|
|
378
|
+
destinationAddress: 'bc1qexample',
|
|
436
379
|
...NO_CLIENT_META,
|
|
437
380
|
});
|
|
438
381
|
expect(result.status).toBe('pending');
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
382
|
+
});
|
|
383
|
+
it('auto-approves once an operator configures a positive cryptoThreshold', async () => {
|
|
384
|
+
const { svc } = makeService({ autoWithdrawal: { cryptoThreshold: '2' } });
|
|
385
|
+
const w = await seedWallet({ currency: 'BTC', balance: '10' });
|
|
386
|
+
const result = await svc.withdraw({
|
|
387
|
+
userId: w.userId,
|
|
388
|
+
amount: '1',
|
|
389
|
+
currency: 'BTC',
|
|
390
|
+
destinationAddress: 'bc1qexample',
|
|
391
|
+
...NO_CLIENT_META,
|
|
392
|
+
});
|
|
393
|
+
expect(result.status).toBe('completed');
|
|
394
|
+
expect(await txById(result.transactionId)).toMatchObject({
|
|
395
|
+
rail: 'crypto',
|
|
396
|
+
providerName: 'fireblocks',
|
|
397
|
+
reviewReason: 'auto-approved',
|
|
453
398
|
});
|
|
399
|
+
});
|
|
400
|
+
it('stays pending when the crypto amount exceeds cryptoThreshold', async () => {
|
|
401
|
+
const { svc } = makeService({ autoWithdrawal: { cryptoThreshold: '0.5' } });
|
|
402
|
+
const w = await seedWallet({ currency: 'BTC', balance: '10' });
|
|
454
403
|
const result = await svc.withdraw({
|
|
455
|
-
userId:
|
|
456
|
-
amount: '
|
|
404
|
+
userId: w.userId,
|
|
405
|
+
amount: '1',
|
|
457
406
|
currency: 'BTC',
|
|
458
|
-
destinationAddress: '
|
|
407
|
+
destinationAddress: 'bc1qexample',
|
|
459
408
|
...NO_CLIENT_META,
|
|
460
409
|
});
|
|
461
410
|
expect(result.status).toBe('pending');
|
|
462
|
-
expect(payment.processWithdrawal).not.toHaveBeenCalled();
|
|
463
411
|
});
|
|
464
|
-
it('
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
});
|
|
469
|
-
const { svc, payment } = makeSvc({
|
|
470
|
-
drizzle: dz,
|
|
471
|
-
directory: verifiedDirectory(),
|
|
472
|
-
autoWithdrawal: {}, // enabled, but no fiatThreshold
|
|
412
|
+
it('applies the same KYC guard as the fiat rail', async () => {
|
|
413
|
+
const { svc } = makeService({
|
|
414
|
+
autoWithdrawal: { cryptoThreshold: '2' },
|
|
415
|
+
kycStatus: 'rejected',
|
|
473
416
|
});
|
|
417
|
+
const w = await seedWallet({ currency: 'BTC', balance: '10' });
|
|
474
418
|
const result = await svc.withdraw({
|
|
475
|
-
userId:
|
|
476
|
-
amount: '
|
|
477
|
-
currency: '
|
|
419
|
+
userId: w.userId,
|
|
420
|
+
amount: '1',
|
|
421
|
+
currency: 'BTC',
|
|
422
|
+
destinationAddress: 'bc1qexample',
|
|
478
423
|
...NO_CLIENT_META,
|
|
479
424
|
});
|
|
480
425
|
expect(result.status).toBe('pending');
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
drizzle: dz,
|
|
493
|
-
directory: verifiedDirectory(),
|
|
494
|
-
autoWithdrawal: { fiatThreshold: '1000' },
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
describe('WalletService auto-approval threshold resolution (real PG)', () => {
|
|
429
|
+
it('a per-player rule below the global threshold blocks what global would allow', async () => {
|
|
430
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '1000' } });
|
|
431
|
+
const w = await seedWallet();
|
|
432
|
+
await svc.setAutoWithdrawalRule({
|
|
433
|
+
userId: w.userId,
|
|
434
|
+
threshold: '10',
|
|
435
|
+
reason: 'watchlist',
|
|
436
|
+
createdBy: randomUUID(),
|
|
495
437
|
});
|
|
496
438
|
const result = await svc.withdraw({
|
|
497
|
-
userId:
|
|
439
|
+
userId: w.userId,
|
|
498
440
|
amount: '40',
|
|
499
441
|
currency: 'USD',
|
|
500
442
|
...NO_CLIENT_META,
|
|
501
443
|
});
|
|
502
444
|
expect(result.status).toBe('pending');
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
...finalizeSelect(),
|
|
513
|
-
],
|
|
514
|
-
returning: [...holdReturning(), ...finalizeReturning()],
|
|
515
|
-
});
|
|
516
|
-
const { svc, payment, audit } = makeSvc({
|
|
517
|
-
drizzle: dz,
|
|
518
|
-
directory: verifiedDirectory(),
|
|
519
|
-
autoWithdrawal: { fiatThreshold: '10' },
|
|
445
|
+
});
|
|
446
|
+
it('a per-player rule above the global threshold allows what global would block', async () => {
|
|
447
|
+
const { svc, audit } = makeService({ autoWithdrawal: { fiatThreshold: '10' } });
|
|
448
|
+
const w = await seedWallet();
|
|
449
|
+
await svc.setAutoWithdrawalRule({
|
|
450
|
+
userId: w.userId,
|
|
451
|
+
threshold: '1000',
|
|
452
|
+
reason: 'trusted',
|
|
453
|
+
createdBy: randomUUID(),
|
|
520
454
|
});
|
|
521
455
|
const result = await svc.withdraw({
|
|
522
|
-
userId:
|
|
456
|
+
userId: w.userId,
|
|
523
457
|
amount: '40',
|
|
524
458
|
currency: 'USD',
|
|
525
459
|
...NO_CLIENT_META,
|
|
526
460
|
});
|
|
527
461
|
expect(result.status).toBe('completed');
|
|
528
|
-
expect(payment.processWithdrawal).toHaveBeenCalledTimes(1);
|
|
529
462
|
expect(audit.record).toHaveBeenCalledWith(expect.objectContaining({
|
|
530
|
-
after: expect.objectContaining({
|
|
463
|
+
after: expect.objectContaining({ thresholdSource: 'per-player' }),
|
|
531
464
|
}));
|
|
532
465
|
});
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
[
|
|
540
|
-
{
|
|
541
|
-
id: 'r-1',
|
|
542
|
-
userId: 'u-1',
|
|
543
|
-
threshold: '500',
|
|
544
|
-
reason: 'trusted',
|
|
545
|
-
createdBy: 'a-1',
|
|
546
|
-
createdAt: new Date('2026-01-01T00:00:00Z'),
|
|
547
|
-
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
548
|
-
},
|
|
549
|
-
],
|
|
550
|
-
],
|
|
551
|
-
});
|
|
552
|
-
const { svc } = makeSvc({ drizzle: dz });
|
|
553
|
-
const rule = await svc.setAutoWithdrawalRule({
|
|
554
|
-
userId: 'u-1',
|
|
555
|
-
threshold: '500',
|
|
466
|
+
it('a per-player rule overrides the crypto threshold too', async () => {
|
|
467
|
+
const { svc } = makeService({ autoWithdrawal: { cryptoThreshold: '0.5' } });
|
|
468
|
+
const w = await seedWallet({ currency: 'BTC', balance: '10' });
|
|
469
|
+
await svc.setAutoWithdrawalRule({
|
|
470
|
+
userId: w.userId,
|
|
471
|
+
threshold: '5',
|
|
556
472
|
reason: 'trusted',
|
|
557
|
-
createdBy:
|
|
473
|
+
createdBy: randomUUID(),
|
|
558
474
|
});
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
475
|
+
const result = await svc.withdraw({
|
|
476
|
+
userId: w.userId,
|
|
477
|
+
amount: '1',
|
|
478
|
+
currency: 'BTC',
|
|
479
|
+
destinationAddress: 'bc1qexample',
|
|
480
|
+
...NO_CLIENT_META,
|
|
481
|
+
});
|
|
482
|
+
expect(result.status).toBe('completed');
|
|
483
|
+
});
|
|
484
|
+
it('a zero per-player threshold turns auto-approval off for that player', async () => {
|
|
485
|
+
const { svc } = makeService({ autoWithdrawal: { fiatThreshold: '1000' } });
|
|
486
|
+
const w = await seedWallet();
|
|
487
|
+
await svc.setAutoWithdrawalRule({
|
|
488
|
+
userId: w.userId,
|
|
489
|
+
threshold: '0',
|
|
490
|
+
reason: 'manual only',
|
|
491
|
+
createdBy: randomUUID(),
|
|
492
|
+
});
|
|
493
|
+
const result = await svc.withdraw({
|
|
494
|
+
userId: w.userId,
|
|
495
|
+
amount: '40',
|
|
496
|
+
currency: 'USD',
|
|
497
|
+
...NO_CLIENT_META,
|
|
564
498
|
});
|
|
565
|
-
expect(
|
|
499
|
+
expect(result.status).toBe('pending');
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
describe('WalletService auto-withdrawal rule methods (real PG)', () => {
|
|
503
|
+
it('setAutoWithdrawalRule inserts, then upserts on a repeat for the same player', async () => {
|
|
504
|
+
const { svc } = makeService();
|
|
505
|
+
const userId = randomUUID();
|
|
506
|
+
const createdBy = randomUUID();
|
|
507
|
+
const created = await svc.setAutoWithdrawalRule({
|
|
508
|
+
userId,
|
|
509
|
+
threshold: '500',
|
|
510
|
+
reason: 'initial',
|
|
511
|
+
createdBy,
|
|
512
|
+
});
|
|
513
|
+
const updated = await svc.setAutoWithdrawalRule({
|
|
514
|
+
userId,
|
|
515
|
+
threshold: '750',
|
|
516
|
+
reason: 'raised',
|
|
517
|
+
createdBy,
|
|
518
|
+
});
|
|
519
|
+
expect(created.id).toBe(updated.id);
|
|
520
|
+
expect(Number(updated.threshold)).toBe(750);
|
|
521
|
+
expect(updated.reason).toBe('raised');
|
|
522
|
+
const rows = await db.drizzle.db
|
|
523
|
+
.select()
|
|
524
|
+
.from(autoWithdrawalRule)
|
|
525
|
+
.where(eq(autoWithdrawalRule.userId, userId));
|
|
526
|
+
expect(rows).toHaveLength(1);
|
|
566
527
|
});
|
|
567
528
|
it('getAutoWithdrawalRule returns null when no rule exists', async () => {
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
529
|
+
const { svc } = makeService();
|
|
530
|
+
expect(await svc.getAutoWithdrawalRule(randomUUID())).toBeNull();
|
|
531
|
+
});
|
|
532
|
+
it('getAutoWithdrawalRule serializes the timestamps as ISO strings', async () => {
|
|
533
|
+
const { svc } = makeService();
|
|
534
|
+
const userId = randomUUID();
|
|
535
|
+
await svc.setAutoWithdrawalRule({
|
|
536
|
+
userId,
|
|
537
|
+
threshold: '500',
|
|
538
|
+
reason: 'initial',
|
|
539
|
+
createdBy: randomUUID(),
|
|
540
|
+
});
|
|
541
|
+
const rule = await svc.getAutoWithdrawalRule(userId);
|
|
542
|
+
expect(rule?.createdAt).toBe(new Date(rule?.createdAt ?? '').toISOString());
|
|
571
543
|
});
|
|
572
544
|
it('deleteAutoWithdrawalRule reports whether a row was removed', async () => {
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
545
|
+
const { svc } = makeService();
|
|
546
|
+
const userId = randomUUID();
|
|
547
|
+
await svc.setAutoWithdrawalRule({
|
|
548
|
+
userId,
|
|
549
|
+
threshold: '500',
|
|
550
|
+
reason: 'initial',
|
|
551
|
+
createdBy: randomUUID(),
|
|
552
|
+
});
|
|
553
|
+
expect(await svc.deleteAutoWithdrawalRule(userId)).toBe(true);
|
|
554
|
+
expect(await svc.deleteAutoWithdrawalRule(userId)).toBe(false);
|
|
555
|
+
expect(await svc.getAutoWithdrawalRule(userId)).toBeNull();
|
|
579
556
|
});
|
|
580
557
|
});
|
|
581
558
|
//# sourceMappingURL=wallet-auto-withdrawal.test.js.map
|