sms-extension/client/src/utils/businessOnboarding.js

99 lines
3.7 KiB
JavaScript

import apiClient from '../api/client';
const STAGE_SEQUENCE = [
{ key: 'queued', label: 'Preparing scrape', note: 'Setting up the onboarding job.' },
{ key: 'crawling', label: 'Scraping brand pages', note: 'Capturing the homepage, about page, and representative product pages.' },
{ key: 'summarizing', label: 'Building site summary', note: 'Condensing the scraped pages into a compact brand summary.' },
{ key: 'parsing_brand', label: 'Extracting brand context', note: 'Inferring tone, taglines, and visual signals.' },
{ key: 'finalizing_business', label: 'Finalizing business', note: 'Saving the captured data and storefront assets.' },
{ key: 'completed', label: 'Completed', note: 'The onboarding job finished successfully.' },
{ key: 'failed', label: 'Failed', note: 'The onboarding job could not be completed.' },
];
const JOB_LOOKUP_RETRY_LIMIT = 4;
const RETRYABLE_JOB_NOT_FOUND_WINDOW_MS = 20_000;
const MAX_RETRYABLE_JOB_NOT_FOUND_MISSES = 4;
export async function startBusinessOnboardingJob(payload) {
const response = await apiClient.post('/api/businesses', payload);
if (response.status !== 202 || !response.data?.jobId) {
throw new Error('Unexpected onboarding response');
}
return response.data;
}
export async function fetchBusinessOnboardingJob(jobId) {
const response = await apiClient.get(`/api/businesses/jobs/${encodeURIComponent(jobId)}`);
return response.data;
}
export function getBusinessOnboardingStageMeta(stage) {
const normalizedStage = String(stage || '').trim().toLowerCase();
const found = STAGE_SEQUENCE.find((entry) => entry.key === normalizedStage) || STAGE_SEQUENCE[0];
const index = STAGE_SEQUENCE.findIndex((entry) => entry.key === found.key);
const percent = normalizedStage === 'completed'
? 100
: normalizedStage === 'failed'
? 100
: Math.max(8, Math.min(92, index <= 0 ? 12 : 12 + (index * 20)));
return {
stage: found.key,
label: found.label,
note: found.note,
index,
percent,
totalStages: STAGE_SEQUENCE.length - 2,
};
}
export function getBusinessOnboardingProgress(job = {}) {
const progress = job?.progress && typeof job.progress === 'object' ? job.progress : {};
return {
pagesProcessed: Number(progress.pagesProcessed || 0),
pagesDiscovered: Number(progress.pagesDiscovered || 0),
creditsUsed: Number(progress.creditsUsed || 0),
representativePages: Number(progress.representativePages || 0),
imageCount: Number(progress.imageCount || 0),
linkCount: Number(progress.linkCount || 0),
};
}
export function getBusinessOnboardingError(job = {}) {
const error = job?.error;
if (!error) return '';
if (typeof error === 'string') return error;
if (typeof error === 'object') {
return String(error.message || error.error || error.details || '').trim();
}
return String(error).trim();
}
export function isRetryableOnboardingJobLookupError(error) {
if (error?.response?.status !== 404) return false;
const message = String(error?.response?.data?.error || error?.message || '').trim();
return /onboarding job not found/i.test(message);
}
export function shouldRetryOnboardingJobLookup(error, missCount = 0) {
return isRetryableOnboardingJobLookupError(error) && missCount < JOB_LOOKUP_RETRY_LIMIT;
}
export function getOnboardingJobRetryDelay(missCount = 0) {
return Math.min(4500, 1500 + (Math.max(0, missCount) * 700));
}
export function shouldRetryMissingBusinessOnboardingJob(job = {}, error, missCount = 0) {
if (error?.response?.status !== 404) return false;
if (missCount >= MAX_RETRYABLE_JOB_NOT_FOUND_MISSES) return false;
const createdAtMs = Date.parse(String(job?.createdAt || '').trim());
if (!Number.isFinite(createdAtMs)) return false;
return Date.now() - createdAtMs <= RETRYABLE_JOB_NOT_FOUND_WINDOW_MS;
}