@moneypot/hub 1.9.0-dev.12 → 1.9.0-dev.14

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.
@@ -112,6 +112,7 @@ export type DbTakeRequest = {
112
112
  mp_transfer_status: DbTransferStatusKind | null;
113
113
  transfer_needs_completion: boolean;
114
114
  transfer_completion_attempted_at: Date | null;
115
+ refunded_at: Date | null;
115
116
  updated_at: Date;
116
117
  };
117
118
  export type DbHashChain = {
@@ -1,5 +1,9 @@
1
1
  import { QueryResult, QueryResultRow } from "pg";
2
2
  import { PgClientResult } from "postgraphile/@dataplan/pg";
3
+ import * as pg from "pg";
4
+ export interface QueryExecutor {
5
+ query<T extends pg.QueryResultRow = any>(queryText: string, values?: any[]): Promise<pg.QueryResult<T>>;
6
+ }
3
7
  type ResultType<T> = PgClientResult<T> | QueryResult<T extends QueryResultRow ? T : never>;
4
8
  export declare function maybeOneRow<T>(result: ResultType<T>): T | undefined;
5
9
  export declare function exactlyOneRow<T>(result: ResultType<T>): T;
@@ -26,7 +26,7 @@ export type ServerOptions = {
26
26
  exportSchemaSDLPath?: string;
27
27
  userDatabaseMigrationsPath?: string;
28
28
  };
29
- type ListenInfo = {
29
+ export declare function startAndListen(options: ServerOptions): Promise<{
30
30
  port: number;
31
- };
32
- export declare function startAndListen(options: ServerOptions): Promise<ListenInfo>;
31
+ stop: () => Promise<void>;
32
+ }>;
package/dist/src/index.js CHANGED
@@ -46,9 +46,11 @@ async function initialize(options) {
46
46
  logger.info("Initialization aborted by graceful shutdown");
47
47
  return;
48
48
  }
49
- initializeTransferProcessors({
50
- signal: options.signal,
51
- });
49
+ if (process.env.NODE_ENV !== "test") {
50
+ initializeTransferProcessors({
51
+ signal: options.signal,
52
+ });
53
+ }
52
54
  }
53
55
  export async function startAndListen(options) {
54
56
  if (options.plugins && options.plugins.some((p) => typeof p === "function")) {
@@ -75,7 +77,7 @@ export async function startAndListen(options) {
75
77
  extraPgSchemas: options.extraPgSchemas,
76
78
  abortSignal: abortController.signal,
77
79
  });
78
- const gracefulShutdown = async () => {
80
+ const gracefulShutdown = async ({ exit = true } = {}) => {
79
81
  if (isShuttingDown) {
80
82
  logger.warn("Already shutting down.");
81
83
  return;
@@ -87,12 +89,13 @@ export async function startAndListen(options) {
87
89
  logger.info("Closing resources...");
88
90
  hubServer.shutdown();
89
91
  try {
92
+ const closeTasks = [db.notifier.close()];
93
+ if (process.env.NODE_ENV !== "test") {
94
+ closeTasks.push(db.postgraphilePool.end());
95
+ closeTasks.push(db.superuserPool.end());
96
+ }
90
97
  await Promise.race([
91
- Promise.all([
92
- db.notifier.close(),
93
- db.postgraphilePool.end(),
94
- db.superuserPool.end(),
95
- ]),
98
+ Promise.all(closeTasks),
96
99
  new Promise((_, reject) => setTimeout(() => reject(new Error("Database cleanup timeout")), 3000)),
97
100
  ]);
98
101
  logger.info("Cleanup complete.");
@@ -100,13 +103,18 @@ export async function startAndListen(options) {
100
103
  catch (err) {
101
104
  logger.warn(err, "Cleanup error (proceeding anyway)");
102
105
  }
103
- process.exit(0);
106
+ if (exit) {
107
+ process.exit(0);
108
+ }
104
109
  };
105
110
  process.on("SIGINT", gracefulShutdown);
106
111
  process.on("SIGTERM", gracefulShutdown);
107
112
  return hubServer.listen().then(() => {
108
113
  return {
109
114
  port: config.PORT,
115
+ stop: async () => {
116
+ await gracefulShutdown({ exit: false });
117
+ },
110
118
  };
111
119
  });
112
120
  }
@@ -0,0 +1 @@
1
+ alter table hub.take_request add column refunded_at timestamptz
@@ -7,6 +7,7 @@ import { TRANSFER_FIELDS } from "./graphql.js";
7
7
  import { logger } from "../logger.js";
8
8
  import { processTransfer } from "./process-transfer.js";
9
9
  import assert from "assert";
10
+ import { superuserPool } from "../db/index.js";
10
11
  import { mpGetTakeRequest, processSingleTakeRequest, } from "../take-request/process-take-request.js";
11
12
  function httpToWs(url) {
12
13
  if (url.protocol === "http:") {
@@ -107,6 +108,7 @@ export function startWebsocketProcessor({ casinoId, graphqlUrl, signal, controll
107
108
  mpTakeRequest,
108
109
  casinoId,
109
110
  graphqlClient,
111
+ pool: superuserPool,
110
112
  });
111
113
  });
112
114
  signal.addEventListener("abort", () => {
@@ -7,11 +7,10 @@ export type RiskPolicyArgs = {
7
7
  maxPotentialPayout: number;
8
8
  outcomes: DbOutcome[];
9
9
  };
10
- type AtLeastOneKey<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Required<Pick<T, Keys>> & Partial<Omit<T, Keys>> : never;
11
- export type RiskLimits = AtLeastOneKey<{
10
+ export type RiskLimits = {
12
11
  maxWager?: number;
13
- maxPayout?: number;
14
- }>;
12
+ maxPayout: number;
13
+ };
15
14
  export type RiskPolicy = (args: RiskPolicyArgs) => RiskLimits;
16
15
  export declare function validateRisk(options: RiskPolicyArgs & {
17
16
  riskPolicy: RiskPolicy;
@@ -19,4 +18,3 @@ export declare function validateRisk(options: RiskPolicyArgs & {
19
18
  displayUnitName: string;
20
19
  displayUnitScale: number;
21
20
  }): Result<void, string>;
22
- export {};
@@ -4,12 +4,9 @@ import { z } from "zod";
4
4
  const RiskLimitsSchema = z
5
5
  .object({
6
6
  maxWager: z.number().positive().optional(),
7
- maxPayout: z.number().positive().optional(),
7
+ maxPayout: z.number().positive(),
8
8
  })
9
- .strict()
10
- .refine((v) => v.maxWager !== undefined || v.maxPayout !== undefined, {
11
- message: "Provide at least one of maxWager or maxPayout.",
12
- });
9
+ .strict();
13
10
  export function validateRisk(options) {
14
11
  const { wager, bankroll, maxPotentialPayout, riskPolicy } = options;
15
12
  if (maxPotentialPayout > bankroll) {
@@ -48,7 +45,7 @@ export function validateRisk(options) {
48
45
  })})`,
49
46
  };
50
47
  }
51
- if (limits.maxPayout !== undefined && maxPotentialPayout > limits.maxPayout) {
48
+ if (maxPotentialPayout > limits.maxPayout) {
52
49
  return {
53
50
  ok: false,
54
51
  error: `Payout (${formatCurrency(maxPotentialPayout, {
@@ -56,6 +56,7 @@ export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abo
56
56
  if (!exportSchemaSDLPath.endsWith(".graphql")) {
57
57
  throw new Error("exportSchemaSDLPath must end with .graphql");
58
58
  }
59
+ logger.info(`Will save generated graphql schema to ${exportSchemaSDLPath}`);
59
60
  }
60
61
  const mutablePlugins = [...plugins];
61
62
  for (const requiredPlugin of requiredPlugins) {
@@ -64,7 +65,6 @@ export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abo
64
65
  mutablePlugins.unshift(requiredPlugin);
65
66
  }
66
67
  }
67
- logger.info(`Will save generated graphql schema to ${exportSchemaSDLPath}`);
68
68
  const preset = {
69
69
  extends: [PostGraphileAmberPreset],
70
70
  disablePlugins: ["NodePlugin"],
@@ -1,5 +1,6 @@
1
1
  import { GraphQLClient } from "graphql-request";
2
2
  import { MpTakeRequestFieldsFragment } from "../__generated__/graphql.js";
3
+ import * as pg from "pg";
3
4
  export declare const MP_TAKE_REQUEST_FIELDS: import("@graphql-typed-document-node/core").TypedDocumentNode<MpTakeRequestFieldsFragment, unknown>;
4
5
  export declare function mpGetTakeRequest(graphqlClient: GraphQLClient, id: string): Promise<MpTakeRequestFieldsFragment | null>;
5
6
  export declare function processTakeRequests({ abortSignal, controllerId, casinoId, graphqlClient, }: {
@@ -8,9 +9,18 @@ export declare function processTakeRequests({ abortSignal, controllerId, casinoI
8
9
  casinoId: string;
9
10
  graphqlClient: GraphQLClient;
10
11
  }): Promise<void>;
11
- export declare function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest, casinoId, graphqlClient, }: {
12
+ export declare function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest, casinoId, graphqlClient, pool, }: {
12
13
  mpTakeRequestId: string;
13
14
  mpTakeRequest?: MpTakeRequestFieldsFragment;
14
15
  casinoId: string;
15
16
  graphqlClient: GraphQLClient;
17
+ pool: pg.Pool;
16
18
  }): Promise<string | null>;
19
+ export declare function completeTransfer({ mpTakeRequestId, takeRequestId, mpTransferId, graphqlClient, casinoId, pool, }: {
20
+ mpTakeRequestId: string;
21
+ takeRequestId: string;
22
+ mpTransferId: string;
23
+ graphqlClient: GraphQLClient;
24
+ casinoId: string;
25
+ pool: pg.Pool;
26
+ }): Promise<void>;
@@ -180,6 +180,7 @@ export async function processTakeRequests({ abortSignal, controllerId, casinoId,
180
180
  mpTakeRequest: takeRequest,
181
181
  casinoId,
182
182
  graphqlClient,
183
+ pool: superuserPool,
183
184
  });
184
185
  }
185
186
  await processPendingTransferCompletions({
@@ -200,8 +201,8 @@ async function fetchPendingTakeRequests(graphqlClient, controllerId) {
200
201
  return useFragment(MP_TAKE_REQUEST_FIELDS, takeRequest);
201
202
  }) || []);
202
203
  }
203
- export async function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest, casinoId, graphqlClient, }) {
204
- return withPgPoolTransaction(superuserPool, async (pgClient) => {
204
+ export async function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest, casinoId, graphqlClient, pool, }) {
205
+ return withPgPoolTransaction(pool, async (pgClient) => {
205
206
  await PgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
206
207
  mpTakeRequestId,
207
208
  casinoId,
@@ -247,10 +248,7 @@ async function createAndProcessNewTakeRequest({ pgClient, mpTakeRequest, casinoI
247
248
  currencyKey: mpTakeRequest.currencyKey,
248
249
  casinoId,
249
250
  });
250
- if (!dbUser || !dbExperience || !dbCurrency || !dbBalance) {
251
- await rejectMpTakeRequest(pgClient, graphqlClient, mpTakeRequest.id);
252
- return null;
253
- }
251
+ assert(dbUser && dbExperience && dbCurrency && dbBalance, "Required entities not found");
254
252
  const amountToTransfer = Math.floor(typeof mpTakeRequest.amount === "number"
255
253
  ? Math.min(mpTakeRequest.amount, dbBalance.amount)
256
254
  : dbBalance.amount);
@@ -476,15 +474,29 @@ async function processPendingTransferCompletions({ casinoId, graphqlClient, abor
476
474
  mpTransferId: request.mp_transfer_id,
477
475
  graphqlClient,
478
476
  casinoId,
477
+ pool: superuserPool,
479
478
  });
480
479
  }
481
480
  }
482
- async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTransferId, graphqlClient, casinoId, }) {
483
- return withPgPoolTransaction(superuserPool, async (pgClient) => {
481
+ export async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTransferId, graphqlClient, casinoId, pool, }) {
482
+ return withPgPoolTransaction(pool, async (pgClient) => {
484
483
  await PgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
485
484
  mpTakeRequestId,
486
485
  casinoId,
487
486
  });
487
+ const dbTakeRequest = await pgClient
488
+ .query({
489
+ text: `
490
+ SELECT transfer_needs_completion
491
+ FROM hub.take_request
492
+ WHERE id = $1
493
+ `,
494
+ values: [takeRequestId],
495
+ })
496
+ .then(exactlyOneRow);
497
+ if (!dbTakeRequest.transfer_needs_completion) {
498
+ return;
499
+ }
488
500
  await pgClient.query({
489
501
  text: `
490
502
  UPDATE hub.take_request
@@ -553,26 +565,57 @@ async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTransferId,
553
565
  if (!mpStatus) {
554
566
  throw new Error("No MP status returned from MP API");
555
567
  }
568
+ const takeRequestData = await pgClient
569
+ .query({
570
+ text: `
571
+ SELECT *
572
+ FROM hub.take_request
573
+ WHERE id = $1
574
+ `,
575
+ values: [takeRequestId],
576
+ })
577
+ .then(exactlyOneRow);
578
+ if (takeRequestData.refunded_at) {
579
+ logger.warn({ mpTransferId, takeRequestId }, `[completeTransfer] Transfer was already refunded. This should never happen.`);
580
+ break;
581
+ }
582
+ await pgClient.query({
583
+ text: `
584
+ UPDATE hub.balance
585
+ SET amount = amount + $1
586
+ WHERE user_id = $2
587
+ AND experience_id = $3
588
+ AND casino_id = $4
589
+ AND currency_key = $5
590
+ `,
591
+ values: [
592
+ takeRequestData.reserved_amount,
593
+ takeRequestData.user_id,
594
+ takeRequestData.experience_id,
595
+ takeRequestData.casino_id,
596
+ takeRequestData.currency_key,
597
+ ],
598
+ });
556
599
  await pgClient.query({
557
600
  text: `
558
601
  UPDATE hub.take_request
559
602
  SET
560
603
  transfer_needs_completion = FALSE,
561
604
  mp_transfer_status = $2,
562
- status = $3,
563
- mp_status = $4,
564
- debug = $5
605
+ mp_status = $3,
606
+ debug = $4,
607
+ refunded_at = now(),
608
+ updated_at = now()
565
609
  WHERE id = $1
566
610
  `,
567
611
  values: [
568
612
  takeRequestId,
569
613
  currentStatus,
570
- LocalTakeRequestStatus.FAILED,
571
614
  mpStatus,
572
- `MP transfer was ${currentStatus}`,
615
+ `MP transfer was ${currentStatus}. Refunded ${takeRequestData.reserved_amount} to user`,
573
616
  ],
574
617
  });
575
- logger.info(`[completeTransfer] Transfer ${mpTransferId} has status ${currentStatus}`);
618
+ logger.info(`[completeTransfer] Transfer ${mpTransferId} has status ${currentStatus}. Refunded ${takeRequestData.reserved_amount} base units of ${takeRequestData.currency_key} to hub user.`);
576
619
  }
577
620
  else {
578
621
  logger.info(`[completeTransfer] Transfer ${mpTransferId} has status ${currentStatus}, will retry later`);
@@ -665,6 +708,7 @@ async function processStuckRequests({ casinoId, graphqlClient, abortSignal, }) {
665
708
  mpTakeRequestId: request.mp_take_request_id,
666
709
  casinoId,
667
710
  graphqlClient,
711
+ pool: superuserPool,
668
712
  });
669
713
  }
670
714
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.9.0-dev.12",
3
+ "version": "1.9.0-dev.14",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [