Skip to content

Feature/time decay reputation#481

Closed
shantanushok wants to merge 23 commits into
roshankumar0036singh:mainfrom
shantanushok:feature/time-decay-reputation
Closed

Feature/time decay reputation#481
shantanushok wants to merge 23 commits into
roshankumar0036singh:mainfrom
shantanushok:feature/time-decay-reputation

Conversation

@shantanushok
Copy link
Copy Markdown
Contributor

@shantanushok shantanushok commented May 31, 2026

Description

This PR introduces the Time-Decay Reputation System, and resolves all recent merge conflicts with upstream/main.

Key Changes:

  1. Time-Decay Reputation Logic (reputation.ts): Replaced flat reputation points with a time-decay model. Recent event attendance awards more points than older attendances (e.g., 6 months old = 50% points).
  2. Atomic Firestore Updates: Refactored the backfill loops and Firestore triggers to use FieldValue.increment() and runTransaction(). This ensures background backfills do not overwrite or clobber real-time trigger writes.
  3. Emulator Testing Suite: Wrote comprehensive unit tests (reputation.test.ts) that run against the Firestore emulator to verify decay math, fallback logic, and Firestore trigger correctness.
  4. ESLint v10 & Flat Config Migration: Upgraded the project to eslint@10 and migrated away from deprecated legacy configs to eslint.config.mjs.
  5. Upstream Sync: Resolved package-lock.json and file conflicts from the latest upstream bumps.

Fixes # 334

Type of change

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

The core reputation math and Firestore triggers were verified using the local Firebase Emulator Suite. I also ran local builds and linting to guarantee the ESLint v10 upgrade works smoothly.

  • Emulator Tests (npm run test): Verified that a 6-month old event correctly yields a 50% reputation reward, and a 12-month event yields 25%.
  • Trigger Testing: Validated onParticipatingCreate securely increments bucket totals via atomic operations.
  • TypeScript Build: Confirmed tsc compiles successfully across cloud-functions.
  • Test file: reputation.test.ts

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Summary by CodeRabbit

  • New Features

    • Added /api/certificate secure download endpoint
    • Time-decayed reputation system with monthly buckets
    • Event start timestamp now persisted across registrations, payments, and check‑ins
  • Bug Fixes

    • Certificate links use short-lived authenticated URLs
    • Daily digest and other flows now paginate to support large user bases
    • Early-bird ticket detection fixed to be case-insensitive
  • Improvements

    • Batched push/notification processing and offline check‑in sync reliability
    • Rate limiting applied to sensitive endpoints
    • Certificate delivery logic refined
  • Documentation

    • Updated data model and reputation system docs

Copilot AI review requested due to automatic review settings May 31, 2026 08:32
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 31, 2026

Review Change Stack

Warning

Review limit reached

@shantanushok, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 14 minutes and 39 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7424d476-0f3d-4158-b538-3ce6456398ca

📥 Commits

Reviewing files that changed from the base of the PR and between 563a0d6 and 47f6102.

⛔ Files ignored due to path filters (1)
  • cloud-functions/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • app/src/lib/checkInService.js
  • app/src/screens/EventDetail.js
  • package.json
📝 Walkthrough

Walkthrough

This PR refactors the reputation system from simple linear scoring to a time-decay bucket model with monthly aggregation, adds event start timestamps across client and backend, hardens certificate distribution with API-based authorization, introduces pagination-based batch processing for scalability, and migrates to ESLint flat configuration. Changes span app screens, check-in service, Cloud Functions, tests, and documentation.

Changes

Time-Decayed Reputation System Refactor

Layer / File(s) Summary
Reputation bucket foundation and date parsing
cloud-functions/lib/reputation.js, cloud-functions/src/reputation.ts
Adds toDate parser for Firestore Timestamps and ISO strings, introduces getMonthKeyFromDate and getMonthStartFromKey month utilities, and implements resolveEventStartAt to fetch authoritative event start dates from Firestore with client-spoofing detection and caching.
Idempotent bucket updates with transaction deduplication
cloud-functions/lib/reputation.js, cloud-functions/src/reputation.ts
Implements updateBucket to apply deltas to monthly reputationBuckets via Firestore atomic increments with transaction-backed idempotency: processedTriggers/{idempotencyKey} marker documents prevent duplicate trigger processing and expire after 30 days.
Reputation refresh and leaderboard updates
cloud-functions/lib/reputation.js, cloud-functions/src/reputation.ts
Adds runReputationRefresh to paginate users and recompute points from monthly buckets using half-life decay, introduces refreshReputationDaily scheduled function, adds shared handleReputationTrigger handler for participating/check-in/reminder triggers, updates leaderboard queries to use documentId() as stable tie-breaker with server timestamps.
Backfill and callable return payload updates
cloud-functions/lib/reputation.js, cloud-functions/src/reputation.ts
Refactors backfillReputationBuckets to scan historical data via pagination with event start caching, aggregate deltas into monthly buckets, batch-write bucket documents, and return { updatedBuckets, scanned } instead of user-count messaging.
Reputation tests with Firebase emulator
cloud-functions/src/reputation.test.ts
Rewrites tests to use Firebase/Firestore emulator and firebase-functions-test, adds deterministic month-start helper, seeds bucket documents and runs runReputationRefresh integration tests, verifies resolveEventStartAt fallback and trigger idempotency.

Event Start Timestamp Propagation

Layer / File(s) Summary
Check-in service timestamp handling
app/src/lib/checkInService.js
Updates checkInAttendee to compute eventStartAt from ticket dates and add to check-in payload; offline sync accepts event startAt, resolves timestamp from item or event fallback, wraps getDoc in error handling, and fetches event document to supply startAt to per-item sync.
Event detail screen RSVP and reminder timestamps
app/src/screens/EventDetail.js
Adds eventStartAt: event.startAt to reminder and RSVP registration documents; updates early-bird detection to use case-insensitive regex; adjusts FAB button logic for certificate download gating and paid-event pricing.
Registration and payment screen timestamp addition
app/src/screens/EventRegistrationFormScreen.js, app/src/screens/PaymentScreen.js
Adds eventStartAt: freshEvent.startAt to user participating documents during unpaid event registration and ticket booking transactions.

Certificate Service Security and API Hardening

Layer / File(s) Summary
Certificate service helpers and URL generation
cloud-functions/lib/certificateService.js, cloud-functions/src/certificateService.ts
Adds getParticipantId, getErrorMessage, and sanitizeFilename helpers; refactors uploadPdfAndGetUrl to return API-based download URL (/api/certificate?eventId=...&participantId=...) instead of signed Storage URL; updates processParticipant call site.
Certificate skipped status handling
cloud-functions/lib/certificateService.js, cloud-functions/src/certificateService.ts
Updates sendCertificatesForEvent completion condition to accept status: 'skipped' alongside success before setting certificatesSent and certificatesSentAt.
Certificate API endpoint with authorization
cloud-functions/lib/server.js, cloud-functions/src/server.ts
Adds new GET /api/certificate endpoint to validate query params, fetch participant metadata, reject missing or revoked certificates, check PDF existence in Storage, generate 15-minute signed read URL, and redirect with centralized error handling.

Scalability: Pagination and Batching

Layer / File(s) Summary
Event creation notification pagination
cloud-functions/lib/onEventCreate.js
Refactors onEventCreate to define processUserBatch helper that paginates users by pushToken, writes per-user notification documents in Firestore batches, sends accumulated push messages, and returns pagination state; replaces one-shot fetch-all with iterative batch loop.
Daily digest batched processing and helper extraction
cloud-functions/lib/dailyDigest.js, cloud-functions/src/dailyDigest.ts
Introduces getTodayEventCount(db) helper to compute today's event count via Firestore query; updates sendDailyDigest to enforce App Check and call the helper; refactors daily digest post handler to paginate users by document ID, filter by digestOptIn === false, write notification batches, and send accumulated pushes.
Event notifications user document batching
cloud-functions/lib/eventNotifications.js, cloud-functions/src/eventNotifications.ts
Updates buildMessagesForEvent to fetch participant user documents in batches of 90 using db.getAll(...) instead of parallel individual .get() calls.
Server pagination and API endpoints
cloud-functions/lib/server.js, cloud-functions/src/server.ts
Updates Firebase Admin conditional initialization; adds rate limiting and push/digest helpers; implements validateFirebaseIdToken and rateLimitMiddleware; refactors /api/sendDailyDigest with pagination and batched push sending.

Admin Security and Rate Limiting

Layer / File(s) Summary
Set role App Check and rate limiting
cloud-functions/lib/setRole.js
Updates setRole callable to enforce App Check, apply checkRateLimit for ADMIN_WRITE, and throw internal error without exposing original error details.
Rate limit cleanup scheduler
cloud-functions/lib/index.js
Adds cleanupRateLimits scheduled Cloud Function (hourly pub/sub) that calls cleanupOldRateLimits middleware; exports via barrel exports including new permanentCleanup module.

Tooling, Configuration, and Documentation

Layer / File(s) Summary
ESLint flat configuration migration
.eslintrc.json (removed), cloud-functions/.eslintrc.json (removed), cloud-functions/eslint.config.mjs, eslint.config.mjs
Removes old .eslintrc.json files; adds cloud-functions/eslint.config.mjs with flat config for TypeScript parsing, Node/Jest globals, and baseline rules; adds eslint.config.mjs for root using FlatCompat to bridge legacy Expo and Prettier configs.
Package dependency updates
cloud-functions/package.json, package.json
Updates ESLint and TypeScript-related dependencies to newer versions; removes eslint-config-google; adds @typescript-eslint/eslint-plugin, @typescript-eslint/parser, and eslint-config-expo.
Data model and features documentation
docs/DATA_MODEL.md, docs/FEATURES.md, cloud-functions/lib/analyzeAttendance.js, cloud-functions/src/analyzeAttendance.ts, cloud-functions/src/postEventFeedback.ts
Updates data model to show reputation as object with buckets subcollection; replaces "Points System" with "Time-Decayed Reputation System" in features; adds JSDoc comments to fraud detection and feedback helpers.
Walkthrough and verification
walkthrough.md
Documents reputation/performance fixes, pagination-based processing, trigger idempotency, push notification error handling, certificate signed-URL security, and test stabilization with emulator-based verification notes.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Suggested labels: level:intermediate, type:refactor, type:design, type:testing, quality:clean

Suggested reviewers:

  • roshankumar0036singh

"I threaded timestamps through each tale and trail,
Buckets of points that slowly fade like a snail,
Certificates guarded with a short‑lived key,
Notifications batched, scaled across the sea,
A rabbit hops, code polished — ship it with a tail!"

🚥 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 'Feature/time decay reputation' directly summarizes the main change—implementing a time-decay reputation system—and is consistent with the PR's primary objective.
Docstring Coverage ✅ Passed Docstring coverage is 92.45% which is sufficient. The required threshold is 80.00%.
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.

✏️ 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.

Copy link
Copy Markdown
Contributor

@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: 9

🧹 Nitpick comments (11)
app/src/screens/EventRegistrationFormScreen.js (1)

143-147: 💤 Low value

Validate freshEvent.startAt before writing to participating document.

Ensure the startAt field is defined before writing to Firestore to prevent undefined from being stored, which could cause issues in reputation bucketing logic that relies on event timestamps.

♻️ Optional enhancement
                 transaction.set(participatingRef, {
                     eventId: event.id,
-                    eventStartAt: freshEvent.startAt,
+                    ...(freshEvent.startAt && { eventStartAt: freshEvent.startAt }),
                     joinedAt: new Date().toISOString(),
                 });
🤖 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 `@app/src/screens/EventRegistrationFormScreen.js` around lines 143 - 147,
Before calling transaction.set on participatingRef, verify freshEvent.startAt is
defined and valid; if it's undefined/invalid, abort the transaction (throw) or
set an explicit fallback (e.g., use event.startAt or current timestamp) instead
of writing undefined. Update the logic around transaction.set(participatingRef,
{...}) to perform this check for freshEvent.startAt and either throw a
descriptive error to halt the transaction or substitute a safe timestamp value
so the participating document never stores undefined.
app/src/screens/EventDetail.js (2)

512-517: 💤 Low value

Validate eventData.startAt exists before writing to participating document.

Similar to the reminder document, ensure startAt is defined before writing it to avoid storing undefined in Firestore, which could break reputation bucketing downstream.

♻️ Optional enhancement
                 transaction.set(userRef, {
                     eventId: eventId,
-                    eventStartAt: eventData.startAt,
+                    ...(eventData.startAt && { eventStartAt: eventData.startAt }),
                     joinedAt: new Date().toISOString(),
                 });
🤖 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 `@app/src/screens/EventDetail.js` around lines 512 - 517, The transaction
writes eventData.startAt into the participating user document without checking
it, which can store undefined; before calling transaction.set for userRef (and
when building participantPayload if applicable), check that eventData.startAt is
defined and only include eventStartAt in the object when it exists (or
explicitly set it to null/default) to avoid writing undefined to Firestore;
update the transaction.set(userRef, {...}) call to conditionally add
eventStartAt based on eventData.startAt (referencing transaction.set, userRef,
participantPayload, eventData.startAt, and eventId).

403-411: 💤 Low value

Consider validating event.startAt before writing to Firestore.

While events should always have a startAt field, writing undefined to Firestore when it's missing could cause issues in reputation bucketing logic that depends on this timestamp. A defensive check would prevent data integrity issues.

♻️ Optional enhancement
                 const docRef = await addDoc(collection(db, 'reminders'), {
                     userId: user.uid,
                     eventId: event.id,
                     eventTitle: event.title,
-                    eventStartAt: event.startAt,
+                    ...(event.startAt && { eventStartAt: event.startAt }),
                     remindAt: new Date(new Date(event.startAt).getTime() - 10 * 60000), // 10 mins before
                     notificationId: notifId,
                     createdAt: new Date().toISOString(),
                 });

This pattern conditionally includes the field only when it exists.

🤖 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 `@app/src/screens/EventDetail.js` around lines 403 - 411, The code is writing
event.startAt and a derived remindAt without validating it; update the block
that calls addDoc(collection(db, 'reminders'), ...) to first check event.startAt
and handle the missing case: either abort/log/throw and avoid creating the
reminder (use process/console logger and return) or build the payload so
eventStartAt and remindAt are only included when event.startAt is a valid date
(i.e., check typeof event.startAt and !isNaN(new Date(event.startAt))). Ensure
the notificationId/other fields (notifId, user.uid, event.id, event.title,
createdAt) are still persisted as appropriate and that remindAt calculation only
runs when event.startAt is valid.
app/src/screens/PaymentScreen.js (1)

198-205: 💤 Low value

Validate freshEvent.startAt before writing to participating document.

Ensure the startAt field exists before writing it to Firestore to avoid storing undefined, which could break downstream reputation bucketing that depends on event timestamps.

♻️ Optional enhancement
                 transaction.set(participatingRef, {
                     eventId: event.id,
-                    eventStartAt: freshEvent.startAt,
+                    ...(freshEvent.startAt && { eventStartAt: freshEvent.startAt }),
                     joinedAt: new Date().toISOString(),
                     role: 'attendee',
                     ticketId: ticketRef.id,
                     status: 'paid',
                 });
🤖 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 `@app/src/screens/PaymentScreen.js` around lines 198 - 205, The participate
write is storing freshEvent.startAt directly which can be undefined; before
calling transaction.set(participatingRef, ...) validate freshEvent.startAt and
only write a safe value (e.g., use a normalized timestamp or null) or omit the
eventStartAt field when absent to avoid persisting undefined. Locate the
transaction.set call that writes the participating document and change the
payload construction so it checks freshEvent.startAt (from freshEvent) and
assigns a fallback or skips the key if undefined, ensuring downstream reputation
bucketing always sees a defined timestamp or null.
app/src/lib/checkInService.js (1)

473-474: ⚡ Quick win

Wrap event fetch in error handling for robustness.

If the event document doesn't exist or the fetch fails (e.g., network error, permissions), eventStartAt will be null and sync will continue. While the fallback chain in syncOfflineCheckInItem handles this, logging the error would aid debugging and surface potential data issues.

♻️ Proposed enhancement
-        const eventSnap = await getDoc(doc(db, 'events', eventId));
-        const eventStartAt = eventSnap.exists() ? eventSnap.data()?.startAt : null;
+        let eventStartAt = null;
+        try {
+            const eventSnap = await getDoc(doc(db, 'events', eventId));
+            eventStartAt = eventSnap.exists() ? eventSnap.data()?.startAt : null;
+        } catch (err) {
+            logger.warn('Could not fetch event for offline sync, continuing without event-level startAt:', err);
+        }
🤖 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 `@app/src/lib/checkInService.js` around lines 473 - 474, The event fetch using
getDoc(doc(db, 'events', eventId)) should be wrapped in a try/catch so failures
(network/permission) are logged rather than silently setting eventStartAt to
null; inside the catch log the error (use the module's logger or console) with
context including eventId and function name (syncOfflineCheckInItem or the
surrounding function) and then set eventStartAt to null so existing fallback
behavior remains; update the code around eventSnap/eventStartAt to reflect this
error handling while keeping the rest of syncOfflineCheckInItem logic unchanged.
cloud-functions/src/reputation.test.ts (1)

94-105: 💤 Low value

Test context missing eventId for idempotency key.

The wrapped trigger uses context.eventId to build the idempotency key. The test context only provides params but not eventId, resulting in an idempotency key of participating_create_undefined. Consider providing eventId to more accurately simulate production behavior and enable idempotency testing.

🧪 Proposed fix to add eventId
         const wrapped = testEnv.wrap(onParticipatingCreate);
-        await wrapped(snap, { params: { userId: 'u1', eventId: 'e1' } });
+        await wrapped(snap, { 
+            params: { userId: 'u1', eventId: 'e1' },
+            eventId: 'test-event-id-123'
+        });
🤖 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 `@cloud-functions/src/reputation.test.ts` around lines 94 - 105, The test for
onParticipatingCreate is missing context.eventId so the idempotency key becomes
"participating_create_undefined"; update the test to pass an eventId in the
wrapped invocation (e.g., when calling wrapped(snap, { params: { userId: 'u1',
eventId: 'e1' } }) include an eventId in the second argument's context/eventId
field) so the code that builds the idempotency key (using context.eventId) is
exercised correctly and idempotency behavior can be asserted.
cloud-functions/src/reputation.ts (2)

535-544: 💤 Low value

Backfill is additive, not idempotent.

Using FieldValue.increment() with merge: true means running this backfill multiple times will accumulate values (e.g., running twice doubles all counts). Consider either:

  1. Adding a processedBackfill marker to skip already-processed records, or
  2. Documenting that buckets should be cleared before re-running backfill.
🤖 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 `@cloud-functions/src/reputation.ts` around lines 535 - 544, The backfill
currently uses admin.firestore.FieldValue.increment(...) with batch.set(..., {
merge: true }) on bucketRef (using bucket.monthKey, bucket.registrations,
bucket.attendances, bucket.reminders), which makes the operation non-idempotent;
modify the backfill to be safe by adding a processed marker or making writes
idempotent: add a processedBackfill flag (e.g., processedBackfill: true or
processedAt timestamp) to each bucket document and have the routine skip
documents that already have that marker, or compute and write absolute counts
instead of using FieldValue.increment so repeated runs won’t accumulate—ensure
the logic around batch.set and bucketRef checks this marker before applying
increments/updates.

188-240: ⚖️ Poor tradeoff

Consider throttling parallel subcollection reads.

The current implementation fires up to 500 concurrent .get() calls for user reputation buckets within each page. Under high load, this may exceed Firestore rate limits or cause memory pressure. Consider batching the reads (e.g., process 50-100 users at a time within each page) or using a semaphore to limit concurrency.

🤖 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 `@cloud-functions/src/reputation.ts` around lines 188 - 240, The code spawns up
to 500 concurrent subcollection reads inside paginateQuery by using Promise.all
over docs; replace that with a bounded-concurrency approach to avoid Firestore
rate limits — either process docs in fixed-size chunks (e.g., 50–100) or use a
semaphore/promise limiter (p-limit) to cap concurrent calls to
userDoc.ref.collection(REPUTATION_BUCKETS_COLLECTION).get(); keep the same
aggregation logic (attendanceCount, registrationCount, remindersSet, points) and
the batch.update calls but perform them as each chunk completes, and only call
batch.commit() after each chunk (or when batch size approaches limits) to avoid
large in-memory batches; update the section around Promise.all(docs.map(...)) to
iterate in controlled concurrency instead.
cloud-functions/lib/server.js (1)

187-232: ⚖️ Poor tradeoff

Consider whether authentication is needed for the certificate endpoint.

The /api/certificate endpoint is publicly accessible without authentication. While the endpoint validates that the certificate exists and isn't revoked, anyone who knows (or guesses) an eventId and participantId can access the certificate.

The walkthrough.md mentions "strict authorization" for this endpoint. If the intent is that only the certificate holder should access it, consider adding authentication. However, if the security model relies on the obscurity of participantId values plus revocation capability, the current approach may be acceptable.

🤖 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 `@cloud-functions/lib/server.js` around lines 187 - 232, The /api/certificate
handler currently allows public access; update it to enforce authentication and
owner-only access by adding an auth check (e.g., middleware like
authenticateRequest or inline use of admin.auth().verifyIdToken on the
Authorization header) inside the app.get('/api/certificate', ...) flow before
fetching the participant document, and then validate that the authenticated
user's uid equals participantId or that the user has an admin/organizer role in
the event document; if the token is missing/invalid or the user is not
authorized, return 401/403 respectively while preserving the existing
existence/revocation/storage checks and signed-url generation logic.
cloud-functions/src/certificateService.ts (1)

135-137: 💤 Low value

JSDoc comment is misleading.

The docstring says "returns its signed URL" but the function now returns an API-based download URL (/api/certificate?...), not a signed Storage URL. Consider updating the documentation to reflect the actual behavior.

📝 Proposed fix
-/**
- * Uploads a generated PDF certificate buffer to Firebase Storage and returns its signed URL.
- */
+/**
+ * Uploads a generated PDF certificate buffer to Firebase Storage and returns an API download URL.
+ */
🤖 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 `@cloud-functions/src/certificateService.ts` around lines 135 - 137, The JSDoc
above the certificate upload function incorrectly states it "returns its signed
URL"; update that docstring for the function that uploads the PDF buffer (the
function with the current comment "Uploads a generated PDF certificate buffer to
Firebase Storage and returns its signed URL.") to accurately describe that it
uploads the PDF to Firebase Storage and returns the application API download URL
(e.g. /api/certificate?...), update the "`@returns`" line to mention the API-based
download URL and any query params/tokens returned, and adjust any example usage
in the comment to show calling the API endpoint rather than a signed Storage
URL.
cloud-functions/lib/dailyDigest.js (1)

54-64: ⚡ Quick win

startAt type matches current writes; only legacy/mixed docs could break the count

getTodayEventCount in cloud-functions/lib/dailyDigest.js queries events.startAt using today.toISOString()/tomorrow.toISOString(). Current event creation writes startAt as an ISO string (app/src/screens/CreateEvent.js uses startAt: startDate.toISOString()), so this query is consistent for newly created events.

cloud-functions/src/reputation.ts supports startAt as Firestore Timestamp too, so if older/legacy events were stored as Timestamp, the daily digest count could undercount them—consider normalizing/migrating to a single type.

🤖 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 `@cloud-functions/lib/dailyDigest.js` around lines 54 - 64, getTodayEventCount
currently queries events.startAt using ISO strings
(today.toISOString()/tomorrow.toISOString()), which will miss legacy documents
where startAt is a Firestore Timestamp; update getTodayEventCount to handle both
types by querying for Timestamp ranges when documents use Timestamps or by
normalizing startAt values before comparison (e.g., convert stored Timestamps to
ISO strings or convert ISO strings to Timestamps), and/or perform a dual-check:
fetch documents where startAt is between Timestamp(today) and
Timestamp(tomorrow) as well as where startAt is between ISO strings, ensuring
you reference the getTodayEventCount function and consider compatibility with
cloud-functions/src/reputation.ts which expects Timestamp values.
🤖 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 `@app/src/lib/checkInService.js`:
- Around line 407-413: The catch block that handles errors from
getDoc(checkInRef) in syncOfflineCheckIns currently logs to console.error then
returns silently; change it to log the error (include checkInRef/id and error
details) and propagate the failure so the caller can track failed items—either
rethrow the error or return a structured failure result; update references in
this block (checkInRef, checkInSnap, getDoc, and syncOfflineCheckIns) so errors
are recorded in logs and the syncOfflineCheckIns caller can handle/report failed
syncs rather than dropping the item silently.

In `@cloud-functions/eslint.config.mjs`:
- Around line 35-36: Remove the two obsolete ESLint core rule entries
"require-jsdoc" and "valid-jsdoc" from the flat config so the config loads under
ESLint v9+; locate the rules in the cloud-functions eslint config (the entries
currently set to "off") and delete those two lines (or remove those keys from
the exported rules object) so they are no longer present in the configuration.

In `@cloud-functions/lib/onEventCreate.js`:
- Around line 54-65: The current snapshot.forEach uses the pushToken guard to
skip creating the in-app notification; change the logic so the notifications
document is always created (use the same notifRef and batch.set for the
'notifications' collection unconditionally) and only gate push-message sending
on the presence of pushToken; update the block around snapshot.forEach /
pushToken / notifRef / batch.set so batch.set always runs to create the in-app
notification and any push delivery logic (where pushToken is read) remains
conditional.
- Around line 43-48: The query uses startAfter(startAfter) without an orderBy,
which breaks Firestore cursor pagination; update the users query built in
onEventCreate.js to include an explicit ordering (e.g.,
.orderBy(FieldPath.documentId())) before .startAfter(...) and ensure the value
passed to startAfter is the matching document id or snapshot used for ordering
(mirror the pattern used in dailyDigest.js), and import/resolve FieldPath from
the Firestore SDK as needed.

In `@cloud-functions/lib/server.js`:
- Line 77: The ipWhitelist middleware is imported after it's used, causing a
TDZ/ReferenceError at runtime; open cloud-functions/src/server.ts and move the
import/require for the ipWhitelist module (the symbol ipWhitelist or import from
"./middleware/ipWhitelist") to precede the first call to app.use("/api",
ipWhitelist) so the emitted CommonJS require appears before that usage; ensure
the import statement is placed above where app is configured (before any
app.use("/api", ...)) and retain the same imported symbol name (ipWhitelist) so
no other code changes are necessary.

In `@cloud-functions/package.json`:
- Around line 47-55: The package.json devDependencies block lists pinned
versions (eslint, typescript, firebase-tools, firebase-functions-test, jest,
prettier, ts-jest, ts-node, globals); verify each version is resolvable in your
CI/registry by running npm view <pkg>@<version> or npm install in the target
environment for firebase-tools@15.19.0, firebase-functions-test@3.1.0,
jest@30.4.2, prettier@3.0.0, ts-jest@29.4.11, ts-node@10.9.2, globals@17.6.0 (in
addition to eslint@10.4.0 and typescript@6.0.3 already confirmed); if any
package/version is unavailable, update the version string in the cloud-functions
package.json to a published release (or add an alternative registry setting in
CI) and re-run install to ensure the lockfile is consistent.

In `@cloud-functions/src/reputation.test.ts`:
- Around line 22-28: The test helper getMonthStart uses local-time setters
causing timezone mismatch with production getMonthKeyFromDate which uses UTC;
update getMonthStart to construct the month-start in UTC using Date.UTC or the
UTC setters (e.g., create a Date from Date.UTC(year, month - offset, 1, 0, 0, 0,
0) or use setUTCDate/setUTCHours/setUTCMonth) so the returned Date aligns with
getMonthKeyFromDate's getUTCFullYear/getUTCMonth behavior.

In `@cloud-functions/src/reputation.ts`:
- Around line 614-620: getTopContributors is missing the App Check enforcement
that other callables use; add a call to enforceAppCheck(context) near the start
of getTopContributors (after the auth check or in the same initialization block)
so the function rejects requests without valid App Check tokens; reference the
existing enforceAppCheck(context) helper (as used in setRole.ts) and ensure it
is invoked before proceeding with any business logic in getTopContributors.

In `@cloud-functions/src/server.ts`:
- Line 40: The middleware ipWhitelist is used in the app.use('/api',
ipWhitelist) call near the top but is imported later in the file, causing a
runtime ReferenceError; move the import for ipWhitelist up into the top import
block with the other imports so it is defined before app.use is executed, and
remove the now-duplicate mid-file import of ipWhitelist to avoid redundancy.

---

Nitpick comments:
In `@app/src/lib/checkInService.js`:
- Around line 473-474: The event fetch using getDoc(doc(db, 'events', eventId))
should be wrapped in a try/catch so failures (network/permission) are logged
rather than silently setting eventStartAt to null; inside the catch log the
error (use the module's logger or console) with context including eventId and
function name (syncOfflineCheckInItem or the surrounding function) and then set
eventStartAt to null so existing fallback behavior remains; update the code
around eventSnap/eventStartAt to reflect this error handling while keeping the
rest of syncOfflineCheckInItem logic unchanged.

In `@app/src/screens/EventDetail.js`:
- Around line 512-517: The transaction writes eventData.startAt into the
participating user document without checking it, which can store undefined;
before calling transaction.set for userRef (and when building participantPayload
if applicable), check that eventData.startAt is defined and only include
eventStartAt in the object when it exists (or explicitly set it to null/default)
to avoid writing undefined to Firestore; update the transaction.set(userRef,
{...}) call to conditionally add eventStartAt based on eventData.startAt
(referencing transaction.set, userRef, participantPayload, eventData.startAt,
and eventId).
- Around line 403-411: The code is writing event.startAt and a derived remindAt
without validating it; update the block that calls addDoc(collection(db,
'reminders'), ...) to first check event.startAt and handle the missing case:
either abort/log/throw and avoid creating the reminder (use process/console
logger and return) or build the payload so eventStartAt and remindAt are only
included when event.startAt is a valid date (i.e., check typeof event.startAt
and !isNaN(new Date(event.startAt))). Ensure the notificationId/other fields
(notifId, user.uid, event.id, event.title, createdAt) are still persisted as
appropriate and that remindAt calculation only runs when event.startAt is valid.

In `@app/src/screens/EventRegistrationFormScreen.js`:
- Around line 143-147: Before calling transaction.set on participatingRef,
verify freshEvent.startAt is defined and valid; if it's undefined/invalid, abort
the transaction (throw) or set an explicit fallback (e.g., use event.startAt or
current timestamp) instead of writing undefined. Update the logic around
transaction.set(participatingRef, {...}) to perform this check for
freshEvent.startAt and either throw a descriptive error to halt the transaction
or substitute a safe timestamp value so the participating document never stores
undefined.

In `@app/src/screens/PaymentScreen.js`:
- Around line 198-205: The participate write is storing freshEvent.startAt
directly which can be undefined; before calling
transaction.set(participatingRef, ...) validate freshEvent.startAt and only
write a safe value (e.g., use a normalized timestamp or null) or omit the
eventStartAt field when absent to avoid persisting undefined. Locate the
transaction.set call that writes the participating document and change the
payload construction so it checks freshEvent.startAt (from freshEvent) and
assigns a fallback or skips the key if undefined, ensuring downstream reputation
bucketing always sees a defined timestamp or null.

In `@cloud-functions/lib/dailyDigest.js`:
- Around line 54-64: getTodayEventCount currently queries events.startAt using
ISO strings (today.toISOString()/tomorrow.toISOString()), which will miss legacy
documents where startAt is a Firestore Timestamp; update getTodayEventCount to
handle both types by querying for Timestamp ranges when documents use Timestamps
or by normalizing startAt values before comparison (e.g., convert stored
Timestamps to ISO strings or convert ISO strings to Timestamps), and/or perform
a dual-check: fetch documents where startAt is between Timestamp(today) and
Timestamp(tomorrow) as well as where startAt is between ISO strings, ensuring
you reference the getTodayEventCount function and consider compatibility with
cloud-functions/src/reputation.ts which expects Timestamp values.

In `@cloud-functions/lib/server.js`:
- Around line 187-232: The /api/certificate handler currently allows public
access; update it to enforce authentication and owner-only access by adding an
auth check (e.g., middleware like authenticateRequest or inline use of
admin.auth().verifyIdToken on the Authorization header) inside the
app.get('/api/certificate', ...) flow before fetching the participant document,
and then validate that the authenticated user's uid equals participantId or that
the user has an admin/organizer role in the event document; if the token is
missing/invalid or the user is not authorized, return 401/403 respectively while
preserving the existing existence/revocation/storage checks and signed-url
generation logic.

In `@cloud-functions/src/certificateService.ts`:
- Around line 135-137: The JSDoc above the certificate upload function
incorrectly states it "returns its signed URL"; update that docstring for the
function that uploads the PDF buffer (the function with the current comment
"Uploads a generated PDF certificate buffer to Firebase Storage and returns its
signed URL.") to accurately describe that it uploads the PDF to Firebase Storage
and returns the application API download URL (e.g. /api/certificate?...), update
the "`@returns`" line to mention the API-based download URL and any query
params/tokens returned, and adjust any example usage in the comment to show
calling the API endpoint rather than a signed Storage URL.

In `@cloud-functions/src/reputation.test.ts`:
- Around line 94-105: The test for onParticipatingCreate is missing
context.eventId so the idempotency key becomes "participating_create_undefined";
update the test to pass an eventId in the wrapped invocation (e.g., when calling
wrapped(snap, { params: { userId: 'u1', eventId: 'e1' } }) include an eventId in
the second argument's context/eventId field) so the code that builds the
idempotency key (using context.eventId) is exercised correctly and idempotency
behavior can be asserted.

In `@cloud-functions/src/reputation.ts`:
- Around line 535-544: The backfill currently uses
admin.firestore.FieldValue.increment(...) with batch.set(..., { merge: true })
on bucketRef (using bucket.monthKey, bucket.registrations, bucket.attendances,
bucket.reminders), which makes the operation non-idempotent; modify the backfill
to be safe by adding a processed marker or making writes idempotent: add a
processedBackfill flag (e.g., processedBackfill: true or processedAt timestamp)
to each bucket document and have the routine skip documents that already have
that marker, or compute and write absolute counts instead of using
FieldValue.increment so repeated runs won’t accumulate—ensure the logic around
batch.set and bucketRef checks this marker before applying increments/updates.
- Around line 188-240: The code spawns up to 500 concurrent subcollection reads
inside paginateQuery by using Promise.all over docs; replace that with a
bounded-concurrency approach to avoid Firestore rate limits — either process
docs in fixed-size chunks (e.g., 50–100) or use a semaphore/promise limiter
(p-limit) to cap concurrent calls to
userDoc.ref.collection(REPUTATION_BUCKETS_COLLECTION).get(); keep the same
aggregation logic (attendanceCount, registrationCount, remindersSet, points) and
the batch.update calls but perform them as each chunk completes, and only call
batch.commit() after each chunk (or when batch size approaches limits) to avoid
large in-memory batches; update the section around Promise.all(docs.map(...)) to
iterate in controlled concurrency instead.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 93158615-1892-4d03-bf68-211f3302099a

📥 Commits

Reviewing files that changed from the base of the PR and between 8d9ec15 and 7ead1f5.

⛔ Files ignored due to path filters (2)
  • cloud-functions/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (31)
  • .eslintrc.json
  • app/src/lib/checkInService.js
  • app/src/screens/EventDetail.js
  • app/src/screens/EventRegistrationFormScreen.js
  • app/src/screens/PaymentScreen.js
  • cloud-functions/.eslintrc.json
  • cloud-functions/eslint.config.mjs
  • cloud-functions/lib/analyzeAttendance.js
  • cloud-functions/lib/certificateService.js
  • cloud-functions/lib/dailyDigest.js
  • cloud-functions/lib/eventNotifications.js
  • cloud-functions/lib/index.js
  • cloud-functions/lib/onEventCreate.js
  • cloud-functions/lib/reputation.js
  • cloud-functions/lib/server.js
  • cloud-functions/lib/setRole.js
  • cloud-functions/package.json
  • cloud-functions/src/analyzeAttendance.ts
  • cloud-functions/src/certificateService.ts
  • cloud-functions/src/dailyDigest.ts
  • cloud-functions/src/eventNotifications.ts
  • cloud-functions/src/onEventCreate.ts
  • cloud-functions/src/postEventFeedback.ts
  • cloud-functions/src/reputation.test.ts
  • cloud-functions/src/reputation.ts
  • cloud-functions/src/server.ts
  • docs/DATA_MODEL.md
  • docs/FEATURES.md
  • eslint.config.mjs
  • package.json
  • walkthrough.md
💤 Files with no reviewable changes (2)
  • .eslintrc.json
  • cloud-functions/.eslintrc.json

Comment thread app/src/lib/checkInService.js
Comment thread cloud-functions/eslint.config.mjs Outdated
Comment on lines +43 to +48
let query = db.collection('users')
.select('pushToken')
.limit(BATCH_SIZE);
if (startAfter) {
query = query.startAfter(startAfter);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing orderBy breaks cursor-based pagination.

The query uses startAfter(startAfter) without an orderBy clause. Firestore cursor pagination requires explicit ordering for deterministic results; without it, batches may skip or duplicate users across iterations.

Compare with the working pattern in dailyDigest.js which correctly uses .orderBy(FieldPath.documentId()).

🐛 Proposed fix
+const { FieldPath } = require("firebase-admin/firestore");
+
 async function processUserBatch(db, eventId, eventTitle, startAfter) {
     let query = db.collection('users')
+        .orderBy(FieldPath.documentId())
         .select('pushToken')
         .limit(BATCH_SIZE);
🤖 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 `@cloud-functions/lib/onEventCreate.js` around lines 43 - 48, The query uses
startAfter(startAfter) without an orderBy, which breaks Firestore cursor
pagination; update the users query built in onEventCreate.js to include an
explicit ordering (e.g., .orderBy(FieldPath.documentId())) before
.startAfter(...) and ensure the value passed to startAfter is the matching
document id or snapshot used for ordering (mirror the pattern used in
dailyDigest.js), and import/resolve FieldPath from the Firestore SDK as needed.

Comment on lines +54 to +65
snapshot.forEach(userDoc => {
const pushToken = userDoc.get('pushToken');
if (pushToken) {
const notifRef = userDoc.ref.collection('notifications').doc(`${eventId}_${userDoc.id}`);
batch.set(notifRef, {
title: 'New Event Alert! 📢',
body: `Check out: "${eventTitle}"`,
eventId: eventId,
createdAt: firestore_1.FieldValue.serverTimestamp(),
read: false
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Users without push tokens miss in-app notifications.

The if (pushToken) guard on line 56 gates both push AND in-app notification creation. Users who haven't granted push permissions should still receive in-app notifications.

🐛 Proposed fix - always create in-app notification
     snapshot.forEach(userDoc => {
         const pushToken = userDoc.get('pushToken');
-        if (pushToken) {
-            const notifRef = userDoc.ref.collection('notifications').doc(`${eventId}_${userDoc.id}`);
-            batch.set(notifRef, {
-                title: 'New Event Alert! 📢',
-                body: `Check out: "${eventTitle}"`,
-                eventId: eventId,
-                createdAt: firestore_1.FieldValue.serverTimestamp(),
-                read: false
-            });
-        }
+        const notifRef = userDoc.ref.collection('notifications').doc(`${eventId}_${userDoc.id}`);
+        batch.set(notifRef, {
+            title: 'New Event Alert! 📢',
+            body: `Check out: "${eventTitle}"`,
+            eventId: eventId,
+            createdAt: firestore_1.FieldValue.serverTimestamp(),
+            read: false
+        });
     });
🤖 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 `@cloud-functions/lib/onEventCreate.js` around lines 54 - 65, The current
snapshot.forEach uses the pushToken guard to skip creating the in-app
notification; change the logic so the notifications document is always created
(use the same notifRef and batch.set for the 'notifications' collection
unconditionally) and only gate push-message sending on the presence of
pushToken; update the block around snapshot.forEach / pushToken / notifRef /
batch.set so batch.set always runs to create the in-app notification and any
push delivery logic (where pushToken is read) remains conditional.

Comment thread cloud-functions/lib/server.js Outdated
Comment on lines +47 to +55
"eslint": "^10.4.0",
"firebase-functions-test": "^3.1.0",
"firebase-tools": "^15.19.0",
"jest": "^30.4.2",
"prettier": "^3.0.0",
"ts-jest": "^29.4.11",
"ts-node": "^10.9.2",
"typescript": "^6.0.3"
"globals": "^17.6.0",
"jest": "^30.4.2",
"prettier": "^3.0.0",
"ts-jest": "^29.4.11",
"ts-node": "^10.9.2",
"typescript": "^6.0.3"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What are the latest published npm versions for eslint, typescript, @eslint/js, and @eslint/eslintrc, and do versions eslint@10.4.0andtypescript@6.0.3 exist?

💡 Result:

As of May 31, 2026, the latest published versions of the requested packages on npm are as follows: * eslint: 10.4.1 [1][2] * typescript: 6.0.3 [3][4] * @eslint/js: 9.39.4 [5] * @eslint/eslintrc: 3.3.5 [1] Regarding the specific versions you asked about: * eslint@10.4.0: This version exists [1][2]. It was published in May 2026 [1][2] and is part of the 10.4.x release series [1][2]. * typescript@6.0.3: This version exists [3][4]. It was officially released in April 2026 [4][6].

Citations:


Confirm npm availability of pinned devDependencies (eslint/typescript exist; remaining packages to double-check)

eslint@10.4.0 and typescript@6.0.3 are published on npm. Still verify the other pinned devDependencies in cloud-functions/package.json (e.g., firebase-tools@15.19.0, firebase-functions-test@3.1.0, jest@30.4.2, prettier@3.0.0, ts-jest@29.4.11, ts-node@10.9.2, globals@17.6.0) are resolvable in the target registry/CI environment.

🤖 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 `@cloud-functions/package.json` around lines 47 - 55, The package.json
devDependencies block lists pinned versions (eslint, typescript, firebase-tools,
firebase-functions-test, jest, prettier, ts-jest, ts-node, globals); verify each
version is resolvable in your CI/registry by running npm view <pkg>@<version> or
npm install in the target environment for firebase-tools@15.19.0,
firebase-functions-test@3.1.0, jest@30.4.2, prettier@3.0.0, ts-jest@29.4.11,
ts-node@10.9.2, globals@17.6.0 (in addition to eslint@10.4.0 and
typescript@6.0.3 already confirmed); if any package/version is unavailable,
update the version string in the cloud-functions package.json to a published
release (or add an alternative registry setting in CI) and re-run install to
ensure the lockfile is consistent.

Comment thread cloud-functions/src/reputation.test.ts
Comment thread cloud-functions/src/reputation.ts
Comment thread cloud-functions/src/server.ts
@sonarqubecloud
Copy link
Copy Markdown

@shantanushok
Copy link
Copy Markdown
Contributor Author

@roshankumar0036singh
check it out

@roshankumar0036singh
Copy link
Copy Markdown
Owner

@shantanushok why 35k diff?

@shantanushok
Copy link
Copy Markdown
Contributor Author

@shantanushok why 35k diff?:

We wanted to ensure that user reputation accurately reflects recent involvement rather than just accumulating points infinitely over time. To solve this, we built a linear time-decay algorithm over a rolling 12-month window.

Here is exactly how the math and architecture work:

  1. Scalable Data Storage (Monthly Bucketing) :
    Instead of querying every single past event a user ever attended (which would be too slow/expensive at scale), we grouped activities into "monthly buckets" stored under the user's document (e.g., a subcollection for 2026-05). When a user checks in or registers for an event, a Firestore trigger simply increments the counters for that specific month.

  2. The 12-Month Rolling Window:
    When calculateReputation runs, it only looks at the user's buckets from the last 12 months. Any points earned 13+ months ago are considered fully "decayed" (worth 0) and are ignored by the calculation.

  3. The Linear Decay Math:
    We apply a multiplier to the points inside each bucket based on how old the bucket is. We used a base value of 10 points for attending an event, and 2 points for registering.

The formula is: Multiplier = (12 - months_elapsed) / 12

This creates a smooth, linear degradation of points over time or a year .

0 months ago (Current Month): Points retain 100% of their value. (Attendance = 10 pts)
3 months ago: Points retain 75% of their value. (Attendance = 7.5 pts)
6 months ago: Points retain 50% of their value. (Attendance = 5 pts)
9 months ago: Points retain 25% of their value. (Attendance = 2.5 pts)
12+ months ago: Points retain 0% of their value. (Attendance = 0 pts)

This involved changes being made onto multiple files (almost 21 in the first commit itself).

To make all of this possible I had to implement the following components:

  1. Core Algorithm Functions
    calculateReputation (HTTP Callable): The core endpoint that executes the time-decay algorithm across a user's 12-month rolling window.
    refreshReputationDaily (PubSub CRON Job): A scheduled function that runs every 24 hours to silently apply the time decay to all users and keep the scores continuously updated without any user interaction.

  2. Atomic Event Triggers
    Instead of calculating everything on the fly, these lightweight triggers update the "monthly buckets" completely asynchronously in the background so the frontend never slows down:

onParticipatingCreate / onParticipatingDelete: Increments/decrements the registration counter when a user joins or leaves an event.
onCheckInCreate / onCheckInDelete: Increments/decrements the attendance counter when a user scans the check-in QR code.
onReminderCreate / onReminderDelete: Increments/decrements the reminders counter when a user favorites an event.

  1. Administrative / Backfill Scripts
    backfillReputationBuckets (HTTP Callable): A powerful, paginated backfill script. We built this so you can run it once to scan the entire existing production database and retroactively generate the new "monthly bucket" architecture for all old data, meaning no historical data is lost during this upgrade.

  2. Leaderboard System
    refreshTopContributorsLeaderboard (PubSub CRON Job): Aggregates the top 10 users globally and caches them to a single lightweight document (leaderboards/topContributors) so the frontend Profile Screen can load the podium instantly without expensive queries.
    getTopContributors (HTTP Callable): A paginated, secure API endpoint that allows clients to load the rest of the leaderboard infinitely. (This is where we patched the missing enforceAppCheck security flaw).

Additionally I also addressed the issues which were being flagged by the coderabbitai and copilot , sonarcloud which may have snowballed .

@roshankumar0036singh
Copy link
Copy Markdown
Owner

@shantanushok but still it isnt justyfying the diff against your feature there are many unecessary pr related chnages there

@roshankumar0036singh
Copy link
Copy Markdown
Owner

@shantanushok remoev the walkthorugh file and package lock and package json file you didn made chnages in them

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