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:
122
src/server/services/onboarding.service.ts
Normal file
122
src/server/services/onboarding.service.ts
Normal 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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user