diff --git a/src/server/services/onboarding.service.ts b/src/server/services/onboarding.service.ts new file mode 100644 index 0000000..5cac54d --- /dev/null +++ b/src/server/services/onboarding.service.ts @@ -0,0 +1,122 @@ +import { eq, inArray } from "drizzle-orm"; +import { db as prodDb } from "../../db/index.ts"; +import { categories, globalItems, items, settings } from "../../db/schema.ts"; + +type Db = typeof prodDb; + +interface OnboardingResult { + itemsCreated: number; + categoriesCreated: string[]; +} + +/** + * Complete onboarding by batch-creating user items from selected global catalog items. + * Auto-creates categories based on the global items' category field. + * Sets onboardingComplete setting to "true". + */ +export async function completeOnboarding( + db: Db = prodDb, + userId: number, + globalItemIds: number[], +): Promise { + if (globalItemIds.length === 0) { + // No items selected — just mark complete + await db + .insert(settings) + .values({ userId, key: "onboardingComplete", value: "true" }) + .onConflictDoUpdate({ + target: [settings.userId, settings.key], + set: { value: "true" }, + }); + return { itemsCreated: 0, categoriesCreated: [] }; + } + + // Fetch all selected global items + const selectedItems = await db + .select() + .from(globalItems) + .where(inArray(globalItems.id, globalItemIds)); + + if (selectedItems.length === 0) { + await db + .insert(settings) + .values({ userId, key: "onboardingComplete", value: "true" }) + .onConflictDoUpdate({ + target: [settings.userId, settings.key], + set: { value: "true" }, + }); + return { itemsCreated: 0, categoriesCreated: [] }; + } + + // Collect unique category names from global items + const categoryNames = [ + ...new Set( + selectedItems + .map((gi) => gi.category) + .filter((c): c is string => c !== null && c.trim() !== ""), + ), + ]; + + // Get existing user categories + const existingCats = await db + .select() + .from(categories) + .where(eq(categories.userId, userId)); + + const existingCatMap = new Map( + existingCats.map((c) => [c.name.toLowerCase(), c.id]), + ); + + // Create missing categories + const newCategoryNames: string[] = []; + for (const catName of categoryNames) { + if (!existingCatMap.has(catName.toLowerCase())) { + const [created] = await db + .insert(categories) + .values({ name: catName, userId }) + .returning(); + existingCatMap.set(catName.toLowerCase(), created.id); + newCategoryNames.push(catName); + } + } + + // Get the "Uncategorized" category for items without a category + let uncategorizedId = existingCatMap.get("uncategorized"); + if (!uncategorizedId) { + const [unc] = await db + .insert(categories) + .values({ name: "Uncategorized", userId }) + .returning(); + uncategorizedId = unc.id; + } + + // Create user items linked to global items + let itemsCreated = 0; + for (const gi of selectedItems) { + const catId = gi.category + ? (existingCatMap.get(gi.category.toLowerCase()) ?? uncategorizedId) + : uncategorizedId; + + await db.insert(items).values({ + name: gi.brand ? `${gi.brand} ${gi.model}` : gi.model, + categoryId: catId, + userId, + weightGrams: gi.weightGrams, + priceCents: gi.priceCents, + imageFilename: gi.imageFilename, + globalItemId: gi.id, + }); + itemsCreated++; + } + + // Mark onboarding complete + await db + .insert(settings) + .values({ userId, key: "onboardingComplete", value: "true" }) + .onConflictDoUpdate({ + target: [settings.userId, settings.key], + set: { value: "true" }, + }); + + return { itemsCreated, categoriesCreated: newCategoryNames }; +}