@@ -3,7 +3,7 @@ import * as functions from "firebase-functions";
33import { getPoints } from "./_getPoints" ;
44import { getTodayLondon } from "./_dateUtils" ;
55import { lookupPostboxes } from "./_lookupPostboxes" ;
6- import { updateUserLeaderboards } from "./_leaderboardUtils" ;
6+ import { updateUserLeaderboards , updateLifetimeLeaderboard } from "./_leaderboardUtils" ;
77import { computeNewStreak } from "./_streakUtils" ;
88
99const database = admin . firestore ( ) ;
@@ -100,7 +100,7 @@ export const startScoring = functions.https.onCall(async (request) => {
100100 // Keep dailyClaim on the postbox doc for display purposes (shows
101101 // "someone found this today" in future UI); does not gate claiming.
102102 tx . set ( postboxRef , { dailyClaim : { date : todayLondon , by : userid } } , { merge : true } ) ;
103- return pts ;
103+ return { key , pts } ;
104104 } ) ;
105105 } )
106106 ) ;
@@ -112,10 +112,17 @@ export const startScoring = functions.https.onCall(async (request) => {
112112 }
113113 }
114114
115- const earnedPoints = claimSettled
116- . filter ( ( r ) : r is PromiseFulfilledResult < number > => r . status === "fulfilled" && typeof r . value === "number" )
115+ const successfulClaims = claimSettled
116+ . filter ( ( r ) : r is PromiseFulfilledResult < { key : string ; pts : number } > =>
117+ r . status === "fulfilled" &&
118+ r . value !== null &&
119+ typeof r . value === "object" &&
120+ "key" in r . value
121+ )
117122 . map ( ( r ) => r . value ) ;
118123
124+ const earnedPoints = successfulClaims . map ( ( c ) => c . pts ) ;
125+
119126 // If no points were earned but at least one transaction was rejected (as
120127 // opposed to being skipped because already claimed today), surface an error
121128 // so the client shows a retry prompt rather than "Already claimed today".
@@ -153,6 +160,43 @@ export const startScoring = functions.https.onCall(async (request) => {
153160 // updateUserLeaderboards uses Promise.allSettled internally and never
154161 // throws; individual period failures are logged inside the function.
155162 await updateUserLeaderboards ( userid , displayName , todayLondon , database ) ;
163+
164+ // ── Lifetime leaderboard update ─────────────────────────────────────────
165+ try {
166+ // For each postbox claimed in this session, check if the user has any
167+ // prior claim on a different day. Empty result = first-ever claim for
168+ // that postbox → increment unique counter by 1.
169+ const uniqueChecks = await Promise . all (
170+ successfulClaims . map ( ( { key } ) =>
171+ database . collection ( "claims" )
172+ . where ( "userid" , "==" , userid )
173+ . where ( "postboxes" , "==" , `/postbox/${ key } ` )
174+ . where ( "dailyDate" , "<" , todayLondon )
175+ . limit ( 1 )
176+ . get ( )
177+ . then ( ( snap ) => snap . empty ? 1 : 0 )
178+ )
179+ ) ;
180+ const uniqueIncrement = uniqueChecks . reduce < number > ( ( a , b ) => a + b , 0 ) ;
181+ const lifetimePointsIncrement = earnedPoints . reduce ( ( s , p ) => s + p , 0 ) ;
182+
183+ await database . collection ( "users" ) . doc ( userid ) . set (
184+ {
185+ uniquePostboxesClaimed : admin . firestore . FieldValue . increment ( uniqueIncrement ) ,
186+ lifetimePoints : admin . firestore . FieldValue . increment ( lifetimePointsIncrement ) ,
187+ } ,
188+ { merge : true }
189+ ) ;
190+
191+ const updatedUser = await database . collection ( "users" ) . doc ( userid ) . get ( ) ;
192+ const d = updatedUser . data ( ) ?? { } ;
193+ const uniquePostboxesClaimed = ( d . uniquePostboxesClaimed as number | undefined ) ?? 0 ;
194+ const lifetimePoints = ( d . lifetimePoints as number | undefined ) ?? 0 ;
195+
196+ await updateLifetimeLeaderboard ( userid , displayName , uniquePostboxesClaimed , lifetimePoints , database ) ;
197+ } catch ( lifetimeErr ) {
198+ console . error ( "lifetime leaderboard update failed (non-fatal):" , lifetimeErr ) ;
199+ }
156200 }
157201
158202 return {
0 commit comments