Skip to content

fix(user): make getNextRef Postgres-compatible#3749

Closed
Blume1977 wants to merge 1 commit into
developfrom
fix/user-ref-postgres-regex
Closed

fix(user): make getNextRef Postgres-compatible#3749
Blume1977 wants to merge 1 commit into
developfrom
fix/user-ref-postgres-regex

Conversation

@Blume1977
Copy link
Copy Markdown
Collaborator

Summary

UserRepository.getNextRef queries the highest existing user ref:

where: { ref: Like('%[0-9]-[0-9]%') }

[0-9] is an MSSQL character class. On Postgres it is a literal sequence of five characters ([, 0, -, 9, ]), so the query matches no rows. findOne returns null, and the chained .then((u) => +u.ref.replace('-', '') + 1) throws TypeError: Cannot read properties of null (reading 'ref').

Since #3746 deployed develop → main (2026-05-22 ~01:06 UTC), every caller of UserRepository.setUserRef on a userData with kycLevel >= 50 has been crashing in production:

  • UserService.createUser (user.service.ts:307)
  • UserService.updateUser with setRef:true (user.service.ts:402)
  • UserDataService.updateKyc (user-data.service.ts:351)
  • UserDataService.updatePersonalData (user-data.service.ts:541)
  • UserDataService.mergeUserData activation loop (user-data.service.ts:1363/1367)

The merge path is especially user-visible: the V8 TypeError text leaks to clients via ApiExceptionFilter (shared/filters/exception.filter.ts:25) and the DFX-Services frontend displays it verbatim on services.dfx.swiss/account-merge?otp=…. Because mergeUserData is non-transactional and the crash fires after userDataRepo.save(master), the merge leaves a half-committed state (new user attached, ref=NULL, AccountMerge is_completed=false).

Change

Single file, single function. Swap MSSQL character-class LIKE for a Postgres POSIX regex on the column, and add a null-safe fallback for the empty-result case.

where: { ref: Raw((alias) => `${alias} ~ '[0-9]-[0-9]'`) }

const nextRef = user?.ref ? +user.ref.replace('-', '') + 1 : 1;

The grep of the codebase confirms this is the only remaining MSSQL-LIKE-character-class pattern after #3745 cleaned up the boolean comparisons.

Test plan

  • npx prettier --write — no formatting changes
  • npx eslint — no warnings
  • npx tsc --noEmit — clean
  • npx jest src/subdomains/generic/user/ — 33/33 pass
  • npx jest (full suite) — 926 passed, 106 skipped, 0 failed
  • Post-deploy on DEV: trigger an email-merge with a KYC50+ master and a fresh wallet slave, verify the merge completes and getNextRef produces a sane next ref
  • Post-deploy on DEV: verify a fresh KYC level 50 promotion succeeds end-to-end

Cherry-pick path

Production is currently broken for KYC50+ operations. Once this lands on develop, please cherry-pick or release-roll to main ASAP. The change is one file, no migration, no env change, no DTO/contract change — safe to fast-track.

Follow-ups (out of scope here, separate PRs)

  1. ApiExceptionFilter info-disclosureshared/filters/exception.filter.ts:25 passes raw exception.message to clients on 500s. Should return a generic message and log the detail server-side.
  2. mergeUserData transactional — wrap the whole sequence in a TypeORM transaction so future mid-merge crashes don't leave half-committed state (slave MERGED + master partially updated + new user without ref + AccountMerge not completed).
  3. App re-entry to POST /v1/realunit/register/wallet — existing-DFX-customers who hit a failed merge get routed by KycCubit to the empty new-user registration form (_registrationSignProduced=false + no Wallet-Status check). They should land on a re-sign screen that calls register/wallet with the existing account data.

Cleanup for already-affected accounts

Customers who hit the crash since 01:06 UTC have a partial state. Per-account fix-up:

-- 1. assign a ref code
WITH next AS (
  SELECT MAX(CAST(REPLACE(ref, '-', '') AS INTEGER)) + 1 AS n
  FROM "user" WHERE ref ~ '[0-9]-[0-9]'
)
UPDATE "user"
SET ref = LPAD((SELECT n FROM next)::text, 6, '0')
WHERE id = <new_user_id> AND ref IS NULL;

UPDATE "user"
SET ref = SUBSTRING(ref, 1, 3) || '-' || SUBSTRING(ref, 4, 3)
WHERE id = <new_user_id> AND ref !~ '-';

-- 2. close the AccountMerge record
UPDATE account_merge SET is_completed = true WHERE id = <merge_id>;

Then trigger POST /v1/realunit/register/wallet from the affected wallet to create the per-wallet REALUNIT_REGISTRATION step so Buy is unlocked.

The `LIKE '%[0-9]-[0-9]%'` filter in UserRepository.getNextRef relied on
MSSQL character-class syntax. On Postgres, `[0-9]` is a literal 5-char
sequence, so the query matched no rows. findOne returned null and the
chained `.then((u) => +u.ref.replace(...) + 1)` threw a TypeError every
time setUserRef ran for a user without a ref on a userData with
kycLevel >= 50.

Since develop->main released the PSQL migration to prod, this has been
crashing every caller of setUserRef: new user creation on KYC50+
userData, admin updateUser with setRef=true, KYC level bumps to >= 50,
updatePersonalData on KYC50+ userData, and the mergeUserData activation
loop. The merge case is especially visible because the V8 TypeError text
reaches end users via ApiExceptionFilter and the DFX-Services frontend.

Replace the MSSQL LIKE with a Postgres POSIX regex on the column, and
guard the deref so a future empty-result state does not reintroduce the
same crash shape.
@Blume1977 Blume1977 marked this pull request as ready for review May 22, 2026 09:27
@github-actions
Copy link
Copy Markdown

⚠️ Unverified Commits (1)

The following commits are not signed/verified:

  • 9d78816 fix(user): make getNextRef Postgres-compatible (Blume1977)
How to sign commits
# SSH signing (recommended)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

# Re-sign last commit
git commit --amend -S --no-edit
git push --force-with-lease

@davidleomay
Copy link
Copy Markdown
Member

Duplicate of #3748

@davidleomay davidleomay marked this as a duplicate of #3748 May 22, 2026
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.

2 participants