@moneypot/hub 1.19.3 → 1.19.5-dev.0

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/README.md CHANGED
@@ -80,6 +80,7 @@ You should always keep your hub server up to date as soon as possible. Never ins
80
80
  ### 1.19.x
81
81
 
82
82
  - All of hub's internal graphql-schema-modifying plugins have been moved to hub's set of required plugins.
83
+ - Added `FAILED` take request auto-handling
83
84
 
84
85
  ### 1.18.x
85
86
 
@@ -114,6 +114,8 @@ export type DbTakeRequest = {
114
114
  mp_transfer_status: DbTransferStatusKind | null;
115
115
  transfer_needs_completion: boolean;
116
116
  transfer_completion_attempted_at: Date | null;
117
+ transfer_failure_count: number;
118
+ transfer_first_failure_at: Date | null;
117
119
  refunded_at: Date | null;
118
120
  updated_at: Date;
119
121
  };
@@ -1,6 +1,6 @@
1
- import { Express } from "express";
1
+ import { type ConfigureAppArgs } from "./server/index.js";
2
2
  import { type PluginIdentity } from "./server/graphile.config.js";
3
- import { ServerContext } from "./context.js";
3
+ import { type ServerContext } from "./context.js";
4
4
  declare global {
5
5
  namespace Grafast {
6
6
  interface Context {
@@ -22,10 +22,9 @@ export { type CustomGameConfig, type CustomGameConfigMap, } from "./custom-game-
22
22
  export { validateRisk, type RiskPolicy, type RiskPolicyArgs, type RiskLimits, } from "./risk-policy.js";
23
23
  export type PluginContext = Grafast.Context;
24
24
  export { defaultPlugins, type PluginIdentity, type UserSessionContext, type HubPlugin, } from "./server/graphile.config.js";
25
- export type ConfigureAppArgs = {
26
- app: Express;
27
- superuserPool: ServerContext["superuserPool"];
28
- };
25
+ export type { ConfigureAppArgs } from "./server/index.js";
26
+ import { runMigrations } from "./migrations.js";
27
+ export { runMigrations };
29
28
  import type { HubPlugin } from "./server/graphile.config.js";
30
29
  export type ServerOptions = {
31
30
  configureApp?: (args: ConfigureAppArgs) => void;
@@ -37,9 +36,6 @@ export type ServerOptions = {
37
36
  enablePlayground?: boolean;
38
37
  port?: number;
39
38
  };
40
- export declare function runMigrations(options: {
41
- userDatabaseMigrationsPath?: string;
42
- }): Promise<void>;
43
39
  export declare function startAndListen(options: ServerOptions): Promise<{
44
40
  port: number;
45
41
  stop: () => Promise<void>;
package/dist/src/index.js CHANGED
@@ -1,77 +1,12 @@
1
- import PgUpgradeSchema, { DatabaseAheadError, } from "@moneypot/pg-upgrade-schema";
2
- import * as db from "./db/index.js";
3
1
  import * as config from "./config.js";
4
- import { createHubServer } from "./server/index.js";
5
- import { initializeTransferProcessors } from "./process-transfers/index.js";
6
- import { join } from "path";
7
2
  import { logger } from "./logger.js";
8
- import { createServerContext, closeServerContext, } from "./context.js";
3
+ import { createHubServer } from "./server/index.js";
9
4
  export { HubGameConfigPlugin, } from "./plugins/hub-game-config-plugin.js";
10
5
  export { validateRisk, } from "./risk-policy.js";
11
6
  export { defaultPlugins, } from "./server/graphile.config.js";
12
- export async function runMigrations(options) {
13
- if (options.userDatabaseMigrationsPath) {
14
- const { existsSync, statSync } = await import("fs");
15
- if (!existsSync(options.userDatabaseMigrationsPath)) {
16
- throw new Error(`userDatabaseMigrationsPath does not exist: ${options.userDatabaseMigrationsPath}`);
17
- }
18
- const stats = statSync(options.userDatabaseMigrationsPath);
19
- if (!stats.isDirectory()) {
20
- throw new Error(`userDatabaseMigrationsPath is not a directory: ${options.userDatabaseMigrationsPath}`);
21
- }
22
- }
23
- const pgClient = db.getPgClient(config.SUPERUSER_DATABASE_URL);
24
- await pgClient.connect();
25
- try {
26
- try {
27
- await PgUpgradeSchema.default({
28
- pgClient,
29
- dirname: join(import.meta.dirname, "pg-versions"),
30
- schemaName: "hub_core_versions",
31
- silent: process.env.NODE_ENV === "test",
32
- });
33
- }
34
- catch (e) {
35
- logger.error(e, "Error upgrading core schema");
36
- if (e instanceof DatabaseAheadError) {
37
- logger.error(`${"⚠️".repeat(10)}\n@moneypot/hub database was reset to prepare for a production release and you must reset your database to continue. Please see <https://www.npmjs.com/package/@moneypot/hub#change-log> for more info.`);
38
- process.exit(1);
39
- }
40
- throw e;
41
- }
42
- if (options.userDatabaseMigrationsPath) {
43
- await PgUpgradeSchema.default({
44
- pgClient,
45
- dirname: options.userDatabaseMigrationsPath,
46
- schemaName: "hub_user_versions",
47
- silent: process.env.NODE_ENV === "test",
48
- });
49
- }
50
- }
51
- finally {
52
- await pgClient.end();
53
- }
54
- }
55
- async function initialize(options) {
56
- if (options.signal.aborted) {
57
- logger.info("Initialization aborted by graceful shutdown");
58
- return;
59
- }
60
- await runMigrations({
61
- userDatabaseMigrationsPath: options.userDatabaseMigrationsPath,
62
- });
63
- if (options.signal.aborted) {
64
- logger.info("Initialization aborted by graceful shutdown");
65
- return;
66
- }
67
- if (process.env.NODE_ENV !== "test") {
68
- initializeTransferProcessors({
69
- signal: options.signal,
70
- pool: options.context.superuserPool,
71
- });
72
- }
73
- }
74
- export async function startAndListen(options) {
7
+ import { runMigrations } from "./migrations.js";
8
+ export { runMigrations };
9
+ function validateOptions(options) {
75
10
  if (options.plugins && options.plugins.some((p) => typeof p === "function")) {
76
11
  throw new Error("`plugins` should be an array of HubPlugin but one of the items is a function. Did you forget to call it?");
77
12
  }
@@ -83,60 +18,38 @@ export async function startAndListen(options) {
83
18
  !options.exportSchemaSDLPath.startsWith("/")) {
84
19
  throw new Error(`exportSchemaSDLPath must be an absolute path, got ${options.exportSchemaSDLPath}`);
85
20
  }
86
- const abortController = new AbortController();
87
- let isShuttingDown = false;
88
- const context = createServerContext();
89
- await initialize({
21
+ }
22
+ export async function startAndListen(options) {
23
+ validateOptions(options);
24
+ await runMigrations({
90
25
  userDatabaseMigrationsPath: options.userDatabaseMigrationsPath,
91
- signal: abortController.signal,
92
- context,
93
26
  });
94
27
  const hubServer = createHubServer({
95
28
  configureApp: options.configureApp,
96
29
  plugins: options.plugins,
97
30
  exportSchemaSDLPath: options.exportSchemaSDLPath,
98
31
  extraPgSchemas: options.extraPgSchemas,
99
- abortSignal: abortController.signal,
100
- context,
101
32
  enableChat: options.enableChat ?? true,
102
33
  enablePlayground: options.enablePlayground ?? true,
103
34
  port: options.port ?? config.PORT,
35
+ runProcessors: process.env.NODE_ENV !== "test",
104
36
  });
105
- const gracefulShutdown = async ({ exit = true } = {}) => {
106
- if (isShuttingDown) {
107
- logger.warn("Already shutting down.");
108
- return;
109
- }
110
- isShuttingDown = true;
111
- logger.info("Shutting down gracefully...");
112
- abortController.abort();
113
- await new Promise((resolve) => setTimeout(resolve, 500));
114
- logger.info("Closing resources...");
115
- await hubServer.shutdown();
116
- try {
117
- await Promise.race([
118
- closeServerContext(context),
119
- new Promise((_, reject) => setTimeout(() => reject(new Error("Database cleanup timeout")), 3000)),
120
- ]);
121
- logger.info("Cleanup complete.");
122
- }
123
- catch (err) {
124
- logger.warn(err, "Cleanup error (proceeding anyway)");
125
- }
126
- if (exit) {
127
- process.exit(0);
128
- }
129
- };
130
37
  if (process.env.NODE_ENV !== "test") {
131
- process.on("SIGINT", gracefulShutdown);
132
- process.on("SIGTERM", gracefulShutdown);
133
- }
134
- return hubServer.listen().then(({ port }) => {
135
- return {
136
- port,
137
- stop: async () => {
138
- await gracefulShutdown({ exit: false });
139
- },
38
+ const handler = () => {
39
+ hubServer
40
+ .shutdown()
41
+ .then(() => process.exit(0))
42
+ .catch((err) => {
43
+ logger.error(err, "Shutdown error");
44
+ process.exit(1);
45
+ });
140
46
  };
141
- });
47
+ process.on("SIGINT", handler);
48
+ process.on("SIGTERM", handler);
49
+ }
50
+ const { port } = await hubServer.listen();
51
+ return {
52
+ port,
53
+ stop: () => hubServer.shutdown(),
54
+ };
142
55
  }
@@ -0,0 +1,3 @@
1
+ export declare function runMigrations(options: {
2
+ userDatabaseMigrationsPath?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,48 @@
1
+ import PgUpgradeSchema, { DatabaseAheadError, } from "@moneypot/pg-upgrade-schema";
2
+ import * as db from "./db/index.js";
3
+ import * as config from "./config.js";
4
+ import { join } from "path";
5
+ import { logger } from "./logger.js";
6
+ export async function runMigrations(options) {
7
+ if (options.userDatabaseMigrationsPath) {
8
+ const { existsSync, statSync } = await import("fs");
9
+ if (!existsSync(options.userDatabaseMigrationsPath)) {
10
+ throw new Error(`userDatabaseMigrationsPath does not exist: ${options.userDatabaseMigrationsPath}`);
11
+ }
12
+ const stats = statSync(options.userDatabaseMigrationsPath);
13
+ if (!stats.isDirectory()) {
14
+ throw new Error(`userDatabaseMigrationsPath is not a directory: ${options.userDatabaseMigrationsPath}`);
15
+ }
16
+ }
17
+ const pgClient = db.getPgClient(config.SUPERUSER_DATABASE_URL);
18
+ await pgClient.connect();
19
+ try {
20
+ try {
21
+ await PgUpgradeSchema.default({
22
+ pgClient,
23
+ dirname: join(import.meta.dirname, "pg-versions"),
24
+ schemaName: "hub_core_versions",
25
+ silent: process.env.NODE_ENV === "test",
26
+ });
27
+ }
28
+ catch (e) {
29
+ logger.error(e, "Error upgrading core schema");
30
+ if (e instanceof DatabaseAheadError) {
31
+ logger.error(`${"⚠️".repeat(10)}\n@moneypot/hub database was reset to prepare for a production release and you must reset your database to continue. Please see <https://www.npmjs.com/package/@moneypot/hub#change-log> for more info.`);
32
+ process.exit(1);
33
+ }
34
+ throw e;
35
+ }
36
+ if (options.userDatabaseMigrationsPath) {
37
+ await PgUpgradeSchema.default({
38
+ pgClient,
39
+ dirname: options.userDatabaseMigrationsPath,
40
+ schemaName: "hub_user_versions",
41
+ silent: process.env.NODE_ENV === "test",
42
+ });
43
+ }
44
+ }
45
+ finally {
46
+ await pgClient.end();
47
+ }
48
+ }
@@ -0,0 +1,11 @@
1
+ ALTER TABLE hub.take_request
2
+ ADD COLUMN IF NOT EXISTS transfer_failure_count int DEFAULT 0,
3
+ ADD COLUMN IF NOT EXISTS transfer_first_failure_at timestamptz;
4
+
5
+ CREATE INDEX IF NOT EXISTS take_request_transfer_retry_idx
6
+ ON hub.take_request(casino_id, status, transfer_first_failure_at)
7
+ WHERE mp_transfer_id IS NULL AND status = 'PROCESSING';
8
+
9
+ CREATE INDEX IF NOT EXISTS take_request_failed_refund_idx
10
+ ON hub.take_request(casino_id, status, refunded_at)
11
+ WHERE mp_transfer_id IS NULL AND status = 'FAILED' AND refunded_at IS NULL;
@@ -1,4 +1,3 @@
1
- import { ServerOptions } from "../index.js";
2
1
  import { ServerContext } from "../context.js";
3
2
  export type HubServer = {
4
3
  listen: () => Promise<{
@@ -6,10 +5,20 @@ export type HubServer = {
6
5
  }>;
7
6
  shutdown: () => Promise<void>;
8
7
  };
9
- export declare function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, enableChat, enablePlayground, port, }: Pick<ServerOptions, "plugins" | "exportSchemaSDLPath" | "extraPgSchemas" | "configureApp"> & {
10
- abortSignal: AbortSignal;
11
- context: ServerContext;
8
+ import type { Express } from "express";
9
+ import type { HubPlugin } from "./graphile.config.js";
10
+ export type ConfigureAppArgs = {
11
+ app: Express;
12
+ superuserPool: ServerContext["superuserPool"];
13
+ };
14
+ export type CreateHubServerOptions = {
15
+ configureApp?: (args: ConfigureAppArgs) => void;
16
+ plugins?: readonly HubPlugin[];
17
+ extraPgSchemas?: string[];
18
+ exportSchemaSDLPath?: string;
12
19
  enableChat: boolean;
13
20
  enablePlayground: boolean;
14
21
  port: number;
15
- }): HubServer;
22
+ runProcessors: boolean;
23
+ };
24
+ export declare function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, enableChat, enablePlayground, port, runProcessors, }: CreateHubServerOptions): HubServer;
@@ -2,6 +2,8 @@ import { createServer as createNodeServer } from "node:http";
2
2
  import { grafserv } from "postgraphile/grafserv/express/v4";
3
3
  import postgraphile from "postgraphile";
4
4
  import { createPreset, defaultPlugins } from "./graphile.config.js";
5
+ import { createServerContext, closeServerContext, } from "../context.js";
6
+ import { initializeTransferProcessors } from "../process-transfers/index.js";
5
7
  import express from "express";
6
8
  import { logger } from "../logger.js";
7
9
  import cors from "./middleware/cors.js";
@@ -44,13 +46,15 @@ function createExpressServer(context) {
44
46
  });
45
47
  return app;
46
48
  }
47
- export function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, enableChat, enablePlayground, port, }) {
49
+ export function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, enableChat, enablePlayground, port, runProcessors, }) {
50
+ const abortController = new AbortController();
51
+ const context = createServerContext();
48
52
  const expressServer = createExpressServer(context);
49
53
  const { preset, pgService } = createPreset({
50
54
  plugins: plugins ?? defaultPlugins,
51
55
  exportSchemaSDLPath,
52
56
  extraPgSchemas: extraPgSchemas ?? [],
53
- abortSignal,
57
+ abortSignal: abortController.signal,
54
58
  context,
55
59
  enableChat,
56
60
  enablePlayground,
@@ -64,10 +68,16 @@ export function createHubServer({ configureApp, plugins, exportSchemaSDLPath, ex
64
68
  nodeServer.on("error", (e) => {
65
69
  logger.error(e);
66
70
  });
67
- const mountMiddlewarePromise = serv.addTo(expressServer, nodeServer);
71
+ let isShuttingDown = false;
68
72
  return {
69
73
  listen: async () => {
70
- await mountMiddlewarePromise;
74
+ if (runProcessors) {
75
+ initializeTransferProcessors({
76
+ signal: abortController.signal,
77
+ pool: context.superuserPool,
78
+ });
79
+ }
80
+ await serv.addTo(expressServer, nodeServer);
71
81
  await pgl.getSchema();
72
82
  return new Promise((resolve) => {
73
83
  nodeServer.listen(port, () => {
@@ -78,12 +88,23 @@ export function createHubServer({ configureApp, plugins, exportSchemaSDLPath, ex
78
88
  });
79
89
  },
80
90
  shutdown: async () => {
91
+ if (isShuttingDown) {
92
+ logger.warn("Already shutting down.");
93
+ return;
94
+ }
95
+ isShuttingDown = true;
96
+ logger.info("Shutting down gracefully...");
97
+ abortController.abort();
98
+ await new Promise((resolve) => setTimeout(resolve, 500));
99
+ logger.info("Closing resources...");
81
100
  nodeServer.closeAllConnections();
101
+ await closeServerContext(context);
82
102
  await pgService.release?.();
83
103
  await pgl.release();
84
104
  await new Promise((resolve) => {
85
105
  nodeServer.close(() => resolve());
86
106
  });
107
+ logger.info("Cleanup complete.");
87
108
  },
88
109
  };
89
110
  }
@@ -25,3 +25,8 @@ export declare function completeTransfer({ mpTakeRequestId, takeRequestId, mpTra
25
25
  casinoId: string;
26
26
  pool: pg.Pool;
27
27
  }): Promise<void>;
28
+ export declare function processFailedRequestRefunds({ casinoId, abortSignal, pool, }: {
29
+ casinoId: string;
30
+ abortSignal: AbortSignal;
31
+ pool: pg.Pool;
32
+ }): Promise<void>;
@@ -193,6 +193,7 @@ export async function processTakeRequests({ abortSignal, controllerId, casinoId,
193
193
  pool,
194
194
  });
195
195
  await processStuckRequests({ casinoId, graphqlClient, abortSignal, pool });
196
+ await processFailedRequestRefunds({ casinoId, abortSignal, pool });
196
197
  }
197
198
  async function fetchPendingTakeRequests(graphqlClient, controllerId) {
198
199
  const result = await graphqlClient.request(MP_PAGINATE_PENDING_TAKE_REQUESTS, {
@@ -382,6 +383,7 @@ async function attemptTransfer({ pgClient, takeRequestId, mpTakeRequestId, mpExp
382
383
  }
383
384
  const result = transferResult.transferCurrencyExperienceToUser.result;
384
385
  let transferId = null;
386
+ let needsImmediateRefund = false;
385
387
  let resultStatus = LocalTakeRequestStatus.PROCESSING;
386
388
  switch (result.__typename) {
387
389
  case "TransferSuccess":
@@ -400,6 +402,7 @@ async function attemptTransfer({ pgClient, takeRequestId, mpTakeRequestId, mpExp
400
402
  }
401
403
  else {
402
404
  resultStatus = LocalTakeRequestStatus.FAILED;
405
+ needsImmediateRefund = true;
403
406
  }
404
407
  break;
405
408
  }
@@ -409,8 +412,8 @@ async function attemptTransfer({ pgClient, takeRequestId, mpTakeRequestId, mpExp
409
412
  await pgClient.query({
410
413
  text: `
411
414
  UPDATE hub.take_request
412
- SET status = $1,
413
- mp_transfer_id = $2,
415
+ SET status = $1,
416
+ mp_transfer_id = $2,
414
417
  mp_transfer_status = $3,
415
418
  transfer_needs_completion = $4,
416
419
  mp_status = $5
@@ -427,18 +430,61 @@ async function attemptTransfer({ pgClient, takeRequestId, mpTakeRequestId, mpExp
427
430
  takeRequestId,
428
431
  ],
429
432
  });
433
+ if (needsImmediateRefund) {
434
+ logger.info({ takeRequestId }, `[attemptTransfer] Take request already terminal on MP, refunding`);
435
+ await refundTakeRequest(pgClient, takeRequestId);
436
+ }
430
437
  return transferId;
431
438
  }
432
439
  catch (error) {
433
- logger.error(error, `[attemptTransfer] Error`);
440
+ logger.error(error, `[attemptTransfer] Error creating transfer`);
441
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
442
+ const shouldRetry = error instanceof Error && isNetworkError(error);
434
443
  await pgClient.query({
435
444
  text: `
436
445
  UPDATE hub.take_request
437
- SET status = $1
438
- WHERE id = $2
446
+ SET transfer_failure_count = COALESCE(transfer_failure_count, 0) + 1,
447
+ transfer_first_failure_at = COALESCE(transfer_first_failure_at, now()),
448
+ debug = COALESCE(debug, '') || ' | Transfer attempt failed: ' || $2::text
449
+ WHERE id = $1
439
450
  `,
440
- values: [LocalTakeRequestStatus.FAILED, takeRequestId],
451
+ values: [takeRequestId, errorMessage.slice(0, 200)],
441
452
  });
453
+ if (!shouldRetry) {
454
+ logger.info({ takeRequestId }, `[attemptTransfer] Non-retryable error, marking as FAILED and refunding`);
455
+ await pgClient.query({
456
+ text: `
457
+ UPDATE hub.take_request
458
+ SET status = $1,
459
+ debug = COALESCE(debug, '') || ' | Non-retryable error, refunding'
460
+ WHERE id = $2
461
+ `,
462
+ values: [LocalTakeRequestStatus.FAILED, takeRequestId],
463
+ });
464
+ await refundTakeRequest(pgClient, takeRequestId);
465
+ return null;
466
+ }
467
+ const TRANSFER_RETRY_TIMEOUT_SECONDS = 30;
468
+ const takeRequestAfterAttempt = await pgClient
469
+ .query({
470
+ text: `SELECT transfer_first_failure_at FROM hub.take_request WHERE id = $1`,
471
+ values: [takeRequestId],
472
+ })
473
+ .then(exactlyOneRow);
474
+ const elapsedMs = Date.now() - takeRequestAfterAttempt.transfer_first_failure_at.getTime();
475
+ if (elapsedMs >= TRANSFER_RETRY_TIMEOUT_SECONDS * 1000) {
476
+ logger.error({ takeRequestId, elapsedMs }, `[attemptTransfer] Retry timeout (${TRANSFER_RETRY_TIMEOUT_SECONDS}s) reached, marking as FAILED and refunding`);
477
+ await pgClient.query({
478
+ text: `
479
+ UPDATE hub.take_request
480
+ SET status = $1,
481
+ debug = COALESCE(debug, '') || ' | Transfer retry timeout reached, refunding'
482
+ WHERE id = $2
483
+ `,
484
+ values: [LocalTakeRequestStatus.FAILED, takeRequestId],
485
+ });
486
+ await refundTakeRequest(pgClient, takeRequestId);
487
+ }
442
488
  return null;
443
489
  }
444
490
  }
@@ -720,7 +766,10 @@ async function processStuckRequests({ casinoId, graphqlClient, abortSignal, pool
720
766
  WHERE casino_id = $1
721
767
  AND status = $2
722
768
  AND mp_transfer_id IS NULL
723
- AND updated_at < now() - interval '5 minutes'
769
+ AND (
770
+ transfer_first_failure_at IS NULL
771
+ OR transfer_first_failure_at < now() - interval '10 seconds'
772
+ )
724
773
  LIMIT 10
725
774
  `,
726
775
  values: [casinoId, LocalTakeRequestStatus.PROCESSING],
@@ -741,6 +790,41 @@ async function processStuckRequests({ casinoId, graphqlClient, abortSignal, pool
741
790
  });
742
791
  }
743
792
  }
793
+ export async function processFailedRequestRefunds({ casinoId, abortSignal, pool, }) {
794
+ if (abortSignal.aborted) {
795
+ return;
796
+ }
797
+ const failedRequests = await pool
798
+ .query({
799
+ text: `
800
+ SELECT id, mp_take_request_id
801
+ FROM hub.take_request
802
+ WHERE casino_id = $1
803
+ AND status = $2
804
+ AND mp_transfer_id IS NULL
805
+ AND refunded_at IS NULL
806
+ AND reserved_amount > 0
807
+ LIMIT 10
808
+ `,
809
+ values: [casinoId, LocalTakeRequestStatus.FAILED],
810
+ })
811
+ .then((result) => result.rows);
812
+ if (failedRequests.length > 0) {
813
+ logger.info(`[processFailedRequestRefunds] Found ${failedRequests.length} FAILED requests needing refunds`);
814
+ }
815
+ for (const request of failedRequests) {
816
+ if (abortSignal.aborted) {
817
+ break;
818
+ }
819
+ await withPgPoolTransaction(pool, async (pgClient) => {
820
+ await PgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
821
+ mpTakeRequestId: request.mp_take_request_id,
822
+ casinoId,
823
+ });
824
+ await refundTakeRequest(pgClient, request.id);
825
+ });
826
+ }
827
+ }
744
828
  async function loadRequiredEntities(pgClient, params) {
745
829
  const { type, currencyKey, casinoId } = params;
746
830
  const dbCurrency = await pgClient
@@ -926,6 +1010,90 @@ async function updateTakeRequestStatus(pgClient, takeRequestId, status) {
926
1010
  values: [status, takeRequestId],
927
1011
  });
928
1012
  }
1013
+ async function refundTakeRequest(pgClient, takeRequestId) {
1014
+ const takeRequestData = await pgClient
1015
+ .query({
1016
+ text: `
1017
+ SELECT *
1018
+ FROM hub.take_request
1019
+ WHERE id = $1
1020
+ FOR UPDATE
1021
+ `,
1022
+ values: [takeRequestId],
1023
+ })
1024
+ .then(exactlyOneRow);
1025
+ if (takeRequestData.refunded_at) {
1026
+ logger.debug({ takeRequestId }, `[refundTakeRequest] Already refunded, skipping`);
1027
+ return false;
1028
+ }
1029
+ if (takeRequestData.reserved_amount <= 0) {
1030
+ logger.debug({ takeRequestId }, `[refundTakeRequest] No reserved amount to refund`);
1031
+ return false;
1032
+ }
1033
+ const dbLockedBalance = await pgClient
1034
+ .query({
1035
+ text: `
1036
+ SELECT id, amount
1037
+ FROM hub.balance
1038
+ WHERE user_id = $1
1039
+ AND experience_id = $2
1040
+ AND casino_id = $3
1041
+ AND currency_key = $4
1042
+ FOR UPDATE
1043
+ `,
1044
+ values: [
1045
+ takeRequestData.user_id,
1046
+ takeRequestData.experience_id,
1047
+ takeRequestData.casino_id,
1048
+ takeRequestData.currency_key,
1049
+ ],
1050
+ })
1051
+ .then(maybeOneRow);
1052
+ if (!dbLockedBalance) {
1053
+ logger.error({ takeRequestId, takeRequestData }, `[refundTakeRequest] Balance record not found - cannot refund`);
1054
+ await pgClient.query({
1055
+ text: `
1056
+ UPDATE hub.take_request
1057
+ SET debug = COALESCE(debug, '') || ' | Refund failed: balance record not found'
1058
+ WHERE id = $1
1059
+ `,
1060
+ values: [takeRequestId],
1061
+ });
1062
+ return false;
1063
+ }
1064
+ await pgClient.query({
1065
+ text: `
1066
+ UPDATE hub.balance
1067
+ SET amount = amount + $1
1068
+ WHERE id = $2
1069
+ `,
1070
+ values: [takeRequestData.reserved_amount, dbLockedBalance.id],
1071
+ });
1072
+ await insertAuditLog(pgClient, "player-balance", {
1073
+ balanceId: dbLockedBalance.id,
1074
+ balanceOld: dbLockedBalance.amount,
1075
+ balanceNew: dbLockedBalance.amount + takeRequestData.reserved_amount,
1076
+ balanceDelta: takeRequestData.reserved_amount,
1077
+ action: "hub:take_request:refund",
1078
+ refType: "hub.take_request",
1079
+ refId: takeRequestId,
1080
+ });
1081
+ await pgClient.query({
1082
+ text: `
1083
+ UPDATE hub.take_request
1084
+ SET refunded_at = now(),
1085
+ debug = COALESCE(debug, '') || $2,
1086
+ updated_at = now()
1087
+ WHERE id = $1
1088
+ `,
1089
+ values: [
1090
+ takeRequestId,
1091
+ ` | Refunded ${takeRequestData.reserved_amount} base units of ${takeRequestData.currency_key}`,
1092
+ ],
1093
+ });
1094
+ logger.info({ takeRequestId, amount: takeRequestData.reserved_amount }, `[refundTakeRequest] Successfully refunded`);
1095
+ return true;
1096
+ }
929
1097
  function isNetworkError(e) {
930
1098
  return (e.message?.includes("ECONNREFUSED") ||
931
1099
  e.message?.includes("ECONNRESET") ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.19.3",
3
+ "version": "1.19.5-dev.0",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [