Skip to content

Wait for the daily login credit before returning the token - #11

Open
DJAscendance wants to merge 2 commits into
masterfrom
fix/daily-credit-await
Open

Wait for the daily login credit before returning the token#11
DJAscendance wants to merge 2 commits into
masterfrom
fix/daily-credit-await

Conversation

@DJAscendance

@DJAscendance DJAscendance commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Fixes a race in the daily login payout that has been live since December 2022.

The bug

login() called maybeGiveDailyCredits(member.id) without awaiting it, so the credit was still in flight when the token went back to the caller.

this.maybeGiveDailyCredits(member.id);   // fire-and-forget
return this.encodeMemberToken(member);

Three ways it bites:

  • the caller can read its own balance and not see the credit yet;
  • two quick logins can both observe "not credited today" and pay twice, since the check and the write are no longer close together;
  • if the request finished first, nothing kept the process interested in the write, so the credit could simply be lost.

Introduced by 633b7ce, which added two call sites in one change — one awaited, one not. This is the one that was not.

Now awaited and caught: refusing someone their account because a daily bonus failed would be a worse outcome than the missing bonus. Previously a failure here surfaced as an unhandled promise rejection and nothing else.

Why the knexfile commit is in here

A spec asserting exactly this behaviour existed the whole time and had never once executed. knexfile.ts had no test environment, so config[process.env.NODE_ENV] was undefined under Jest and 8 of 12 suites died at import:

TypeError: Cannot read properties of undefined (reading 'client')
  at new Db (src/db/db.class.ts:13:22)

Without that commit the fix ships unprovable, and the fix's own commit message refers back to it. They are separate commits so the split is still easy if you want the one-file change on its own.

Verification

  • member.service.spec.ts 9/9 with the fix (was 7/9).
  • Mutation test: reintroducing the unawaited call fails exactly the two daily-credit tests — "gives daily xp to the member" and "updates the timestamp of when the user last received login credit" — and leaves the other seven green. So they are not passing by accident.

Note the spec reaches a real database through an unmocked transaction, so it needs ctr-db-1 up; that is a pre-existing issue, not introduced here.

Out of scope, but worth knowing

Summary by CodeRabbit

  • Bug Fixes

    • Improved login reliability by ensuring daily credits are processed before sign-in completes.
    • Prevented credit distribution issues from blocking login or causing unhandled errors.
  • Tests

    • Added a dedicated test database configuration with controlled connection limits for more reliable automated testing.

DJAscendance added 2 commits July 31, 2026 20:07
`npm test` reported 8 of 12 suites as failing. None of them were failing on an
assertion -- they died while loading:

  TypeError: Cannot read properties of undefined (reading 'client')
    at new Db (src/db/db.class.ts:13:22)

Db's constructor runs `knex(config[process.env.NODE_ENV])` at import time, jest
sets NODE_ENV=test, and knexfile only defined `development` and `production`.
`config['test']` was undefined, so every module that transitively imported a
repository blew up before a single test ran. Four tests executed in total.

Adding the key takes that to 8 suites passing and 28 tests running.

pool.min is 0 here rather than 2: a minimum of 2 makes the pool open
connections nothing asked for, and jest then hangs waiting on handles that will
never close. Unit tests never open a socket regardless, because knex connects
lazily -- they only needed the constructor not to throw.

Connection details still come from the environment, so this is also the key a
database-backed test points at; set DB_DATABASE to a throwaway schema for that.

Two things this exposes, both pre-existing and neither caused by this change:

  - src/services/member/member.service.spec.ts has 2 genuine assertion
    failures. They are real; see the next commit.
  - role.repository.spec.ts, wallet.service.spec.ts and club.service.spec.ts
    are empty files, so jest reports "Your test suite must contain at least one
    test". They are stubs that were never written, previously indistinguishable
    from the import crash.
`login()` called `this.maybeGiveDailyCredits(member.id)` without awaiting it, so
the credit was still in flight when the token went back to the caller.

The existing spec asserts exactly this and has been failing all along -- nobody
saw it, because the suite could not load at all until the previous commit. The
call has been unawaited since 633b7ce (December 2022), which added two call
sites in one change: one awaited, one not. This is the one that was not.

What the race costs:

  - the caller can read its own balance and not see the credit yet;
  - two quick logins can both observe "not credited today" and pay twice, since
    the check and the write are no longer close together;
  - if the request finished first, nothing kept the process interested in the
    write, so the credit could simply be lost.

Now awaited, and caught: refusing someone their account because a daily bonus
failed would be a worse outcome than the missing bonus. Previously a failure
here surfaced as an unhandled promise rejection and nothing else.

member.service.spec.ts goes from 7/9 to 9/9. Full suite: 28/28 tests pass.

Same shape of bug as the fire-and-forget reconcilePrimaryRole in
fix/primary-role-atomicity -- worth a sweep for other unawaited service calls,
which is not attempted here.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request adds a test Knex environment for MySQL and updates member login to await daily credit distribution while preserving authentication when distribution fails.

Changes

Test database configuration

Layer / File(s) Summary
Knex test environment
api/src/knexfile.ts
Adds environment-based MySQL settings, migration and seed paths, and a pool limited to zero through five connections.

Login credit handling

Layer / File(s) Summary
Login daily credit flow
api/src/services/member/member.service.ts
MemberService.login awaits maybeGiveDailyCredits before issuing the token. It logs credit-distribution failures and allows authentication to succeed.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: awaiting daily login credit distribution before returning the member token.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/daily-credit-await

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/src/knexfile.ts`:
- Line 54: Update the knex configuration’s DB_PORT handling to parse with radix
10 and explicitly handle unset, empty, or invalid values; use the project’s
documented default of 3306 if applicable, otherwise throw a clear configuration
error instead of passing NaN.

In `@api/src/services/member/member.service.ts`:
- Around line 363-376: Make maybeGiveDailyCredits atomic and idempotent: execute
the daily-credit eligibility check, payout creation, and member timestamp update
within one database transaction, using a conditional claim or unique (memberId,
creditDate, creditType) constraint to prevent concurrent duplicate payouts and
allow safe retries after partial failures. Update the login flow’s comments to
reflect the transactional behavior, and add tests covering concurrent logins and
retrying after a failed transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ad6d8741-28f0-4d46-ba0b-354155e39d64

📥 Commits

Reviewing files that changed from the base of the PR and between 5c4ad75 and cd915c7.

📒 Files selected for processing (2)
  • api/src/knexfile.ts
  • api/src/services/member/member.service.ts

Comment thread api/src/knexfile.ts
client: 'mysql',
connection: {
host: process.env.DB_HOST,
port: Number.parseInt(process.env.DB_PORT),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate knexfile =="
fd -a 'knexfile\.(ts|js|mjs|cjs)$' . || true

echo "== relevant knexfile =="
if [ -f api/src/knexfile.ts ]; then
  nl -ba api/src/knexfile.ts | sed -n '1,120p'
fi

echo "== search DB_PORT usage and docs =="
rg -n "DB_PORT|process\.env\.DB_PORT|Number\.parseInt|parseInt\(" -S . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200

echo "== parse DB_PORT behavior probe =="
node - <<'JS'
const values = [undefined, "", "3306", "abc", "0xc42"];
for (const v of values) process.env.DB_PORT = v;
for (const v of values) {
  const used = process.env.DB_PORT;
  console.log(JSON.stringify({env: used, Number.parseInt: Number.parseInt(used), JSON: Number.isNaN(Number.parseInt(used))}));
}
JS

Repository: DJAscendance/ctr

Length of output: 1921


Handle an absent or invalid DB_PORT.

Number.parseInt(process.env.DB_PORT) returns NaN when DB_PORT is unset, empty, invalid, or hexadecimal. Default only if the project contract is port 3306; otherwise validate and throw a clear configuration error. Use radix 10 for parseInt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/src/knexfile.ts` at line 54, Update the knex configuration’s DB_PORT
handling to parse with radix 10 and explicitly handle unset, empty, or invalid
values; use the project’s documented default of 3306 if applicable, otherwise
throw a clear configuration error instead of passing NaN.

Comment on lines +363 to +376
// Awaited, so the credit has actually landed before the token goes back. Unawaited,
// this raced the response: the caller could read its own balance and not see it yet,
// two quick logins could both observe "not credited today" and pay twice, and a
// request that finished first left the write in flight with nothing keeping the
// process around to finish it.
//
// Caught rather than propagated, because a failure to hand out a daily bonus is not a
// reason to refuse someone their account. Before, errors here surfaced as an
// unhandled rejection instead of anything anyone would see.
try {
await this.maybeGiveDailyCredits(member.id);
} catch (error) {
console.error(`Failed to give daily credits to member ${member.id}:`, error);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make daily credit claiming atomic and idempotent. await does not fix the duplicate-payout race.

The comment on Lines [363]-[367] is not correct for the current implementation. await only waits for the current login() call. Two concurrent calls can still enter maybeGiveDailyCredits together.

That method checks the timestamp at Line [388], then creates the wallet transaction at Line [398] and updates the member at Lines [399]-[402] in separate operations. Both calls can pass the check and create duplicate payouts. If the transaction succeeds and the member update fails, the catch at Lines [374]-[376] can also allow a later retry to pay again.

Wrap the credit claim and writes in one database transaction. Use a conditional claim or a unique (memberId, creditDate, creditType) guard. Add concurrent-login and partial-failure retry tests.

#!/bin/bash
set -euo pipefail

# Verify whether repository methods already enforce an atomic daily-credit guard.
rg -n -C 12 \
  'maybeGiveDailyCredits|createDailyCreditTransaction|last_daily_login_credit|creditDate|daily.*credit' \
  api --glob '*.ts'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/src/services/member/member.service.ts` around lines 363 - 376, Make
maybeGiveDailyCredits atomic and idempotent: execute the daily-credit
eligibility check, payout creation, and member timestamp update within one
database transaction, using a conditional claim or unique (memberId, creditDate,
creditType) constraint to prevent concurrent duplicate payouts and allow safe
retries after partial failures. Update the login flow’s comments to reflect the
transactional behavior, and add tests covering concurrent logins and retrying
after a failed transaction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant