@moneypot/hub 1.1.1 → 1.2.0-dev.1
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/src/__generated__/gql.d.ts +10 -2
- package/dist/src/__generated__/gql.js +5 -1
- package/dist/src/__generated__/graphql.d.ts +552 -138
- package/dist/src/__generated__/graphql.js +62 -23
- package/dist/src/db/index.d.ts +4 -1
- package/dist/src/db/index.js +1 -0
- package/dist/src/db/types.d.ts +36 -0
- package/dist/src/graphql-queries.d.ts +1 -0
- package/dist/src/graphql-queries.js +13 -0
- package/dist/src/pg-advisory-lock.d.ts +8 -0
- package/dist/src/pg-advisory-lock.js +28 -0
- package/dist/src/pg-versions/002-balance-id.sql +11 -0
- package/dist/src/pg-versions/003-take-request.sql +107 -0
- package/dist/src/plugins/hub-withdraw.js +2 -2
- package/dist/src/process-transfers.js +23 -2
- package/dist/src/process-withdrawal-request.js +16 -0
- package/dist/src/take-request/process-take-request.d.ts +7 -0
- package/dist/src/take-request/process-take-request.js +783 -0
- package/package.json +1 -1
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
import { gql } from "../__generated__/gql.js";
|
|
2
|
+
import { TakeRequestStatus as MpTakeRequestStatus, TransferStatusKind as MpTransferStatus, } from "../__generated__/graphql.js";
|
|
3
|
+
import { exactlyOneRow, maybeOneRow, superuserPool, withPgPoolTransaction, } from "../db/index.js";
|
|
4
|
+
import { assert } from "tsafe";
|
|
5
|
+
import { pgAdvisoryLock } from "../pg-advisory-lock.js";
|
|
6
|
+
const MP_PAGINATE_PENDING_TAKE_REQUESTS = gql(`
|
|
7
|
+
query MpPaginatedPendingTakeRequests($controllerId: UUID!, $after: Cursor) {
|
|
8
|
+
allTakeRequests(
|
|
9
|
+
condition: { controllerId: $controllerId, status: PENDING }
|
|
10
|
+
after: $after
|
|
11
|
+
orderBy: ID_ASC
|
|
12
|
+
) {
|
|
13
|
+
pageInfo {
|
|
14
|
+
endCursor
|
|
15
|
+
hasNextPage
|
|
16
|
+
}
|
|
17
|
+
edges {
|
|
18
|
+
cursor
|
|
19
|
+
node {
|
|
20
|
+
id
|
|
21
|
+
status
|
|
22
|
+
amount
|
|
23
|
+
currencyKey
|
|
24
|
+
userId
|
|
25
|
+
experienceId
|
|
26
|
+
experienceTransfer {
|
|
27
|
+
id
|
|
28
|
+
amount
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
`);
|
|
35
|
+
const MP_REJECT_TAKE_REQUEST = gql(`
|
|
36
|
+
mutation MpRejectTakeRequest($mpTakeRequestId: UUID!) {
|
|
37
|
+
rejectTakeRequest(input: { id: $mpTakeRequestId }) {
|
|
38
|
+
result {
|
|
39
|
+
... on RejectTakeRequestSuccess {
|
|
40
|
+
__typename
|
|
41
|
+
takeRequest {
|
|
42
|
+
id
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
... on TakeRequestAlreadyTerminal {
|
|
46
|
+
__typename
|
|
47
|
+
takeRequest {
|
|
48
|
+
id
|
|
49
|
+
status
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`);
|
|
56
|
+
const MP_TRANSFER_TAKE_REQUEST = gql(`
|
|
57
|
+
mutation MpTransferTakeRequest(
|
|
58
|
+
$mpTakeRequestId: UUID!
|
|
59
|
+
$mpExperienceId: UUID!
|
|
60
|
+
$mpUserId: UUID!
|
|
61
|
+
$amount: Int!
|
|
62
|
+
$currencyKey: String!
|
|
63
|
+
) {
|
|
64
|
+
transferCurrencyExperienceToUser(
|
|
65
|
+
input: {
|
|
66
|
+
takeRequestId: $mpTakeRequestId
|
|
67
|
+
experienceId: $mpExperienceId
|
|
68
|
+
userId: $mpUserId
|
|
69
|
+
amount: $amount
|
|
70
|
+
currency: $currencyKey
|
|
71
|
+
}
|
|
72
|
+
) {
|
|
73
|
+
result {
|
|
74
|
+
... on TransferCurrencyExperienceToUserSuccess {
|
|
75
|
+
__typename
|
|
76
|
+
transfer {
|
|
77
|
+
id
|
|
78
|
+
status
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
... on TransferMetadataIdExists {
|
|
82
|
+
__typename
|
|
83
|
+
transfer {
|
|
84
|
+
id
|
|
85
|
+
status
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
... on TakeRequestAlreadyTerminal {
|
|
89
|
+
__typename
|
|
90
|
+
takeRequest {
|
|
91
|
+
id
|
|
92
|
+
status
|
|
93
|
+
experienceTransfer {
|
|
94
|
+
id
|
|
95
|
+
status
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
`);
|
|
103
|
+
const MP_COMPLETE_TRANSFER = gql(`
|
|
104
|
+
mutation CompleteTransfer2($mpTransferId: UUID!) {
|
|
105
|
+
completeTransfer(input: { id: $mpTransferId }) {
|
|
106
|
+
result {
|
|
107
|
+
... on CompleteTransferSuccess {
|
|
108
|
+
__typename
|
|
109
|
+
transfer {
|
|
110
|
+
__typename
|
|
111
|
+
id
|
|
112
|
+
status
|
|
113
|
+
... on ExperienceTransfer {
|
|
114
|
+
id
|
|
115
|
+
takeRequest {
|
|
116
|
+
id
|
|
117
|
+
status
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
... on InvalidTransferStatus {
|
|
123
|
+
__typename
|
|
124
|
+
currentStatus
|
|
125
|
+
message
|
|
126
|
+
transfer {
|
|
127
|
+
__typename
|
|
128
|
+
... on ExperienceTransfer {
|
|
129
|
+
id
|
|
130
|
+
takeRequest {
|
|
131
|
+
id
|
|
132
|
+
status
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
`);
|
|
141
|
+
var LocalTakeRequestStatus;
|
|
142
|
+
(function (LocalTakeRequestStatus) {
|
|
143
|
+
LocalTakeRequestStatus["PENDING"] = "PENDING";
|
|
144
|
+
LocalTakeRequestStatus["PROCESSING"] = "PROCESSING";
|
|
145
|
+
LocalTakeRequestStatus["COMPLETED"] = "COMPLETED";
|
|
146
|
+
LocalTakeRequestStatus["FAILED"] = "FAILED";
|
|
147
|
+
LocalTakeRequestStatus["REJECTED"] = "REJECTED";
|
|
148
|
+
})(LocalTakeRequestStatus || (LocalTakeRequestStatus = {}));
|
|
149
|
+
export async function processTakeRequests({ abortSignal, controllerId, casinoId, graphqlClient, }) {
|
|
150
|
+
if (abortSignal.aborted) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const takeRequests = await fetchPendingTakeRequests(graphqlClient, controllerId);
|
|
154
|
+
console.log(`[processTakeRequests] Found ${takeRequests.length} take requests`);
|
|
155
|
+
for (const takeRequest of takeRequests) {
|
|
156
|
+
if (abortSignal.aborted) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
await processSingleTakeRequest({
|
|
160
|
+
mpTakeRequestId: takeRequest.id,
|
|
161
|
+
mpTakeRequest: takeRequest,
|
|
162
|
+
casinoId,
|
|
163
|
+
graphqlClient,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
await processPendingTransferCompletions({
|
|
167
|
+
casinoId,
|
|
168
|
+
graphqlClient,
|
|
169
|
+
abortSignal,
|
|
170
|
+
});
|
|
171
|
+
await processStuckRequests({ casinoId, graphqlClient, abortSignal });
|
|
172
|
+
}
|
|
173
|
+
async function fetchPendingTakeRequests(graphqlClient, controllerId) {
|
|
174
|
+
const result = await graphqlClient.request(MP_PAGINATE_PENDING_TAKE_REQUESTS, {
|
|
175
|
+
controllerId,
|
|
176
|
+
});
|
|
177
|
+
return (result.allTakeRequests?.edges.flatMap((edge) => edge?.node || []) || []);
|
|
178
|
+
}
|
|
179
|
+
async function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest, casinoId, graphqlClient, }) {
|
|
180
|
+
return withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
181
|
+
await pgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
|
|
182
|
+
mpTakeRequestId,
|
|
183
|
+
casinoId,
|
|
184
|
+
});
|
|
185
|
+
const existingRequest = await pgClient
|
|
186
|
+
.query({
|
|
187
|
+
text: `
|
|
188
|
+
SELECT id, mp_take_request_id, status, reserved_amount, user_id, experience_id, currency_key
|
|
189
|
+
FROM hub.take_request
|
|
190
|
+
WHERE mp_take_request_id = $1 AND casino_id = $2
|
|
191
|
+
FOR UPDATE
|
|
192
|
+
`,
|
|
193
|
+
values: [mpTakeRequestId, casinoId],
|
|
194
|
+
})
|
|
195
|
+
.then(maybeOneRow);
|
|
196
|
+
if (!existingRequest && mpTakeRequest) {
|
|
197
|
+
return await createAndProcessNewTakeRequest({
|
|
198
|
+
pgClient,
|
|
199
|
+
mpTakeRequest,
|
|
200
|
+
casinoId,
|
|
201
|
+
graphqlClient,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (existingRequest) {
|
|
205
|
+
return await processExistingTakeRequest({
|
|
206
|
+
pgClient,
|
|
207
|
+
takeRequest: existingRequest,
|
|
208
|
+
casinoId,
|
|
209
|
+
graphqlClient,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
console.log(`[processSingleTakeRequest] Take request ${mpTakeRequestId} not found in MP or our DB for casino ${casinoId}`);
|
|
213
|
+
return null;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async function createAndProcessNewTakeRequest({ pgClient, mpTakeRequest, casinoId, graphqlClient, }) {
|
|
217
|
+
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
218
|
+
assert(mpTakeRequest.status === MpTakeRequestStatus.Pending);
|
|
219
|
+
const { dbUser, dbExperience, dbCurrency, dbBalance } = await loadRequiredEntities(pgClient, {
|
|
220
|
+
type: "mpId",
|
|
221
|
+
mpUserId: mpTakeRequest.userId,
|
|
222
|
+
mpExperienceId: mpTakeRequest.experienceId,
|
|
223
|
+
currencyKey: mpTakeRequest.currencyKey,
|
|
224
|
+
casinoId,
|
|
225
|
+
});
|
|
226
|
+
if (!dbUser || !dbExperience || !dbCurrency || !dbBalance) {
|
|
227
|
+
await rejectMpTakeRequest(pgClient, graphqlClient, mpTakeRequest.id);
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const amountToTransfer = Math.floor(typeof mpTakeRequest.amount === "number"
|
|
231
|
+
? Math.min(mpTakeRequest.amount, dbBalance.amount)
|
|
232
|
+
: dbBalance.amount);
|
|
233
|
+
if (amountToTransfer < 1) {
|
|
234
|
+
await rejectMpTakeRequest(pgClient, graphqlClient, mpTakeRequest.id);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const newTakeRequest = await pgClient
|
|
238
|
+
.query({
|
|
239
|
+
text: `
|
|
240
|
+
INSERT INTO hub.take_request (
|
|
241
|
+
mp_take_request_id,
|
|
242
|
+
user_id,
|
|
243
|
+
experience_id,
|
|
244
|
+
casino_id,
|
|
245
|
+
currency_key,
|
|
246
|
+
amount,
|
|
247
|
+
status,
|
|
248
|
+
mp_status,
|
|
249
|
+
reserved_amount
|
|
250
|
+
)
|
|
251
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
252
|
+
RETURNING id
|
|
253
|
+
`,
|
|
254
|
+
values: [
|
|
255
|
+
mpTakeRequest.id,
|
|
256
|
+
dbUser.id,
|
|
257
|
+
dbExperience.id,
|
|
258
|
+
casinoId,
|
|
259
|
+
dbCurrency.key,
|
|
260
|
+
mpTakeRequest.amount,
|
|
261
|
+
LocalTakeRequestStatus.PENDING,
|
|
262
|
+
mpTakeRequest.status,
|
|
263
|
+
amountToTransfer,
|
|
264
|
+
],
|
|
265
|
+
})
|
|
266
|
+
.then(exactlyOneRow);
|
|
267
|
+
await pgClient.query({
|
|
268
|
+
text: `
|
|
269
|
+
UPDATE hub.balance
|
|
270
|
+
SET amount = amount - $1
|
|
271
|
+
WHERE id = $2
|
|
272
|
+
`,
|
|
273
|
+
values: [amountToTransfer, dbBalance.id],
|
|
274
|
+
});
|
|
275
|
+
await pgClient.query({
|
|
276
|
+
text: `
|
|
277
|
+
UPDATE hub.take_request
|
|
278
|
+
SET status = $1, updated_at = now()
|
|
279
|
+
WHERE id = $2
|
|
280
|
+
`,
|
|
281
|
+
values: [LocalTakeRequestStatus.PROCESSING, newTakeRequest.id],
|
|
282
|
+
});
|
|
283
|
+
return await attemptTransfer({
|
|
284
|
+
pgClient,
|
|
285
|
+
takeRequestId: newTakeRequest.id,
|
|
286
|
+
mpTakeRequestId: mpTakeRequest.id,
|
|
287
|
+
mpExperienceId: dbExperience.mp_experience_id,
|
|
288
|
+
mpUserId: dbUser.mp_user_id,
|
|
289
|
+
amount: amountToTransfer,
|
|
290
|
+
currencyKey: dbCurrency.key,
|
|
291
|
+
graphqlClient,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async function processExistingTakeRequest({ pgClient, takeRequest, casinoId, graphqlClient, }) {
|
|
295
|
+
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
296
|
+
switch (takeRequest.status) {
|
|
297
|
+
case LocalTakeRequestStatus.PENDING:
|
|
298
|
+
console.log(`[processExistingTakeRequest] Take request ${takeRequest.id} in PENDING state`);
|
|
299
|
+
break;
|
|
300
|
+
case LocalTakeRequestStatus.PROCESSING: {
|
|
301
|
+
const { dbUser, dbExperience, dbCurrency } = await loadRequiredEntities(pgClient, {
|
|
302
|
+
type: "localId",
|
|
303
|
+
userId: takeRequest.user_id,
|
|
304
|
+
experienceId: takeRequest.experience_id,
|
|
305
|
+
currencyKey: takeRequest.currency_key,
|
|
306
|
+
casinoId,
|
|
307
|
+
});
|
|
308
|
+
if (!dbUser || !dbExperience || !dbCurrency) {
|
|
309
|
+
await updateTakeRequestStatus(pgClient, takeRequest.id, LocalTakeRequestStatus.FAILED);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
return await attemptTransfer({
|
|
313
|
+
pgClient,
|
|
314
|
+
takeRequestId: takeRequest.id,
|
|
315
|
+
mpTakeRequestId: takeRequest.mp_take_request_id,
|
|
316
|
+
mpExperienceId: dbExperience.mp_experience_id,
|
|
317
|
+
mpUserId: dbUser.mp_user_id,
|
|
318
|
+
amount: takeRequest.reserved_amount,
|
|
319
|
+
currencyKey: takeRequest.currency_key,
|
|
320
|
+
graphqlClient,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
case LocalTakeRequestStatus.COMPLETED:
|
|
324
|
+
console.log(`[processExistingTakeRequest] Take request ${takeRequest.id} already COMPLETED`);
|
|
325
|
+
break;
|
|
326
|
+
case LocalTakeRequestStatus.FAILED:
|
|
327
|
+
console.log(`[processExistingTakeRequest] Take request ${takeRequest.id} in FAILED state`);
|
|
328
|
+
break;
|
|
329
|
+
case LocalTakeRequestStatus.REJECTED:
|
|
330
|
+
console.log(`[processExistingTakeRequest] Take request ${takeRequest.id} already REJECTED`);
|
|
331
|
+
break;
|
|
332
|
+
default:
|
|
333
|
+
console.error(`[processExistingTakeRequest] Unknown status: ${takeRequest.status}`);
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
async function attemptTransfer({ pgClient, takeRequestId, mpTakeRequestId, mpExperienceId, mpUserId, amount, currencyKey, graphqlClient, }) {
|
|
339
|
+
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
340
|
+
try {
|
|
341
|
+
const transferResult = await graphqlClient.request(MP_TRANSFER_TAKE_REQUEST, {
|
|
342
|
+
mpTakeRequestId,
|
|
343
|
+
mpExperienceId,
|
|
344
|
+
mpUserId,
|
|
345
|
+
amount,
|
|
346
|
+
currencyKey,
|
|
347
|
+
});
|
|
348
|
+
if (!transferResult.transferCurrencyExperienceToUser?.result) {
|
|
349
|
+
throw new Error("No transfer result");
|
|
350
|
+
}
|
|
351
|
+
const result = transferResult.transferCurrencyExperienceToUser.result;
|
|
352
|
+
let transferId = null;
|
|
353
|
+
let resultStatus = LocalTakeRequestStatus.PROCESSING;
|
|
354
|
+
switch (result.__typename) {
|
|
355
|
+
case "TransferCurrencyExperienceToUserSuccess":
|
|
356
|
+
transferId = result.transfer.id;
|
|
357
|
+
break;
|
|
358
|
+
case "TransferMetadataIdExists":
|
|
359
|
+
transferId = result.transfer.id;
|
|
360
|
+
break;
|
|
361
|
+
case "TakeRequestAlreadyTerminal": {
|
|
362
|
+
const mpStatus = result.takeRequest.status;
|
|
363
|
+
if (result.takeRequest.experienceTransfer) {
|
|
364
|
+
transferId = result.takeRequest.experienceTransfer.id;
|
|
365
|
+
if (mpStatus === MpTakeRequestStatus.UserCanceled) {
|
|
366
|
+
resultStatus = LocalTakeRequestStatus.FAILED;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
resultStatus = LocalTakeRequestStatus.FAILED;
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
default:
|
|
375
|
+
throw new Error(`Unexpected result type: ${result.__typename}`);
|
|
376
|
+
}
|
|
377
|
+
await pgClient.query({
|
|
378
|
+
text: `
|
|
379
|
+
UPDATE hub.take_request
|
|
380
|
+
SET status = $1,
|
|
381
|
+
mp_transfer_id = $2,
|
|
382
|
+
mp_transfer_status = $3,
|
|
383
|
+
transfer_needs_completion = $4,
|
|
384
|
+
mp_status = $5
|
|
385
|
+
WHERE id = $6
|
|
386
|
+
`,
|
|
387
|
+
values: [
|
|
388
|
+
resultStatus,
|
|
389
|
+
transferId,
|
|
390
|
+
MpTransferStatus.Pending,
|
|
391
|
+
true,
|
|
392
|
+
result.__typename === "TakeRequestAlreadyTerminal"
|
|
393
|
+
? result.takeRequest.status
|
|
394
|
+
: MpTakeRequestStatus.Pending,
|
|
395
|
+
takeRequestId,
|
|
396
|
+
],
|
|
397
|
+
});
|
|
398
|
+
return transferId;
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
console.error(`[attemptTransfer] Error: ${error}`);
|
|
402
|
+
await pgClient.query({
|
|
403
|
+
text: `
|
|
404
|
+
UPDATE hub.take_request
|
|
405
|
+
SET status = $1
|
|
406
|
+
WHERE id = $2
|
|
407
|
+
`,
|
|
408
|
+
values: [LocalTakeRequestStatus.FAILED, takeRequestId],
|
|
409
|
+
});
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function processPendingTransferCompletions({ casinoId, graphqlClient, abortSignal, }) {
|
|
414
|
+
if (abortSignal.aborted) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const pendingCompletions = await superuserPool
|
|
418
|
+
.query({
|
|
419
|
+
text: `
|
|
420
|
+
SELECT id, mp_take_request_id, mp_transfer_id, transfer_completion_attempted_at
|
|
421
|
+
FROM hub.take_request
|
|
422
|
+
WHERE casino_id = $1
|
|
423
|
+
AND mp_transfer_id IS NOT NULL
|
|
424
|
+
AND transfer_needs_completion = TRUE
|
|
425
|
+
AND (
|
|
426
|
+
transfer_completion_attempted_at IS NULL OR
|
|
427
|
+
transfer_completion_attempted_at < now() - CASE
|
|
428
|
+
-- Exponential backoff: 10s, 1m, 5m, 15m, 30m, 1h, 3h
|
|
429
|
+
WHEN transfer_completion_attempted_at IS NULL THEN interval '0 seconds'
|
|
430
|
+
WHEN transfer_completion_attempted_at > now() - interval '10 minutes' THEN interval '10 seconds'
|
|
431
|
+
WHEN transfer_completion_attempted_at > now() - interval '30 minutes' THEN interval '1 minute'
|
|
432
|
+
WHEN transfer_completion_attempted_at > now() - interval '1 hour' THEN interval '5 minutes'
|
|
433
|
+
WHEN transfer_completion_attempted_at > now() - interval '3 hours' THEN interval '15 minutes'
|
|
434
|
+
WHEN transfer_completion_attempted_at > now() - interval '6 hours' THEN interval '30 minutes'
|
|
435
|
+
WHEN transfer_completion_attempted_at > now() - interval '24 hours' THEN interval '1 hour'
|
|
436
|
+
ELSE interval '3 hours'
|
|
437
|
+
END
|
|
438
|
+
)
|
|
439
|
+
LIMIT 10
|
|
440
|
+
`,
|
|
441
|
+
values: [casinoId],
|
|
442
|
+
})
|
|
443
|
+
.then((result) => result.rows);
|
|
444
|
+
console.log(`[processPendingTransferCompletions] Found ${pendingCompletions.length} transfers needing completion`);
|
|
445
|
+
for (const request of pendingCompletions) {
|
|
446
|
+
if (abortSignal.aborted) {
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
await completeTransfer({
|
|
450
|
+
mpTakeRequestId: request.mp_take_request_id,
|
|
451
|
+
takeRequestId: request.id,
|
|
452
|
+
mpTransferId: request.mp_transfer_id,
|
|
453
|
+
graphqlClient,
|
|
454
|
+
casinoId,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTransferId, graphqlClient, casinoId, }) {
|
|
459
|
+
return withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
460
|
+
await pgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
|
|
461
|
+
mpTakeRequestId,
|
|
462
|
+
casinoId,
|
|
463
|
+
});
|
|
464
|
+
await pgClient.query({
|
|
465
|
+
text: `
|
|
466
|
+
UPDATE hub.take_request
|
|
467
|
+
SET transfer_completion_attempted_at = now()
|
|
468
|
+
WHERE id = $1
|
|
469
|
+
`,
|
|
470
|
+
values: [takeRequestId],
|
|
471
|
+
});
|
|
472
|
+
try {
|
|
473
|
+
const result = await graphqlClient.request(MP_COMPLETE_TRANSFER, {
|
|
474
|
+
mpTransferId,
|
|
475
|
+
});
|
|
476
|
+
const completionResult = result.completeTransfer?.result;
|
|
477
|
+
if (!completionResult) {
|
|
478
|
+
throw new Error("No completion result returned from MP API");
|
|
479
|
+
}
|
|
480
|
+
switch (completionResult.__typename) {
|
|
481
|
+
case "CompleteTransferSuccess":
|
|
482
|
+
await pgClient.query({
|
|
483
|
+
text: `
|
|
484
|
+
UPDATE hub.take_request
|
|
485
|
+
SET
|
|
486
|
+
transfer_needs_completion = FALSE,
|
|
487
|
+
mp_transfer_status = $2,
|
|
488
|
+
status = $3,
|
|
489
|
+
mp_status = $4
|
|
490
|
+
WHERE id = $1
|
|
491
|
+
`,
|
|
492
|
+
values: [
|
|
493
|
+
takeRequestId,
|
|
494
|
+
completionResult.transfer.status,
|
|
495
|
+
LocalTakeRequestStatus.COMPLETED,
|
|
496
|
+
MpTakeRequestStatus.Transferred,
|
|
497
|
+
],
|
|
498
|
+
});
|
|
499
|
+
console.log(`[completeTransfer] Successfully completed transfer ${mpTransferId}`);
|
|
500
|
+
break;
|
|
501
|
+
case "InvalidTransferStatus": {
|
|
502
|
+
const currentStatus = completionResult.currentStatus;
|
|
503
|
+
if (currentStatus === "COMPLETED") {
|
|
504
|
+
await pgClient.query({
|
|
505
|
+
text: `
|
|
506
|
+
UPDATE hub.take_request
|
|
507
|
+
SET
|
|
508
|
+
transfer_needs_completion = FALSE,
|
|
509
|
+
mp_transfer_status = $2,
|
|
510
|
+
status = $3,
|
|
511
|
+
mp_status = $4
|
|
512
|
+
WHERE id = $1
|
|
513
|
+
`,
|
|
514
|
+
values: [
|
|
515
|
+
takeRequestId,
|
|
516
|
+
currentStatus,
|
|
517
|
+
LocalTakeRequestStatus.COMPLETED,
|
|
518
|
+
MpTakeRequestStatus.Transferred,
|
|
519
|
+
],
|
|
520
|
+
});
|
|
521
|
+
console.log(`[completeTransfer] Transfer ${mpTransferId} was already completed`);
|
|
522
|
+
}
|
|
523
|
+
else if (currentStatus === "CANCELED" ||
|
|
524
|
+
currentStatus === "EXPIRED") {
|
|
525
|
+
const mpStatus = completionResult.transfer.__typename === "ExperienceTransfer"
|
|
526
|
+
? completionResult.transfer.takeRequest?.status
|
|
527
|
+
: null;
|
|
528
|
+
if (!mpStatus) {
|
|
529
|
+
throw new Error("No MP status returned from MP API");
|
|
530
|
+
}
|
|
531
|
+
await pgClient.query({
|
|
532
|
+
text: `
|
|
533
|
+
UPDATE hub.take_request
|
|
534
|
+
SET
|
|
535
|
+
transfer_needs_completion = FALSE,
|
|
536
|
+
mp_transfer_status = $2,
|
|
537
|
+
status = $3,
|
|
538
|
+
mp_status = $4
|
|
539
|
+
WHERE id = $1
|
|
540
|
+
`,
|
|
541
|
+
values: [
|
|
542
|
+
takeRequestId,
|
|
543
|
+
currentStatus,
|
|
544
|
+
LocalTakeRequestStatus.FAILED,
|
|
545
|
+
mpStatus,
|
|
546
|
+
],
|
|
547
|
+
});
|
|
548
|
+
console.log(`[completeTransfer] Transfer ${mpTransferId} has status ${currentStatus}`);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
console.log(`[completeTransfer] Transfer ${mpTransferId} has status ${currentStatus}, will retry later`);
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
default: {
|
|
556
|
+
const exhaustiveCheck = completionResult;
|
|
557
|
+
throw new Error(`Unknown completion result type: ${exhaustiveCheck}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
console.error(`[completeTransfer] Error completing transfer ${mpTransferId}:`, error);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
async function processStuckRequests({ casinoId, graphqlClient, abortSignal, }) {
|
|
567
|
+
if (abortSignal.aborted) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const stuckRequests = await superuserPool
|
|
571
|
+
.query({
|
|
572
|
+
text: `
|
|
573
|
+
SELECT id, mp_take_request_id
|
|
574
|
+
FROM hub.take_request
|
|
575
|
+
WHERE casino_id = $1
|
|
576
|
+
AND status = $2
|
|
577
|
+
AND mp_transfer_id IS NULL
|
|
578
|
+
AND updated_at < now() - interval '5 minutes'
|
|
579
|
+
LIMIT 10
|
|
580
|
+
`,
|
|
581
|
+
values: [casinoId, LocalTakeRequestStatus.PROCESSING],
|
|
582
|
+
})
|
|
583
|
+
.then((result) => result.rows);
|
|
584
|
+
console.log(`[processStuckRequests] Found ${stuckRequests.length} stuck take requests`);
|
|
585
|
+
for (const request of stuckRequests) {
|
|
586
|
+
if (abortSignal.aborted) {
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
await processSingleTakeRequest({
|
|
590
|
+
mpTakeRequestId: request.mp_take_request_id,
|
|
591
|
+
casinoId,
|
|
592
|
+
graphqlClient,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function loadRequiredEntities(pgClient, params) {
|
|
597
|
+
const { type, currencyKey, casinoId } = params;
|
|
598
|
+
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
599
|
+
const dbCurrency = await pgClient
|
|
600
|
+
.query({
|
|
601
|
+
text: `
|
|
602
|
+
SELECT key
|
|
603
|
+
FROM hub.currency
|
|
604
|
+
WHERE key = $1 AND casino_id = $2
|
|
605
|
+
`,
|
|
606
|
+
values: [currencyKey, casinoId],
|
|
607
|
+
})
|
|
608
|
+
.then(maybeOneRow);
|
|
609
|
+
if (!dbCurrency) {
|
|
610
|
+
console.warn(`[loadRequiredEntities] Currency ${currencyKey} not found`);
|
|
611
|
+
return {
|
|
612
|
+
dbCurrency: null,
|
|
613
|
+
dbUser: null,
|
|
614
|
+
dbExperience: null,
|
|
615
|
+
dbBalance: null,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
let dbUser;
|
|
619
|
+
if (type === "localId") {
|
|
620
|
+
dbUser = await pgClient
|
|
621
|
+
.query({
|
|
622
|
+
text: `
|
|
623
|
+
SELECT *
|
|
624
|
+
FROM hub.user
|
|
625
|
+
WHERE id = $1 AND casino_id = $2
|
|
626
|
+
`,
|
|
627
|
+
values: [params.userId, casinoId],
|
|
628
|
+
})
|
|
629
|
+
.then(maybeOneRow);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
dbUser = await pgClient
|
|
633
|
+
.query({
|
|
634
|
+
text: `
|
|
635
|
+
SELECT *
|
|
636
|
+
FROM hub.user
|
|
637
|
+
WHERE mp_user_id = $1 AND casino_id = $2
|
|
638
|
+
`,
|
|
639
|
+
values: [params.mpUserId, casinoId],
|
|
640
|
+
})
|
|
641
|
+
.then(maybeOneRow);
|
|
642
|
+
}
|
|
643
|
+
if (!dbUser) {
|
|
644
|
+
console.warn(`[loadRequiredEntities] User not found`);
|
|
645
|
+
return { dbCurrency, dbUser: null, dbExperience: null, dbBalance: null };
|
|
646
|
+
}
|
|
647
|
+
let dbExperience;
|
|
648
|
+
if (type === "localId") {
|
|
649
|
+
dbExperience = await pgClient
|
|
650
|
+
.query({
|
|
651
|
+
text: `
|
|
652
|
+
SELECT *
|
|
653
|
+
FROM hub.experience
|
|
654
|
+
WHERE id = $1 AND casino_id = $2
|
|
655
|
+
`,
|
|
656
|
+
values: [params.experienceId, casinoId],
|
|
657
|
+
})
|
|
658
|
+
.then(maybeOneRow);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
dbExperience = await pgClient
|
|
662
|
+
.query({
|
|
663
|
+
text: `
|
|
664
|
+
SELECT *
|
|
665
|
+
FROM hub.experience
|
|
666
|
+
WHERE mp_experience_id = $1 AND casino_id = $2
|
|
667
|
+
`,
|
|
668
|
+
values: [params.mpExperienceId, casinoId],
|
|
669
|
+
})
|
|
670
|
+
.then(maybeOneRow);
|
|
671
|
+
}
|
|
672
|
+
if (!dbExperience) {
|
|
673
|
+
console.warn(`[loadRequiredEntities] Experience not found`);
|
|
674
|
+
return { dbCurrency, dbUser, dbExperience: null, dbBalance: null };
|
|
675
|
+
}
|
|
676
|
+
let dbBalance = null;
|
|
677
|
+
if (dbUser && dbExperience) {
|
|
678
|
+
dbBalance = await pgClient
|
|
679
|
+
.query({
|
|
680
|
+
text: `
|
|
681
|
+
SELECT id, amount
|
|
682
|
+
FROM hub.balance
|
|
683
|
+
WHERE user_id = $1
|
|
684
|
+
AND experience_id = $2
|
|
685
|
+
AND casino_id = $3
|
|
686
|
+
AND currency_key = $4
|
|
687
|
+
FOR UPDATE
|
|
688
|
+
`,
|
|
689
|
+
values: [dbUser.id, dbExperience.id, casinoId, dbCurrency.key],
|
|
690
|
+
})
|
|
691
|
+
.then(maybeOneRow)
|
|
692
|
+
.then((row) => row ?? null);
|
|
693
|
+
}
|
|
694
|
+
return { dbCurrency, dbUser, dbExperience, dbBalance };
|
|
695
|
+
}
|
|
696
|
+
async function rejectMpTakeRequest(pgClient, graphqlClient, mpTakeRequestId, takeRequestId) {
|
|
697
|
+
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
698
|
+
try {
|
|
699
|
+
console.log(`[rejectMpTakeRequest] Rejecting take request ${mpTakeRequestId}`);
|
|
700
|
+
const rejectResult = await graphqlClient.request(MP_REJECT_TAKE_REQUEST, {
|
|
701
|
+
mpTakeRequestId,
|
|
702
|
+
});
|
|
703
|
+
let success = false;
|
|
704
|
+
let updatedMpStatus = null;
|
|
705
|
+
switch (rejectResult.rejectTakeRequest.result.__typename) {
|
|
706
|
+
case "RejectTakeRequestSuccess":
|
|
707
|
+
updatedMpStatus = MpTakeRequestStatus.ControllerRejected;
|
|
708
|
+
success = true;
|
|
709
|
+
break;
|
|
710
|
+
case "TakeRequestAlreadyTerminal":
|
|
711
|
+
if (rejectResult.rejectTakeRequest.result.takeRequest?.status) {
|
|
712
|
+
updatedMpStatus = rejectResult.rejectTakeRequest.result.takeRequest
|
|
713
|
+
.status;
|
|
714
|
+
}
|
|
715
|
+
success = true;
|
|
716
|
+
break;
|
|
717
|
+
default: {
|
|
718
|
+
const exhaustiveCheck = rejectResult.rejectTakeRequest.result;
|
|
719
|
+
throw new Error(`Unexpected result type: ${exhaustiveCheck}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (updatedMpStatus) {
|
|
723
|
+
if (takeRequestId) {
|
|
724
|
+
await pgClient.query({
|
|
725
|
+
text: `
|
|
726
|
+
UPDATE hub.take_request
|
|
727
|
+
SET status = $1,
|
|
728
|
+
mp_status = $2
|
|
729
|
+
WHERE id = $3
|
|
730
|
+
`,
|
|
731
|
+
values: [
|
|
732
|
+
LocalTakeRequestStatus.REJECTED,
|
|
733
|
+
updatedMpStatus,
|
|
734
|
+
takeRequestId,
|
|
735
|
+
],
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
const existingRequest = await pgClient
|
|
740
|
+
.query({
|
|
741
|
+
text: `
|
|
742
|
+
SELECT id FROM hub.take_request
|
|
743
|
+
WHERE mp_take_request_id = $1
|
|
744
|
+
FOR UPDATE
|
|
745
|
+
`,
|
|
746
|
+
values: [mpTakeRequestId],
|
|
747
|
+
})
|
|
748
|
+
.then(maybeOneRow);
|
|
749
|
+
if (existingRequest) {
|
|
750
|
+
await pgClient.query({
|
|
751
|
+
text: `
|
|
752
|
+
UPDATE hub.take_request
|
|
753
|
+
SET status = $1,
|
|
754
|
+
mp_status = $2
|
|
755
|
+
WHERE id = $3
|
|
756
|
+
`,
|
|
757
|
+
values: [
|
|
758
|
+
LocalTakeRequestStatus.REJECTED,
|
|
759
|
+
updatedMpStatus,
|
|
760
|
+
existingRequest.id,
|
|
761
|
+
],
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return success;
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
console.error(`[rejectMpTakeRequest] Error: ${error}`);
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function updateTakeRequestStatus(pgClient, takeRequestId, status) {
|
|
774
|
+
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
775
|
+
return pgClient.query({
|
|
776
|
+
text: `
|
|
777
|
+
UPDATE hub.take_request
|
|
778
|
+
SET status = $1, updated_at = now()
|
|
779
|
+
WHERE id = $2
|
|
780
|
+
`,
|
|
781
|
+
values: [status, takeRequestId],
|
|
782
|
+
});
|
|
783
|
+
}
|