Fixed onboardingdraft for School/Program. Added pointer removal in App.js after navigate. Altered server3 draft body checks.
This commit is contained in:
parent
4a2aaedf63
commit
a2a2d9b558
@ -1 +1 @@
|
||||
720d57c0d6d787c629f53d47689088a50b9e9b5b-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
90af14136b0b935418ae62167703d1dcbcb7b3ce-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
|
@ -30,3 +30,10 @@ coverage
|
||||
*.crt
|
||||
*.pfx
|
||||
|
||||
# Test sources & artifacts
|
||||
tests/
|
||||
playwright-report/
|
||||
test-results/
|
||||
blob-report/
|
||||
*.trace.zip
|
||||
|
||||
|
@ -580,143 +580,9 @@ async function storeRiskAnalysisInDB({
|
||||
}
|
||||
|
||||
const COOKIE_NAME = process.env.COOKIE_NAME || 'aptiva_session';
|
||||
//*PremiumOnboarding draft
|
||||
// GET current user's draft
|
||||
app.get('/api/premium/onboarding/draft', authenticatePremiumUser, async (req, res) => {
|
||||
const [[row]] = await pool.query(
|
||||
`SELECT id, step, data
|
||||
FROM onboarding_drafts
|
||||
WHERE user_id=?
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 1`,
|
||||
[req.id]
|
||||
);
|
||||
return res.json(row || null);
|
||||
});
|
||||
|
||||
// POST upsert draft (ID-agnostic, partial merge, 1 draft per user)
|
||||
app.post('/api/premium/onboarding/draft', authenticatePremiumUser, async (req, res) => {
|
||||
try {
|
||||
// ---- 0) Harden req.body and incoming shapes
|
||||
const body = (req && typeof req.body === 'object' && req.body) ? req.body : {};
|
||||
let { id, step } = body;
|
||||
|
||||
// Accept either {data:{careerData/financialData/collegeData}} or section keys at top level
|
||||
let incoming = {};
|
||||
if (body.data != null) {
|
||||
if (typeof body.data === 'string') {
|
||||
try { incoming = JSON.parse(body.data); } catch { incoming = {}; }
|
||||
} else if (typeof body.data === 'object') {
|
||||
incoming = body.data;
|
||||
}
|
||||
}
|
||||
// If callers provided sections directly (EducationalProgramsPage),
|
||||
// lift them into the data envelope without crashing if body is blank.
|
||||
['careerData','financialData','collegeData'].forEach(k => {
|
||||
if (Object.prototype.hasOwnProperty.call(body, k)) {
|
||||
if (!incoming || typeof incoming !== 'object') incoming = {};
|
||||
incoming[k] = body[k];
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 1) Base draft: by id (if provided) else latest for this user
|
||||
let base = null;
|
||||
if (id) {
|
||||
const [[row]] = await pool.query(
|
||||
`SELECT id, step, data FROM onboarding_drafts WHERE user_id=? AND id=? LIMIT 1`,
|
||||
[req.id, id]
|
||||
);
|
||||
base = row || null;
|
||||
} else {
|
||||
const [[row]] = await pool.query(
|
||||
`SELECT id, step, data
|
||||
FROM onboarding_drafts
|
||||
WHERE user_id=?
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 1`,
|
||||
[req.id]
|
||||
);
|
||||
base = row || null;
|
||||
}
|
||||
|
||||
// ---- 2) Parse prior JSON safely
|
||||
let prev = {};
|
||||
if (base?.data != null) {
|
||||
try {
|
||||
if (typeof base.data === 'string') prev = JSON.parse(base.data);
|
||||
else if (Buffer.isBuffer(base.data)) prev = JSON.parse(base.data.toString('utf8'));
|
||||
else if (typeof base.data === 'object') prev = base.data;
|
||||
} catch { prev = {}; }
|
||||
}
|
||||
|
||||
// ---- 3) Section-wise shallow merge (prev + incoming)
|
||||
const merged = mergeDraft(prev, (incoming && typeof incoming === 'object') ? incoming : {});
|
||||
|
||||
// ---- 3.5) Refuse empty drafts so we don't wipe real data
|
||||
const isEmptyObject = (o) => o && typeof o === 'object' && !Array.isArray(o) && Object.keys(o).length === 0;
|
||||
const allSubsectionsEmpty =
|
||||
isEmptyObject(merged) ||
|
||||
(
|
||||
typeof merged === 'object' &&
|
||||
['careerData','financialData','collegeData'].every(k => !merged[k] || isEmptyObject(merged[k]))
|
||||
);
|
||||
if (allSubsectionsEmpty) {
|
||||
return res.status(400).json({ error: 'empty_draft' });
|
||||
}
|
||||
|
||||
console.log('[draft-upsert]', {
|
||||
userId : req.id,
|
||||
draftId : draftId,
|
||||
step : finalStep,
|
||||
incoming : Object.keys(incoming || {}).sort(),
|
||||
mergedKeys: Object.keys(merged || {}).sort(),
|
||||
});
|
||||
|
||||
// ---- 4) Final id/step and upsert
|
||||
const draftId = base?.id || id || uuidv4();
|
||||
const finalStep = Number.isInteger(step) ? step : (parseInt(step,10) || base?.step || 0);
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO onboarding_drafts (user_id, id, step, data)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
step = VALUES(step),
|
||||
data = VALUES(data),
|
||||
updated_at = CURRENT_TIMESTAMP`,
|
||||
[req.id, draftId, finalStep, JSON.stringify(merged)]
|
||||
);
|
||||
|
||||
return res.json({ id: draftId, step: finalStep });
|
||||
} catch (e) {
|
||||
console.error('draft upsert failed:', e?.message || e);
|
||||
return res.status(500).json({ error: 'draft_upsert_failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// unchanged
|
||||
function mergeDraft(a = {}, b = {}) {
|
||||
const out = { ...a };
|
||||
for (const k of Object.keys(b || {})) {
|
||||
const left = a[k];
|
||||
const right = b[k];
|
||||
if (
|
||||
left && typeof left === 'object' && !Array.isArray(left) &&
|
||||
right && typeof right === 'object' && !Array.isArray(right)
|
||||
) {
|
||||
out[k] = { ...left, ...right };
|
||||
} else {
|
||||
out[k] = right;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// DELETE draft (after finishing / cancelling)
|
||||
app.delete('/api/premium/onboarding/draft', authenticatePremiumUser, async (req, res) => {
|
||||
await pool.query('DELETE FROM onboarding_drafts WHERE user_id=?', [req.id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
//Stripe webhook endpoint (raw body)
|
||||
// 1) Raw body parser (must be before express.json)
|
||||
app.post(
|
||||
'/api/premium/stripe/webhook',
|
||||
express.raw({ type: 'application/json' }),
|
||||
@ -808,14 +674,153 @@ app.post(
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
// 2) Basic middlewares
|
||||
app.use(helmet({ contentSecurityPolicy:false, crossOriginEmbedderPolicy:false }));
|
||||
|
||||
//leave below Stripe webhook
|
||||
app.use(express.json({ limit: '5mb' }));
|
||||
|
||||
|
||||
//*PremiumOnboarding draft
|
||||
// GET current user's draft
|
||||
app.get('/api/premium/onboarding/draft', authenticatePremiumUser, async (req, res) => {
|
||||
const [[row]] = await pool.query(
|
||||
`SELECT id, step, data
|
||||
FROM onboarding_drafts
|
||||
WHERE user_id=?
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 1`,
|
||||
[req.id]
|
||||
);
|
||||
return res.json(row || null);
|
||||
});
|
||||
|
||||
// POST upsert draft (ID-agnostic, partial merge, 1 draft per user)
|
||||
app.post('/api/premium/onboarding/draft', authenticatePremiumUser, async (req, res) => {
|
||||
try {
|
||||
|
||||
// ---- 0) Harden req.body and incoming shapes (accept object *or* JSON string)
|
||||
let body = {};
|
||||
if (req && req.body != null) {
|
||||
if (typeof req.body === 'object') {
|
||||
body = req.body;
|
||||
} else if (typeof req.body === 'string') {
|
||||
try { body = JSON.parse(req.body); } catch { body = {}; }
|
||||
}
|
||||
}
|
||||
let { id, step } = body;
|
||||
|
||||
// Accept either {data:{careerData/financialData/collegeData}} or section keys at top level
|
||||
let incoming = {};
|
||||
if (body.data != null) {
|
||||
if (typeof body.data === 'string') {
|
||||
try { incoming = JSON.parse(body.data); } catch { incoming = {}; }
|
||||
} else if (typeof body.data === 'object') {
|
||||
incoming = body.data;
|
||||
}
|
||||
}
|
||||
// If callers provided sections directly (EducationalProgramsPage),
|
||||
// lift them into the data envelope without crashing if body is blank.
|
||||
['careerData','financialData','collegeData'].forEach(k => {
|
||||
if (Object.prototype.hasOwnProperty.call(body, k)) {
|
||||
if (!incoming || typeof incoming !== 'object') incoming = {};
|
||||
incoming[k] = body[k];
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 1) Base draft: by id (if provided) else latest for this user
|
||||
let base = null;
|
||||
if (id) {
|
||||
const [[row]] = await pool.query(
|
||||
`SELECT id, step, data FROM onboarding_drafts WHERE user_id=? AND id=? LIMIT 1`,
|
||||
[req.id, id]
|
||||
);
|
||||
base = row || null;
|
||||
} else {
|
||||
const [[row]] = await pool.query(
|
||||
`SELECT id, step, data
|
||||
FROM onboarding_drafts
|
||||
WHERE user_id=?
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 1`,
|
||||
[req.id]
|
||||
);
|
||||
base = row || null;
|
||||
}
|
||||
|
||||
// ---- 2) Parse prior JSON safely
|
||||
let prev = {};
|
||||
if (base?.data != null) {
|
||||
try {
|
||||
if (typeof base.data === 'string') prev = JSON.parse(base.data);
|
||||
else if (Buffer.isBuffer(base.data)) prev = JSON.parse(base.data.toString('utf8'));
|
||||
else if (typeof base.data === 'object') prev = base.data;
|
||||
} catch { prev = {}; }
|
||||
}
|
||||
|
||||
// ---- 3) Section-wise shallow merge (prev + incoming)
|
||||
const merged = mergeDraft(prev, (incoming && typeof incoming === 'object') ? incoming : {});
|
||||
|
||||
// ---- 3.5) Only reject when there's truly no incoming content AND no prior draft
|
||||
const isPlainObj = (o) => o && typeof o === 'object' && !Array.isArray(o);
|
||||
const isEmptyObj = (o) => isPlainObj(o) && Object.keys(o).length === 0;
|
||||
const hasIncoming = isPlainObj(incoming) && !isEmptyObj(incoming);
|
||||
const hasPrior = !!base && isPlainObj(prev) && !isEmptyObj(prev);
|
||||
if (!hasIncoming && !hasPrior) {
|
||||
return res.status(400).json({ error: 'empty_draft' });
|
||||
}
|
||||
|
||||
// ---- 4) Final id/step and upsert
|
||||
const draftId = base?.id || id || uuidv4();
|
||||
const finalStep = Number.isInteger(step) ? step : (parseInt(step,10) || base?.step || 0);
|
||||
console.log('[draft-upsert]', {
|
||||
userId : req.id,
|
||||
draftId : draftId,
|
||||
step : finalStep,
|
||||
incoming : Object.keys(incoming || {}).sort(),
|
||||
mergedKeys: Object.keys(merged || {}).sort(),
|
||||
});
|
||||
await pool.query(
|
||||
`INSERT INTO onboarding_drafts (user_id, id, step, data)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
step = VALUES(step),
|
||||
data = VALUES(data),
|
||||
updated_at = CURRENT_TIMESTAMP`,
|
||||
[req.id, draftId, finalStep, JSON.stringify(merged)]
|
||||
);
|
||||
|
||||
return res.json({ id: draftId, step: finalStep });
|
||||
} catch (e) {
|
||||
console.error('draft upsert failed:', e?.message || e);
|
||||
return res.status(500).json({ error: 'draft_upsert_failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// unchanged
|
||||
function mergeDraft(a = {}, b = {}) {
|
||||
const out = { ...a };
|
||||
for (const k of Object.keys(b || {})) {
|
||||
const left = a[k];
|
||||
const right = b[k];
|
||||
if (
|
||||
left && typeof left === 'object' && !Array.isArray(left) &&
|
||||
right && typeof right === 'object' && !Array.isArray(right)
|
||||
) {
|
||||
out[k] = { ...left, ...right };
|
||||
} else {
|
||||
out[k] = right;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// DELETE draft (after finishing / cancelling)
|
||||
app.delete('/api/premium/onboarding/draft', authenticatePremiumUser, async (req, res) => {
|
||||
await pool.query('DELETE FROM onboarding_drafts WHERE user_id=?', [req.id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* ─── Require critical env vars ─────────────────────────────── */
|
||||
if (!process.env.CORS_ALLOWED_ORIGINS) {
|
||||
console.error('FATAL CORS_ALLOWED_ORIGINS is not set');
|
||||
|
@ -120,6 +120,14 @@ useEffect(() => {
|
||||
if (!ok) {
|
||||
// bounce back to where the user was
|
||||
navigate(prevPathRef.current, { replace: true });
|
||||
} else {
|
||||
// user chose to leave: clear client pointer and server draft immediately
|
||||
try {
|
||||
safeLocal.clearMany(['premiumOnboardingPointer', 'premiumOnboardingState']);
|
||||
} catch {}
|
||||
(async () => {
|
||||
try { await api.delete('/api/premium/onboarding/draft'); } catch {}
|
||||
})();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname]);
|
||||
|
@ -188,7 +188,7 @@ const handleSelectSchool = async (school) => {
|
||||
|
||||
// 1) normalize college fields
|
||||
const selected_school = school?.INSTNM || '';
|
||||
const selected_program = (school?.CIPDESC || '').replace(/\.\s*$/, '');
|
||||
const selected_program = (school?.CIPDESC || '');
|
||||
const program_type = school?.CREDDESC || '';
|
||||
const unit_id = school?.UNITID || '';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user