Skip to content

Commit 955cd7c

Browse files
committed
fix: address code review feedback on PR #38
normalizeCodeStatus: replace parseInt with /^\d+$/ guard so partial numeric strings like '4foo' are no longer misclassified as a known status code. deleteUserRedeemedCodes: replace RETURNING-all-ids approach with a COUNT(*) + DELETE pair to avoid materialising a large array of row IDs for users with many redemption records. backfillHandler: validate instance_id is present and non-'0' after confirming playerData.details exists, matching the check in autoRedeemer.ts. Skip the submitCode call when instance_id is invalid. backfillHandler: replace the N+1 isCodeRedeemed/addPendingCode loop with a single getRedeemedCodesFromList query to build the excluded set, then bulk-insert the remainder via addNewPendingCodes. Adds getRedeemedCodesFromList to codeManager (SELECT … IN with Set return). Tests: add normalizeCodeStatus partial-string case, getRedeemedCodesFromList unit tests, and a backfillHandler scenario covering invalid instance_id. Signed-off-by: Michael Cramer <michael@bigmichi1.de>
1 parent f61ebd2 commit 955cd7c

4 files changed

Lines changed: 107 additions & 19 deletions

File tree

src/bot/database/codeManager.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ describe('normalizeCodeStatus', () => {
5959
test('normalizes numeric string "4" to Code Expired', () => {
6060
expect(normalizeCodeStatus('4')).toBe('Code Expired');
6161
});
62+
test('treats partial-numeric strings as canonical (not parsed as int)', () => {
63+
expect(normalizeCodeStatus('4foo')).toBe('4foo');
64+
expect(normalizeCodeStatus('0bar')).toBe('0bar');
65+
});
6266
});
6367

6468
// ---------------------------------------------------------------------------
@@ -447,6 +451,36 @@ describe('deleteUserRedeemedCodes', () => {
447451
});
448452
});
449453

454+
// ---------------------------------------------------------------------------
455+
// getRedeemedCodesFromList
456+
// ---------------------------------------------------------------------------
457+
describe('getRedeemedCodesFromList', () => {
458+
test('returns empty set for empty codes list', async () => {
459+
const result = await codeManager.getRedeemedCodesFromList([]);
460+
expect(result.size).toBe(0);
461+
});
462+
test('returns empty set when no codes have been redeemed', async () => {
463+
const result = await codeManager.getRedeemedCodesFromList(['CODE1234ABCD']);
464+
expect(result.size).toBe(0);
465+
});
466+
test('returns codes with Success status', async () => {
467+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_A, 'Success');
468+
const result = await codeManager.getRedeemedCodesFromList(['CODE1234ABCD', 'CODE1111AAAA']);
469+
expect(result.has('CODE1234ABCD')).toBe(true);
470+
expect(result.has('CODE1111AAAA')).toBe(false);
471+
});
472+
test('returns codes with Code Expired status', async () => {
473+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_A, 'Code Expired');
474+
const result = await codeManager.getRedeemedCodesFromList(['CODE1234ABCD']);
475+
expect(result.has('CODE1234ABCD')).toBe(true);
476+
});
477+
test('excludes non-qualifying statuses like Invalid Parameters', async () => {
478+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_A, 'Invalid Parameters');
479+
const result = await codeManager.getRedeemedCodesFromList(['CODE1234ABCD']);
480+
expect(result.has('CODE1234ABCD')).toBe(false);
481+
});
482+
});
483+
450484
// ---------------------------------------------------------------------------
451485
// getRedeemedCodesByUsers
452486
// ---------------------------------------------------------------------------

src/bot/database/codeManager.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ const CODE_STATUS_MAP: Record<number, string> = {
1818
* to the canonical status string stored in the database.
1919
*/
2020
export function normalizeCodeStatus(status: number | string): string {
21-
const n = typeof status === 'number' ? status : parseInt(String(status), 10);
22-
if (!isNaN(n)) {
23-
return CODE_STATUS_MAP[n] ?? 'Unknown Status';
21+
if (typeof status === 'number') {
22+
return CODE_STATUS_MAP[status] ?? 'Unknown Status';
2423
}
25-
return String(status);
24+
if (/^\d+$/.test(status)) {
25+
return CODE_STATUS_MAP[Number(status)] ?? 'Unknown Status';
26+
}
27+
return status;
2628
}
2729

2830
export const CHEST_TYPE_NAMES: Record<number, string> = {
@@ -292,13 +294,34 @@ class CodeManager {
292294
return inserted.map((r) => r.code);
293295
}
294296

297+
/**
298+
* Returns the subset of `codes` that already have at least one
299+
* Success or Code Expired row in redeemed_codes (i.e. globally redeemed).
300+
* Uses a single IN query — suitable for bulk pre-filter before addNewPendingCodes.
301+
*/
302+
async getRedeemedCodesFromList(codes: string[]): Promise<Set<string>> {
303+
if (codes.length === 0) return new Set();
304+
const rows = db
305+
.select({ code: redeemedCodes.code })
306+
.from(redeemedCodes)
307+
.where(
308+
and(
309+
inArray(redeemedCodes.code, codes),
310+
or(eq(redeemedCodes.status, 'Success'), eq(redeemedCodes.status, 'Code Expired'))
311+
)
312+
)
313+
.all();
314+
return new Set(rows.map((r) => r.code));
315+
}
316+
295317
async deleteUserRedeemedCodes(discordId: string): Promise<number> {
296-
const deleted = db
297-
.delete(redeemedCodes)
318+
const countRow = db
319+
.select({ count: sql<number>`count(*)` })
320+
.from(redeemedCodes)
298321
.where(eq(redeemedCodes.discordId, discordId))
299-
.returning({ id: redeemedCodes.id })
300-
.all();
301-
return deleted.length;
322+
.get();
323+
db.delete(redeemedCodes).where(eq(redeemedCodes.discordId, discordId)).run();
324+
return countRow?.count ?? 0;
302325
}
303326

304327
/**

src/bot/handlers/backfillHandler.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,31 @@ describe('Scenario 3: duplicate backfill runs do not create duplicate pending co
278278
expect(hits).toHaveLength(1);
279279
});
280280
});
281+
282+
// ---------------------------------------------------------------------------
283+
// Scenario 4 — invalid instance_id skips submission but stores codes as pending
284+
// ---------------------------------------------------------------------------
285+
286+
describe('Scenario 4: invalid instance_id skips code submission', () => {
287+
test('codes become pending when instance_id is missing from user details', async () => {
288+
await userManager.saveCredentials({
289+
discordId: USER,
290+
userId: '316463',
291+
userHash: 'f4e6d3dbc34173d23e7d198e4a8fc773',
292+
server: MOCK_SERVER,
293+
});
294+
295+
// details object exists but instance_id is absent (falsy → treated as '0')
296+
getUserDetailsSpy.mockResolvedValue({ details: {} } as any);
297+
298+
const stats = await backfillChannelHistory(makeChannel([CODE_A, CODE_B]));
299+
300+
expect(stats.codesRedeemed).toBe(0);
301+
expect(submitCodeSpy).not.toHaveBeenCalled();
302+
303+
const pending = await codeManager.getPendingCodes();
304+
expect(pending).toContain(CODE_A);
305+
expect(pending).toContain(CODE_B);
306+
});
307+
});
308+

src/bot/handlers/backfillHandler.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,13 @@ export async function backfillChannelHistory(
192192
continue;
193193
}
194194

195-
const instanceId = playerData.details?.instance_id || '';
195+
const instanceId = playerData.details?.instance_id || '0';
196+
if (!instanceId || instanceId === '0') {
197+
logger.warn(
198+
`[BACKFILL] Skipping code ${code} for user ${user.discordId}: invalid instance_id`
199+
);
200+
continue;
201+
}
196202

197203
const response = await IdleChampionsApi.submitCode({
198204
server,
@@ -234,15 +240,12 @@ export async function backfillChannelHistory(
234240
}
235241

236242
// Store any remaining codes as pending so /catchup can find them later.
237-
// addPendingCode is idempotent (onConflictDoNothing) so running backfill
238-
// multiple times is safe.
239-
for (const code of allCodes) {
240-
const isRedeemed = await codeManager.isCodeRedeemed(code);
241-
if (!isRedeemed) {
242-
await codeManager.addPendingCode(code);
243-
stats.pendingCodes++;
244-
}
245-
}
243+
// Single query to find already-redeemed codes, then bulk-insert the rest.
244+
const allCodesArr = [...allCodes];
245+
const redeemedSet = await codeManager.getRedeemedCodesFromList(allCodesArr);
246+
const codesToPend = allCodesArr.filter((c) => !redeemedSet.has(c));
247+
const inserted = await codeManager.addNewPendingCodes(codesToPend);
248+
stats.pendingCodes = inserted.length;
246249

247250
onProgress?.(
248251
`✅ Backfill complete! Found: ${stats.codesFound}, Redeemed: ${stats.codesRedeemed}, Pending: ${stats.pendingCodes}`

0 commit comments

Comments
 (0)