Skip to content

refactor: server-side QR extraction for institution submissions#560

Merged
khrnchn merged 3 commits intokhrnchn:mainfrom
faizkhairi:refactor/server-side-qr-extraction
Mar 24, 2026
Merged

refactor: server-side QR extraction for institution submissions#560
khrnchn merged 3 commits intokhrnchn:mainfrom
faizkhairi:refactor/server-side-qr-extraction

Conversation

@faizkhairi
Copy link
Copy Markdown
Contributor

@faizkhairi faizkhairi commented Mar 23, 2026

Addresses #553

Summary

Moves QR code decoding from browser-only (canvas + @zxing/browser) to server-side (sharp + @zxing/library) for the institution submission flow. Server-side extraction is more reliable because sharp preserves full image fidelity without browser rendering variance or canvas compression artifacts.

What changed

File Change
lib/qr-decode.ts New -- shared server-side QR decode utility extracted from admin tool
submit-institution.ts Server-side extraction after R2 upload, fallback to client value, duplicate check, persist qrContent in DB
upload-qr-replacement.ts Use shared decodeQrFromBuffer() instead of inline decode (37 lines -> 1 line)

How it works

  1. User uploads QR image (client-side compression still runs -- saves bandwidth)
  2. Server receives image, uploads to R2
  3. New: Server decodes QR from the image buffer using sharp + @zxing/library
  4. Falls back to client-provided qrContent if server extraction fails (belt-and-suspenders during transition)
  5. Runs duplicate QR check with the final extracted content
  6. Inserts qrContent into DB (previously only used for payment type detection, not persisted)

What stays the same

  • Client-side QR extraction still runs (provides instant feedback to users)
  • File upload UI unchanged
  • Graceful degradation: submission succeeds even if QR extraction fails on both client and server (admin handles manually)

Follow-up opportunities

The client-side extraction code (useQrExtractionLazy, QRProcessor, QRExtractionFeature) can be simplified in a follow-up PR now that the server handles extraction. These are shared across multiple features (main form, edit sheet, quest form, admin replacement), so removing them requires updating all consumers:

  • hooks/use-qr-extraction-lazy.ts -- used by quest form + admin replacement upload
  • app/(user)/contribute/_components/qr-processor.tsx -- bridge component
  • app/(user)/contribute/_components/qr-extraction-feature.tsx -- upload UI + extraction feedback
  • @zxing/browser can be removed from package.json once all consumers migrate

Test plan

  • Submit new institution with clear QR image -- verify qrContent is populated in DB
  • Submit with image that has no QR code -- verify submission succeeds with qrContent = null
  • Submit duplicate QR -- verify server-side duplicate check catches it
  • Admin QR replacement still works with shared utility
  • ToyyibPay payment detection still works

Summary by CodeRabbit

  • Refactor

    • Centralized QR decoding into a shared utility used by institution upload and submission flows.
  • Improvements

    • Server-side QR decoding with fallback to submitted QR text when needed.
    • Duplicate QR checks now run before uploading images to avoid orphaned uploads.
    • Clearer handling when no QR is found (stored as null) and more consistent error logging.

Open with Devin

Move QR code decoding from browser-only to server-side using
sharp + @zxing/library. This is more reliable than client-side canvas
decoding, which fails on compressed images due to binarization issues.

Changes:
- Create lib/qr-decode.ts: shared server-side QR decode utility
  extracted from the admin QR replacement tool
- Update submit-institution.ts: decode QR from image buffer after
  R2 upload, with fallback to client-provided value during transition
- Update upload-qr-replacement.ts: use shared utility instead of
  inline decode (DRY)
- Add server-side duplicate QR check after extraction
- Persist qrContent in DB insert (was previously only used for
  payment type detection)

Addresses khrnchn#553
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Shared QR decoding was extracted to lib/qr-decode.ts. Admin upload and user submit flows now call decodeQrFromBuffer(buffer) for server-side QR extraction; submit flow reorders duplicate-checking and R2 upload and stores explicit null when no QR content is found.

Changes

Cohort / File(s) Summary
Shared QR Decoding Utility
lib/qr-decode.ts
New exported `decodeQrFromBuffer(buffer): Promise<string
Admin QR Upload Refactoring
app/(admin)/admin/institutions/_lib/upload-qr-replacement.ts
Removed inline RGBA→RGB and ZXing usage; replaced with call to shared decodeQrFromBuffer; local decode error handling removed.
User Contribution QR Processing
app/(user)/contribute/_lib/submit-institution.ts
Added server-side decode via decodeQrFromBuffer; fallback to trimmed form qrContent if decode fails; moved duplicate-check to run after determining final qrContent and before R2 upload; insert uses `qrContent: qrContent

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client (browser/form)
  participant Server as Server (submit / admin endpoints)
  participant QR as decodeQrFromBuffer
  participant DB as Database (institutions)
  participant R2 as R2 Storage

  Client->>Server: POST form + optional QR image
  Server->>QR: decodeQrFromBuffer(imageBuffer)
  QR-->>Server: decodedText|null
  alt decodedText present
    Server->>DB: query for existing institution by decodedText
  else decodedText null
    Server->>Server: use trimmed client qrContent (if any)
    Server->>DB: query for existing institution by final qrContent
  end
  alt duplicate not found
    Server->>R2: upload QR image
    R2-->>Server: upload URL/metadata
    Server->>DB: insert institution (qrContent || null, r2 metadata...)
    DB-->>Server: insert result
    Server-->>Client: success
  else duplicate found
    Server-->>Client: error (duplicate)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through pixels, sharp and bright,
I chased the codes by soft moonlight,
One shared decode to rule them all,
From admin upload to submit call,
Hooray — no duplicates to spook the night! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: server-side QR extraction for institution submissions' accurately describes the main change: moving QR decoding from client-side to server-side, which is the primary focus of all three modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

coderabbitai[bot]

This comment was marked as resolved.

- Add .toColorspace("srgb") before .ensureAlpha() to force 3-channel
  RGB output for grayscale inputs (which would otherwise produce
  2-channel GA data, corrupting the RGBA-to-RGB pixel copy loop)
- Add channel count guard before pixel processing
- Use arrow function per project convention

Addresses CodeRabbit review feedback on khrnchn#560.
@faizkhairi
Copy link
Copy Markdown
Contributor Author

Add pre-existing bug fix found by coderabbitai

@khrnchn
Copy link
Copy Markdown
Owner

khrnchn commented Mar 24, 2026

Add pre-existing bug fix found by coderabbitai

thanks for this! will look into it in the morning

- Move QR decoding and duplicate check before R2 upload to prevent
  orphaned objects in storage when duplicates are rejected
- Remove redundant early client-side duplicate check (server-side
  extraction is now authoritative)
- Add console.warn to QR decode catch block for observability
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(user)/contribute/_lib/submit-institution.ts:
- Around line 293-311: Add a DB-level unique constraint on
institutions.qrContent to prevent race conditions: update the Drizzle schema for
the institutions table to mark qrContent as .unique() (or run an ALTER TABLE ...
ADD UNIQUE(qr_content) migration) and create/apply the migration; keep the
existing application-level duplicate check around qrContent but also catch
unique-constraint violations on insert (the code that writes the institution
record) and translate that DB error into the same return payload used when an
existing QR is detected so concurrent inserts return the same user-facing error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1284187-5ddc-4f00-b623-6fa86fa21aff

📥 Commits

Reviewing files that changed from the base of the PR and between 09cee89 and 0e2510c.

📒 Files selected for processing (2)
  • app/(user)/contribute/_lib/submit-institution.ts
  • lib/qr-decode.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/qr-decode.ts

Comment on lines +293 to +311
// --- Duplicate QR check (before R2 upload to avoid orphaned objects)
if (qrContent) {
const [existingQr] = await db
.select({ id: institutions.id })
.from(institutions)
.where(eq(institutions.qrContent, qrContent))
.limit(1);

if (existingQr) {
return {
status: "error",
errors: {
general: [
"QR code ini telah pun wujud dalam sistem. Sila semak semula.",
],
},
};
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if institutions table has a unique constraint/index on qrContent
fd -t f -e ts 'institutions' --exec grep -l 'qrContent' {} | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'

# Look for unique constraint or index definition
rg -n 'unique|index' -A2 -B2 --type ts | grep -i 'qrContent' -A2 -B2

Repository: khrnchn/sedekah-je

Length of output: 50375


Add unique constraint on institutions.qrContent to prevent race conditions.

The duplicate check (lines 295-299) runs before the database insert, but there's a window where concurrent requests could insert the same qrContent, bypassing the application-level check. The qrContent field currently lacks a database-level unique constraint. Adding one will prevent duplicate entries even in race conditions:

ALTER TABLE institutions ADD UNIQUE(qr_content);

Or in the Drizzle schema definition, update the field to include .unique().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(user)/contribute/_lib/submit-institution.ts around lines 293 - 311, Add
a DB-level unique constraint on institutions.qrContent to prevent race
conditions: update the Drizzle schema for the institutions table to mark
qrContent as .unique() (or run an ALTER TABLE ... ADD UNIQUE(qr_content)
migration) and create/apply the migration; keep the existing application-level
duplicate check around qrContent but also catch unique-constraint violations on
insert (the code that writes the institution record) and translate that DB error
into the same return payload used when an existing QR is detected so concurrent
inserts return the same user-facing error.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@khrnchn khrnchn merged commit d2d625b into khrnchn:main Mar 24, 2026
2 checks passed
@khrnchn
Copy link
Copy Markdown
Owner

khrnchn commented Mar 24, 2026

lgtm thank you sir

@faizkhairi
Copy link
Copy Markdown
Contributor Author

faizkhairi commented Mar 24, 2026

current project sedang develop (kerja hakiki) pun using the same approach (but more complex 💀 for no reason). Anyway, happy to help project sedekah-je 🎉 😄

@khrnchn
Copy link
Copy Markdown
Owner

khrnchn commented Mar 24, 2026

current project sedang develop (kerja hakiki) pun using the same approach (but more complex 💀 for no reason). Anyway, happy to help project sedekah-je 🎉 😄

kim salam akif dengan aimand hahaha

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