Codex commited on
Commit
ec4cf41
·
1 Parent(s): 7da65ca

Add bulk resolve command

Browse files
Files changed (6) hide show
  1. README.md +1 -0
  2. src/commands.js +20 -0
  3. src/embeds.js +20 -0
  4. src/index.js +47 -0
  5. src/resolve-bulk.js +39 -0
  6. test/resolve-bulk.test.js +24 -0
README.md CHANGED
@@ -16,6 +16,7 @@ A simple Discord bot for manually tracking bets and ROI per user. Each Discord u
16
 
17
  - `/bet` opens a modal so users can log bets privately
18
  - `/resolve` grades a bet as `win`, `loss`, or `void`
 
19
  - `/roi` shows net profit, ROI percent, and a cumulative profit chart
20
  - `/bets` shows total bet count and the latest 5 bets
21
  - `/summary` shows win/loss totals plus a chart
 
16
 
17
  - `/bet` opens a modal so users can log bets privately
18
  - `/resolve` grades a bet as `win`, `loss`, or `void`
19
+ - `/resolveall` grades multiple bet IDs to the same result
20
  - `/roi` shows net profit, ROI percent, and a cumulative profit chart
21
  - `/bets` shows total bet count and the latest 5 bets
22
  - `/summary` shows win/loss totals plus a chart
src/commands.js CHANGED
@@ -34,6 +34,26 @@ export const commands = [
34
  { name: 'void', value: 'void' }
35
  )
36
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  new SlashCommandBuilder()
38
  .setName('roi')
39
  .setDescription('Show your ROI, net profit, and cumulative profit chart.'),
 
34
  { name: 'void', value: 'void' }
35
  )
36
  ),
37
+ new SlashCommandBuilder()
38
+ .setName('resolveall')
39
+ .setDescription('Resolve multiple of your tracked bets with the same result.')
40
+ .addStringOption((option) =>
41
+ option
42
+ .setName('bet_ids')
43
+ .setDescription('Comma-separated bet IDs, for example: 12, 13, 14')
44
+ .setRequired(true)
45
+ )
46
+ .addStringOption((option) =>
47
+ option
48
+ .setName('result')
49
+ .setDescription('The final result for all listed bets.')
50
+ .setRequired(true)
51
+ .addChoices(
52
+ { name: 'win', value: 'win' },
53
+ { name: 'loss', value: 'loss' },
54
+ { name: 'void', value: 'void' }
55
+ )
56
+ ),
57
  new SlashCommandBuilder()
58
  .setName('roi')
59
  .setDescription('Show your ROI, net profit, and cumulative profit chart.'),
src/embeds.js CHANGED
@@ -117,6 +117,7 @@ export function buildCommandsEmbed() {
117
  .addFields(
118
  { name: '/bet', value: 'Open a private modal to log a new bet.' },
119
  { name: '/resolve', value: 'Grade one of your bet IDs as win, loss, or void.' },
 
120
  { name: '/roi', value: 'Show your net profit, ROI %, and cumulative chart.' },
121
  { name: '/bets', value: 'Show your total tracked bets and latest 5 bets.' },
122
  { name: '/summary', value: 'Show full totals, record, ROI %, and chart.' },
@@ -172,3 +173,22 @@ export function buildBooksEmbed(bookRows) {
172
 
173
  return embed;
174
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  .addFields(
118
  { name: '/bet', value: 'Open a private modal to log a new bet.' },
119
  { name: '/resolve', value: 'Grade one of your bet IDs as win, loss, or void.' },
120
+ { name: '/resolveall', value: 'Grade multiple bet IDs to the same result in one command.' },
121
  { name: '/roi', value: 'Show your net profit, ROI %, and cumulative chart.' },
122
  { name: '/bets', value: 'Show your total tracked bets and latest 5 bets.' },
123
  { name: '/summary', value: 'Show full totals, record, ROI %, and chart.' },
 
173
 
174
  return embed;
175
  }
176
+
177
+ export function buildResolveAllEmbed(result, summary) {
178
+ const colorMap = {
179
+ win: 0x15803d,
180
+ loss: 0xb91c1c,
181
+ void: 0x475569,
182
+ };
183
+
184
+ const embed = new EmbedBuilder()
185
+ .setColor(colorMap[result] ?? 0x475569)
186
+ .setTitle(`Bulk resolve complete: ${result.toUpperCase()}`)
187
+ .addFields(
188
+ { name: 'Resolved', value: summary.resolved.length > 0 ? summary.resolved.map((id) => `#${id}`).join(', ') : 'None' },
189
+ { name: 'Already settled', value: summary.alreadyResolved.length > 0 ? summary.alreadyResolved.map((id) => `#${id}`).join(', ') : 'None', inline: true },
190
+ { name: 'Not found', value: summary.missing.length > 0 ? summary.missing.map((id) => `#${id}`).join(', ') : 'None', inline: true }
191
+ );
192
+
193
+ return embed;
194
+ }
src/index.js CHANGED
@@ -21,10 +21,12 @@ import {
21
  buildChartCaption,
22
  buildCommandsEmbed,
23
  buildErrorEmbed,
 
24
  buildResolveEmbed,
25
  buildSummaryEmbed,
26
  buildWelcomeEmbed,
27
  } from './embeds.js';
 
28
 
29
  const BET_MODAL_PREFIX = 'bet-entry-modal';
30
  const BET_PROP_INPUT_ID = 'bet-prop';
@@ -119,6 +121,11 @@ async function handleChatInput(interaction, store, config) {
119
  return;
120
  }
121
 
 
 
 
 
 
122
  if (commandName === 'roi') {
123
  await handleRoi(interaction, store);
124
  return;
@@ -266,6 +273,46 @@ async function handleResolve(interaction, store) {
266
  });
267
  }
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  async function handleRoi(interaction, store) {
270
  const summary = await store.getUserSummary(interaction.user.id);
271
  const points = await store.getChartPoints(interaction.user.id);
 
21
  buildChartCaption,
22
  buildCommandsEmbed,
23
  buildErrorEmbed,
24
+ buildResolveAllEmbed,
25
  buildResolveEmbed,
26
  buildSummaryEmbed,
27
  buildWelcomeEmbed,
28
  } from './embeds.js';
29
+ import { parseBetIdList } from './resolve-bulk.js';
30
 
31
  const BET_MODAL_PREFIX = 'bet-entry-modal';
32
  const BET_PROP_INPUT_ID = 'bet-prop';
 
121
  return;
122
  }
123
 
124
+ if (commandName === 'resolveall') {
125
+ await handleResolveAll(interaction, store);
126
+ return;
127
+ }
128
+
129
  if (commandName === 'roi') {
130
  await handleRoi(interaction, store);
131
  return;
 
273
  });
274
  }
275
 
276
+ async function handleResolveAll(interaction, store) {
277
+ const betIdsRaw = interaction.options.getString('bet_ids', true);
278
+ const result = interaction.options.getString('result', true);
279
+ const parsedIds = parseBetIdList(betIdsRaw);
280
+
281
+ if (!parsedIds.ok) {
282
+ await interaction.reply({
283
+ embeds: [buildErrorEmbed('Invalid bet IDs', parsedIds.error)],
284
+ flags: MessageFlags.Ephemeral,
285
+ });
286
+ return;
287
+ }
288
+
289
+ const summary = {
290
+ resolved: [],
291
+ alreadyResolved: [],
292
+ missing: [],
293
+ };
294
+
295
+ for (const betId of parsedIds.ids) {
296
+ const resolution = await store.resolveBet(interaction.user.id, betId, result);
297
+ if (resolution.type === 'resolved') {
298
+ summary.resolved.push(betId);
299
+ continue;
300
+ }
301
+
302
+ if (resolution.type === 'already_resolved') {
303
+ summary.alreadyResolved.push(betId);
304
+ continue;
305
+ }
306
+
307
+ summary.missing.push(betId);
308
+ }
309
+
310
+ await interaction.reply({
311
+ embeds: [buildResolveAllEmbed(result, summary)],
312
+ flags: MessageFlags.Ephemeral,
313
+ });
314
+ }
315
+
316
  async function handleRoi(interaction, store) {
317
  const summary = await store.getUserSummary(interaction.user.id);
318
  const points = await store.getChartPoints(interaction.user.id);
src/resolve-bulk.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function parseBetIdList(input) {
2
+ const tokens = String(input ?? '')
3
+ .split(/[,\s]+/)
4
+ .map((token) => token.trim())
5
+ .filter(Boolean);
6
+
7
+ if (tokens.length === 0) {
8
+ return { ok: false, error: 'Please provide at least one bet ID.' };
9
+ }
10
+
11
+ const ids = [];
12
+ const invalid = [];
13
+
14
+ for (const token of tokens) {
15
+ if (!/^\d+$/.test(token)) {
16
+ invalid.push(token);
17
+ continue;
18
+ }
19
+
20
+ const value = Number(token);
21
+ if (!Number.isSafeInteger(value) || value <= 0) {
22
+ invalid.push(token);
23
+ continue;
24
+ }
25
+
26
+ if (!ids.includes(value)) {
27
+ ids.push(value);
28
+ }
29
+ }
30
+
31
+ if (invalid.length > 0) {
32
+ return {
33
+ ok: false,
34
+ error: `Invalid bet ID value(s): ${invalid.join(', ')}`,
35
+ };
36
+ }
37
+
38
+ return { ok: true, ids };
39
+ }
test/resolve-bulk.test.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { parseBetIdList } from '../src/resolve-bulk.js';
4
+
5
+ test('parses comma-separated bet ids', () => {
6
+ const parsed = parseBetIdList('12, 13,14');
7
+
8
+ assert.equal(parsed.ok, true);
9
+ assert.deepEqual(parsed.ids, [12, 13, 14]);
10
+ });
11
+
12
+ test('deduplicates repeated ids', () => {
13
+ const parsed = parseBetIdList('7 7 8');
14
+
15
+ assert.equal(parsed.ok, true);
16
+ assert.deepEqual(parsed.ids, [7, 8]);
17
+ });
18
+
19
+ test('rejects invalid ids', () => {
20
+ const parsed = parseBetIdList('4, nope, 6');
21
+
22
+ assert.equal(parsed.ok, false);
23
+ assert.match(parsed.error, /nope/);
24
+ });