import { Inject, Injectable, forwardRef } from '@nestjs/common' import { DatabaseService } from '../database/database.service' import { GOLD_LAND_LEVELS, GOLD_LAND_MAX_STARS, getLevelDefinition, } from './levels' import type { UserState } from '../shared/types/user.types' import { UsersService } from '../users/users.service' type LevelStatus = 'locked' | 'available' | 'in_progress' | 'completed' | 'coming_soon' interface LevelProgressRow { user_id: string level_id: number best_score: number stars: number attempts_count: number started_at: string | null completed_at: string | null updated_at: string } interface LevelItemResponse { id: number order: number slug: string title: string description: string status: LevelStatus stars: 0 | 1 | 2 | 3 bestScore: number activityType: string activityKey: string activityRoute?: string rewardXp: number rewardCoins: number } @Injectable() export class ProgressionSyncService { constructor( @Inject(DatabaseService) private readonly database: DatabaseService, @Inject(forwardRef(() => UsersService)) private readonly users: UsersService, ) {} syncUserProgressionState(userId: string): UserState { return this.database.transaction(() => { const state = this.users.getUserState(userId) const items = this.buildLevelItems(userId) this.syncLegacyMapLevels(state, items) return this.users.saveUserState(state) }) } private getProgressRow(userId: string, levelId: number): LevelProgressRow | undefined { return this.database .getDb() .prepare('SELECT * FROM user_level_progress WHERE user_id = ? AND level_id = ?') .get(userId, levelId) as LevelProgressRow | undefined } private getAllProgressRows(userId: string): LevelProgressRow[] { return this.database .getDb() .prepare('SELECT * FROM user_level_progress WHERE user_id = ?') .all(userId) as LevelProgressRow[] } private migrateFromLegacyState(userId: string): void { const existing = this.getAllProgressRows(userId) if (existing.length > 0) return const state = this.users.getUserState(userId) const now = new Date().toISOString() const db = this.database.getDb() const insert = db.prepare(` INSERT INTO user_level_progress ( user_id, level_id, best_score, stars, attempts_count, started_at, completed_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `) this.database.transaction(() => { for (const legacy of state.mapLevels) { if (legacy.stars <= 0 && legacy.status !== 'completed') continue insert.run( userId, legacy.id, legacy.stars > 0 ? legacy.stars * 30 : 0, legacy.stars, legacy.stars > 0 ? 1 : 0, legacy.stars > 0 ? now : null, legacy.status === 'completed' && legacy.stars > 0 ? now : null, now, ) } }) } private isLevelUnlocked(levelId: number, rowsById: Map): boolean { if (levelId === 1) return true const prevStars = rowsById.get(levelId - 1)?.stars ?? 0 return prevStars >= 1 } private resolveLevelStatus( levelId: number, progress: LevelProgressRow | undefined, rowsById: Map, ): LevelStatus { const definition = getLevelDefinition(levelId) if (!definition) return 'locked' if ((progress?.stars ?? 0) > 0) return 'completed' if (progress?.started_at && !progress.completed_at) return 'in_progress' if (!this.isLevelUnlocked(levelId, rowsById)) return 'locked' if (!definition.isActive) return 'coming_soon' return 'available' } private buildLevelItems(userId: string): LevelItemResponse[] { this.migrateFromLegacyState(userId) const rows = this.getAllProgressRows(userId) const byId = new Map(rows.map((row) => [row.level_id, row])) return GOLD_LAND_LEVELS.map((definition) => { const progress = byId.get(definition.id) const status = this.resolveLevelStatus(definition.id, progress, byId) return { id: definition.id, order: definition.order, slug: definition.slug, title: definition.title, description: definition.description, status, stars: (progress?.stars ?? 0) as 0 | 1 | 2 | 3, bestScore: progress?.best_score ?? 0, activityType: definition.activityType, activityKey: definition.activityKey, activityRoute: definition.activityRoute, rewardXp: definition.rewardXp, rewardCoins: definition.rewardCoins, } }) } private syncLegacyMapLevels(state: UserState, items: LevelItemResponse[]): void { state.mapLevels = items.map((item) => ({ id: item.id, title: item.title, stars: item.stars, maxStars: 3, status: this.mapStatusToLegacy(item.status), lessonRoute: item.activityRoute, x: 0, y: 0, })) state.totalStars = items.reduce((sum, item) => sum + item.stars, 0) state.maxStars = GOLD_LAND_MAX_STARS } private mapStatusToLegacy(status: LevelStatus): UserState['mapLevels'][number]['status'] { switch (status) { case 'completed': return 'completed' case 'in_progress': return 'current' case 'available': return 'unlocked' case 'coming_soon': return 'locked' default: return 'locked' } } }