@@ -289,6 +289,95 @@ router.post('/reset-password', (req: Request, res: Response) => {
289289 } ) ( ) ;
290290} ) ;
291291
292+ /**
293+ * POST /auth/initialize-trial
294+ * Initialize trial entitlement for newly created user
295+ * Called by frontend immediately after CouchDB account creation
296+ *
297+ * Body params:
298+ * - username: string (required) - CouchDB username
299+ * - origin: string (optional) - Frontend origin URL (e.g., 'letterspractice.com') for courseId inference
300+ * - courseId: string (optional) - Explicit courseId, if not provided inferred from origin
301+ */
302+ router . post ( '/initialize-trial' , ( req : Request , res : Response ) => {
303+ void ( async ( ) => {
304+ try {
305+ const { username, origin, courseId : explicitCourseId } = req . body ;
306+
307+ if ( ! username ) {
308+ return res . status ( 400 ) . json ( { ok : false , error : 'Username required' } ) ;
309+ }
310+
311+ // Infer courseId from origin if not explicitly provided
312+ let courseId = explicitCourseId ;
313+ if ( ! courseId && origin ) {
314+ // Map origin to courseId (e.g., 'letterspractice.com' → 'letterspractice-basic')
315+ const originMap : Record < string , string > = {
316+ 'letterspractice.com' : 'letterspractice-basic' ,
317+ 'localhost:5173' : 'letterspractice-basic' , // local dev
318+ 'localhost:3000' : 'letterspractice-basic' , // express server itself
319+ } ;
320+ courseId = originMap [ origin ] || origin . replace ( / \. / g, '-' ) . toLowerCase ( ) ;
321+ }
322+
323+ // Default to letterspractice-basic if still not determined
324+ if ( ! courseId ) {
325+ courseId = 'letterspractice-basic' ;
326+ }
327+
328+ // Find user in _users db (should exist since just created via CouchDB)
329+ const userDoc = await findUserByUsername ( username ) ;
330+ if ( ! userDoc ) {
331+ return res . status ( 404 ) . json ( { ok : false , error : 'User not found' } ) ;
332+ }
333+
334+ // Initialize entitlements if not present
335+ if ( ! userDoc . entitlements ) {
336+ userDoc . entitlements = { } ;
337+ }
338+
339+ // Don't overwrite if entitlement already exists (idempotent)
340+ if ( userDoc . entitlements [ courseId ] ) {
341+ logger . info ( `Trial already initialized for ${ username } on ${ courseId } - skipping` ) ;
342+ return res . json ( {
343+ ok : true ,
344+ message : 'Trial already initialized' ,
345+ entitlement : userDoc . entitlements [ courseId ]
346+ } ) ;
347+ }
348+
349+ // Calculate expiration: 30 days from now
350+ const now = new Date ( ) ;
351+ const expiresDate = new Date ( now ) ;
352+ expiresDate . setDate ( expiresDate . getDate ( ) + 30 ) ;
353+
354+ // Create trial entitlement
355+ userDoc . entitlements [ courseId ] = {
356+ status : 'trial' ,
357+ registrationDate : now . toISOString ( ) ,
358+ purchaseDate : now . toISOString ( ) , // Same as registration for trials
359+ expires : expiresDate . toISOString ( ) ,
360+ } ;
361+
362+ await updateUserDoc ( userDoc ) ;
363+
364+ logger . info ( `Trial initialized for ${ username } on ${ courseId } - expires ${ expiresDate . toISOString ( ) } ` ) ;
365+
366+ res . json ( {
367+ ok : true ,
368+ entitlement : userDoc . entitlements [ courseId ]
369+ } ) ;
370+
371+ } catch ( error ) {
372+ logger . error ( 'Error initializing trial:' , error ) ;
373+ res . status ( 500 ) . json ( {
374+ ok : false ,
375+ error : 'Failed to initialize trial' ,
376+ } ) ;
377+ }
378+ } ) ( ) ;
379+ } ) ;
380+
292381/**
293382 * POST /auth/permissions
294383 * Grant or update course permissions for a user (called by payment webhooks)
0 commit comments