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
|
*.crt
|
||||||
*.pfx
|
*.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';
|
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(
|
app.post(
|
||||||
'/api/premium/stripe/webhook',
|
'/api/premium/stripe/webhook',
|
||||||
express.raw({ type: 'application/json' }),
|
express.raw({ type: 'application/json' }),
|
||||||
@ -808,14 +674,153 @@ app.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 2) Basic middlewares
|
// 2) Basic middlewares
|
||||||
app.use(helmet({ contentSecurityPolicy:false, crossOriginEmbedderPolicy:false }));
|
app.use(helmet({ contentSecurityPolicy:false, crossOriginEmbedderPolicy:false }));
|
||||||
|
|
||||||
//leave below Stripe webhook
|
//leave below Stripe webhook
|
||||||
app.use(express.json({ limit: '5mb' }));
|
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 ─────────────────────────────── */
|
/* ─── Require critical env vars ─────────────────────────────── */
|
||||||
if (!process.env.CORS_ALLOWED_ORIGINS) {
|
if (!process.env.CORS_ALLOWED_ORIGINS) {
|
||||||
console.error('FATAL CORS_ALLOWED_ORIGINS is not set');
|
console.error('FATAL CORS_ALLOWED_ORIGINS is not set');
|
||||||
|
@ -120,6 +120,14 @@ useEffect(() => {
|
|||||||
if (!ok) {
|
if (!ok) {
|
||||||
// bounce back to where the user was
|
// bounce back to where the user was
|
||||||
navigate(prevPathRef.current, { replace: true });
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
@ -188,7 +188,7 @@ const handleSelectSchool = async (school) => {
|
|||||||
|
|
||||||
// 1) normalize college fields
|
// 1) normalize college fields
|
||||||
const selected_school = school?.INSTNM || '';
|
const selected_school = school?.INSTNM || '';
|
||||||
const selected_program = (school?.CIPDESC || '').replace(/\.\s*$/, '');
|
const selected_program = (school?.CIPDESC || '');
|
||||||
const program_type = school?.CREDDESC || '';
|
const program_type = school?.CREDDESC || '';
|
||||||
const unit_id = school?.UNITID || '';
|
const unit_id = school?.UNITID || '';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user