Onboarding flow

This commit is contained in:
2026-07-06 22:25:52 +02:00
parent 99cd885833
commit 6b57a2acfc
37 changed files with 1503 additions and 507 deletions

View File

@@ -204,11 +204,11 @@ const resolveIdempotencyKey = (request) => {
return '';
};
const ensureNotGuest = (userId, requiredCredits) => {
// Guests use the client-side demo scan; the shared 'guest' billing account
// must never consume real credits or run free AI analyses.
if (isGuest(userId)) {
const error = new Error('Sign in to use scan credits.');
const ensureNotGuest = (userId, requiredCredits) => {
// Guests may use the limited pre-auth demo identification scan. Other
// endpoints must not consume credits from the shared 'guest' account.
if (isGuest(userId)) {
const error = new Error('Sign in to use scan credits.');
error.code = 'INSUFFICIENT_CREDITS';
error.status = 402;
error.metadata = { required: requiredCredits, available: 0 };
@@ -726,17 +726,20 @@ app.post('/v1/scan', async (request, response) => {
let modelUsed = null;
let modelFallbackCount = 0;
const [accountSnapshot, catalogEntries] = await Promise.all([
getAccountSnapshot(db, userId),
getCachedCatalogEntries(db),
]);
ensureNotGuest(userId, SCAN_PRIMARY_COST);
creditsCharged += await consumeCreditsWithIdempotency(
db,
userId,
chargeKey('scan-primary', userId, idempotencyKey),
SCAN_PRIMARY_COST,
);
const [accountSnapshot, catalogEntries] = await Promise.all([
getAccountSnapshot(db, userId),
getCachedCatalogEntries(db),
]);
if (isGuest(userId)) {
modelPath.push('guest-demo-no-credit');
} else {
creditsCharged += await consumeCreditsWithIdempotency(
db,
userId,
chargeKey('scan-primary', userId, idempotencyKey),
SCAN_PRIMARY_COST,
);
}
// Free tier gets the same model quality; quantity (3 credits/month) is the differentiator.
const scanPlan = 'pro';
@@ -764,8 +767,14 @@ app.post('/v1/scan', async (request, response) => {
usedOpenAi = true;
modelUsed = openAiPrimary.modelUsed || modelUsed;
modelPath.push('openai-primary');
if (grounded.grounded) modelPath.push('catalog-grounded-primary');
} else {
if (grounded.grounded) modelPath.push('catalog-grounded-primary');
} else {
if (isGuest(userId)) {
const error = new Error('AI demo scan failed. Please try again with a clearer plant photo.');
error.code = 'PROVIDER_ERROR';
error.status = 502;
throw error;
}
console.warn(`OpenAI primary identification returned null for user ${userId} — using catalog fallback.`, {
attemptedModels: openAiPrimary?.attemptedModels,
plant: result?.name,
@@ -773,8 +782,14 @@ app.post('/v1/scan', async (request, response) => {
modelPath.push('openai-primary-failed');
modelPath.push('catalog-primary-fallback');
}
} else {
console.log(`OpenAI not configured, using catalog fallback for user ${userId}`);
} else {
if (isGuest(userId)) {
const error = new Error('AI demo scan is unavailable. Please configure OpenAI before enabling guest demo scans.');
error.code = 'PROVIDER_ERROR';
error.status = 502;
throw error;
}
console.log(`OpenAI not configured, using catalog fallback for user ${userId}`);
modelPath.push('openai-not-configured');
modelPath.push('catalog-primary-fallback');
}