feat(server): remove hard paywall — credits gate scans, guests stay blocked, free tier gets pro model
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -203,17 +203,15 @@ const resolveIdempotencyKey = (request) => {
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const createHardPaywallError = (requiredCredits) => {
|
const ensureNotGuest = (userId, requiredCredits) => {
|
||||||
const error = new Error('Active Pro or trial entitlement required.');
|
// 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.');
|
||||||
error.code = 'INSUFFICIENT_CREDITS';
|
error.code = 'INSUFFICIENT_CREDITS';
|
||||||
error.status = 402;
|
error.status = 402;
|
||||||
error.metadata = { required: requiredCredits, available: 0 };
|
error.metadata = { required: requiredCredits, available: 0 };
|
||||||
return error;
|
throw error;
|
||||||
};
|
|
||||||
|
|
||||||
const ensureActiveProEntitlement = (accountSnapshot, requiredCredits) => {
|
|
||||||
if (!accountSnapshot || accountSnapshot.plan !== 'pro') {
|
|
||||||
throw createHardPaywallError(requiredCredits);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -731,7 +729,7 @@ app.post('/v1/scan', async (request, response) => {
|
|||||||
getAccountSnapshot(db, userId),
|
getAccountSnapshot(db, userId),
|
||||||
getCachedCatalogEntries(db),
|
getCachedCatalogEntries(db),
|
||||||
]);
|
]);
|
||||||
ensureActiveProEntitlement(accountSnapshot, SCAN_PRIMARY_COST);
|
ensureNotGuest(userId, SCAN_PRIMARY_COST);
|
||||||
creditsCharged += await consumeCreditsWithIdempotency(
|
creditsCharged += await consumeCreditsWithIdempotency(
|
||||||
db,
|
db,
|
||||||
userId,
|
userId,
|
||||||
@@ -739,7 +737,8 @@ app.post('/v1/scan', async (request, response) => {
|
|||||||
SCAN_PRIMARY_COST,
|
SCAN_PRIMARY_COST,
|
||||||
);
|
);
|
||||||
|
|
||||||
const scanPlan = accountSnapshot.plan === 'pro' ? 'pro' : 'free';
|
// Free tier gets the same model quality; quantity (3 credits/month) is the differentiator.
|
||||||
|
const scanPlan = 'pro';
|
||||||
let result = pickCatalogFallback(catalogEntries, imageUri, false, { silent: true });
|
let result = pickCatalogFallback(catalogEntries, imageUri, false, { silent: true });
|
||||||
let usedOpenAi = false;
|
let usedOpenAi = false;
|
||||||
let rawPrimaryResult = null;
|
let rawPrimaryResult = null;
|
||||||
@@ -903,7 +902,7 @@ app.post('/v1/search/semantic', async (request, response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const accountSnapshot = await getAccountSnapshot(db, userId);
|
const accountSnapshot = await getAccountSnapshot(db, userId);
|
||||||
ensureActiveProEntitlement(accountSnapshot, SEMANTIC_SEARCH_COST);
|
ensureNotGuest(userId, SEMANTIC_SEARCH_COST);
|
||||||
|
|
||||||
const creditsCharged = await consumeCreditsWithIdempotency(
|
const creditsCharged = await consumeCreditsWithIdempotency(
|
||||||
db,
|
db,
|
||||||
@@ -943,7 +942,7 @@ app.post('/v1/health-check', async (request, response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const accountSnapshot = await getAccountSnapshot(db, userId);
|
const accountSnapshot = await getAccountSnapshot(db, userId);
|
||||||
ensureActiveProEntitlement(accountSnapshot, HEALTH_CHECK_COST);
|
ensureNotGuest(userId, HEALTH_CHECK_COST);
|
||||||
|
|
||||||
if (!isOpenAiConfigured()) {
|
if (!isOpenAiConfigured()) {
|
||||||
const error = new Error('OpenAI health check is unavailable. Please configure OPENAI_API_KEY.');
|
const error = new Error('OpenAI health check is unavailable. Please configure OPENAI_API_KEY.');
|
||||||
|
|||||||
Reference in New Issue
Block a user