feat(30-01): create onboarding service with batch item creation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 20:42:59 +02:00
parent d64708056f
commit 9da4c8435c

View File

@@ -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<OnboardingResult> {
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 };
}