properly fixed user upto [200~Business Context Questions

This commit is contained in:
Chandini 2025-09-11 15:45:44 +05:30
parent 909d446d77
commit 49a19447dc
5 changed files with 215 additions and 115 deletions

View File

@ -375,6 +375,19 @@ app.use('/api/requirements',
serviceRouter.createServiceProxy(serviceTargets.REQUIREMENT_PROCESSOR_URL, 'requirement-processor')
);
// Questions (Requirement Processor) - expose /api/questions via gateway
// Rewrites /api/questions/* -> /api/v1/* at the Requirement Processor
app.use('/api/questions',
createServiceLimiter(300),
// Allow unauthenticated access for generating questions (public step in builder)
(req, res, next) => next(),
serviceRouter.createServiceProxy(
serviceTargets.REQUIREMENT_PROCESSOR_URL,
'requirement-processor-questions',
{ pathRewrite: { '^/api/questions': '/api/v1' } }
)
);
// Tech Stack Selector Service
app.use('/api/tech-stack',
createServiceLimiter(200),

View File

@ -42,13 +42,55 @@ class CustomFeature {
static async create(data) {
const id = uuidv4();
// Normalize JSONB-like fields to ensure valid JSON is sent to PG
const normalizeJsonb = (value) => {
if (value === undefined || value === null || value === '') return null;
if (typeof value === 'string') {
try { return JSON.parse(value); } catch {
// Accept plain strings by storing as JSON string (quoted)
return String(value);
}
}
return value;
};
const toJsonbSafe = (value) => {
try {
const v = normalizeJsonb(value);
// Only objects/arrays/strings are JSON-serializable; numbers/booleans fine too
// But if we get something unexpected, fallback to null
if (v === null || v === undefined) return null;
// If array, coerce entries away from undefined
if (Array.isArray(v)) {
return v.map((item) => (item === undefined ? null : item));
}
// Plain object or primitive
return v;
} catch {
return null;
}
};
const businessRules = toJsonbSafe(data.business_rules);
const technicalRequirements = toJsonbSafe(data.technical_requirements);
// Debug logging to trace JSON payloads that will be cast to jsonb
try {
console.log('🧪 [CustomFeature.create] JSON payloads:', {
businessRulesPreview: businessRules === null ? null : JSON.stringify(businessRules).slice(0, 200),
technicalRequirementsPreview: technicalRequirements === null ? null : JSON.stringify(technicalRequirements).slice(0, 200)
});
} catch (_) {}
const query = `
INSERT INTO custom_features (
id, template_id, template_type, name, description, complexity,
business_rules, technical_requirements, approved, usage_count, created_by_user_session,
status, admin_notes, admin_reviewed_at, admin_reviewed_by, canonical_feature_id, similarity_score,
created_at, updated_at
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,DEFAULT,DEFAULT)
) VALUES (
$1,$2,$3,$4,$5,$6,
$7::jsonb,$8::jsonb,$9,$10,$11,
$12,$13,$14,$15,$16,$17,
DEFAULT,DEFAULT
)
RETURNING *
`;
const values = [
@ -58,8 +100,8 @@ class CustomFeature {
data.name,
data.description || null,
data.complexity,
data.business_rules || null,
data.technical_requirements || null,
(() => { try { return businessRules === null ? null : JSON.stringify(businessRules); } catch { return null; } })(),
(() => { try { return technicalRequirements === null ? null : JSON.stringify(technicalRequirements); } catch { return null; } })(),
data.approved ?? false,
data.usage_count ?? 1,
data.created_by_user_session || null,
@ -75,6 +117,24 @@ class CustomFeature {
}
static async update(id, updates) {
// Normalize JSONB-like fields before constructing the query
const normalizeJsonb = (value) => {
if (value === undefined) return undefined;
if (value === null || value === '') return null;
if (typeof value === 'string') {
try { return JSON.parse(value); } catch {
// Accept plain strings by storing as JSON string (quoted)
return String(value);
}
}
return value;
};
if (updates && Object.prototype.hasOwnProperty.call(updates, 'business_rules')) {
updates.business_rules = normalizeJsonb(updates.business_rules);
}
if (updates && Object.prototype.hasOwnProperty.call(updates, 'technical_requirements')) {
updates.technical_requirements = normalizeJsonb(updates.technical_requirements);
}
const fields = [];
const values = [];
let idx = 1;
@ -85,10 +145,16 @@ class CustomFeature {
];
for (const k of allowed) {
if (updates[k] !== undefined) {
if (k === 'business_rules' || k === 'technical_requirements') {
fields.push(`${k} = $${idx++}::jsonb`);
const v = updates[k] === null ? null : JSON.stringify(updates[k]);
values.push(v);
} else {
fields.push(`${k} = $${idx++}`);
values.push(updates[k]);
}
}
}
if (fields.length === 0) return await CustomFeature.getById(id);
const query = `UPDATE custom_features SET ${fields.join(', ')}, updated_at = NOW() WHERE id = $${idx} RETURNING *`;
values.push(id);

View File

@ -367,25 +367,10 @@ router.put('/:id', async (req, res) => {
let updated;
if (isCustomFeature) {
// Update custom feature
// Update custom feature (only in custom_features table)
updated = await CustomFeature.update(id, updateData);
// Mirror update into template_features where feature_id = `custom_<id>`
try {
const featureId = `custom_${id}`;
const mirroredExisting = await Feature.getByFeatureId(existing.template_id, featureId);
if (mirroredExisting) {
await Feature.update(mirroredExisting.id, {
name: updateData.name ?? mirroredExisting.name,
description: updateData.description ?? mirroredExisting.description,
complexity: updateData.complexity ?? mirroredExisting.complexity,
});
}
} catch (mirrorErr) {
console.error('Failed to mirror custom feature update:', mirrorErr.message);
}
} else {
// Update regular feature
// Update regular feature (only in template_features table)
updated = await Feature.update(id, updateData);
}
@ -427,6 +412,28 @@ router.delete('/:id', async (req, res) => {
router.post('/custom', async (req, res) => {
try {
const data = req.body || {}
// Normalize and validate JSON-like fields to avoid invalid input syntax for type json
const tryParseJson = (value) => {
if (value === undefined || value === null || value === '') return null;
if (typeof value === 'string') {
const trimmed = value.trim();
// Only attempt to parse if it looks like JSON; otherwise pass through as plain string
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try { return JSON.parse(trimmed); } catch {
// If it looks like JSON but fails to parse, fallback to null so we don't send invalid jsonb
return null;
}
}
// Allow plain strings; model will stringify safely for jsonb columns
return trimmed;
}
return value;
}
// Be lenient: normalize but do not reject if strings are not valid JSON
if (data.business_rules !== undefined) data.business_rules = tryParseJson(data.business_rules)
if (data.logic_rules !== undefined) data.logic_rules = tryParseJson(data.logic_rules)
if (data.technical_requirements !== undefined) data.technical_requirements = tryParseJson(data.technical_requirements)
console.log('🔍 Custom feature creation request:', { template_id: data.template_id, name: data.name, complexity: data.complexity, description: data.description })
const required = ['template_id', 'name', 'complexity']
for (const f of required) {
@ -466,6 +473,9 @@ router.post('/custom', async (req, res) => {
name: data.name,
description: data.description,
complexity: data.complexity,
// Persist incoming structured fields on the row as well
business_rules: data.business_rules ?? (data.logic_rules ? [{ requirement: 'Aggregated', rules: data.logic_rules }] : null),
technical_requirements: data.technical_requirements ?? null,
approved: false,
usage_count: 1,
created_by_user_session: data.created_by_user_session,
@ -476,27 +486,27 @@ router.post('/custom', async (req, res) => {
try { await AdminNotification.notifyNewFeature(created.id, created.name); } catch (e) { console.error('⚠️ Failed to create admin notification:', e.message); }
try {
await Feature.create({
template_id: data.template_id,
feature_id: data.id,
name: data.name,
description: data.description,
feature_type: 'custom',
complexity: data.complexity,
display_order: 999,
is_default: false,
created_by_user: true
})
} catch (mirrorErr) { console.error('Failed to mirror custom feature into template_features:', mirrorErr.message) }
// Custom features are only stored in custom_features table, not mirrored to template_features
// Persist aggregated rules
// Persist aggregated rules (using actual custom feature ID)
try {
// Prefer structured business_rules; fallback to flat logic_rules
const rules = (data.business_rules ?? data.logic_rules ?? []);
await FeatureBusinessRules.upsert(data.template_id, `custom_${created.id}`, rules);
if (Array.isArray(rules) && rules.length > 0) {
await FeatureBusinessRules.upsert(data.template_id, created.id, rules);
} else {
// If nothing to upsert, keep a copy on the custom_features row
try { await CustomFeature.update(created.id, { business_rules: rules }); } catch {}
}
} catch (ruleErr) {
console.error('⚠️ Failed to persist custom feature business rules:', ruleErr.message);
// Fallback: persist on the custom_features row so edit can still load them
console.error('⚠️ Failed to persist custom feature business rules (feature_business_rules). Falling back to custom_features.business_rules:', ruleErr.message);
try {
const fallbackRules = (data.business_rules ?? data.logic_rules ?? []);
await CustomFeature.update(created.id, { business_rules: fallbackRules });
} catch (fallbackErr) {
console.error('⚠️ Fallback save to custom_features.business_rules also failed:', fallbackErr.message);
}
}
const response = { success: true, data: created, message: `Custom feature '${created.name}' created successfully and submitted for admin review` };
@ -545,20 +555,8 @@ router.put('/custom/:id', async (req, res) => {
if (!existing) return res.status(404).json({ success: false, error: 'Not found' });
const updates = req.body || {}
const updated = await CustomFeature.update(id, updates);
// Mirror update into template_features where feature_id = `custom_<id>`
try {
const featureId = `custom_${id}`
const mirroredExisting = await Feature.getByFeatureId(existing.template_id, featureId)
if (mirroredExisting) {
await Feature.update(mirroredExisting.id, {
name: updates.name ?? mirroredExisting.name,
description: updates.description ?? mirroredExisting.description,
complexity: updates.complexity ?? mirroredExisting.complexity,
})
}
} catch (mirrorErr) {
console.error('Failed to mirror custom feature update:', mirrorErr.message)
}
// Custom features are only stored in custom_features table, no mirroring needed
res.json({ success: true, data: updated, message: `Custom feature '${updated.name}' updated successfully` });
} catch (e) {
res.status(500).json({ success: false, error: 'Failed to update custom feature', message: e.message });
@ -569,46 +567,24 @@ router.put('/custom/:id', async (req, res) => {
router.delete('/custom/:id', async (req, res) => {
try {
const { id } = req.params;
// const rawId = String(id).replace(/^custom_/, '')
// Try deleting from custom_features first
let existing = await CustomFeature.getById(id);
if (existing) {
await CustomFeature.delete(id);
// Remove mirrored template_features with feature_id = `custom_<id>`
try {
const featureId = id
const mirroredExisting = await Feature.getByFeatureId(existing.template_id, featureId)
if (mirroredExisting) {
await Feature.delete(mirroredExisting.id)
// Find and delete from custom_features table
const existing = await CustomFeature.getById(id);
if (!existing) {
return res.status(404).json({ success: false, error: 'Not found', message: 'Custom feature not found' });
}
// Delete the custom feature
await CustomFeature.delete(id);
// Cleanup business rules if present
try {
await database.query('DELETE FROM feature_business_rules WHERE template_id = $1 AND feature_id = $2', [existing.template_id, featureId])
} catch (cleanupErr) { console.error('Failed to cleanup business rules:', cleanupErr.message) }
} catch (mirrorErr) {
console.error('Failed to mirror custom feature delete:', mirrorErr.message)
await database.query('DELETE FROM feature_business_rules WHERE template_id = $1 AND feature_id = $2', [existing.template_id, id])
} catch (cleanupErr) {
console.error('Failed to cleanup business rules:', cleanupErr.message)
}
return res.json({ success: true, message: `Custom feature '${existing.name}' deleted successfully` });
}
// Fallback: handle case where only mirrored template_features exists or client sent prefixed id
try {
const prefixed = id.startsWith('custom_') ? id : `custom_${rawId}`
const tf = await database.query('SELECT id, template_id, name FROM template_features WHERE feature_id = $1', [prefixed])
if (tf.rows.length > 0) {
const row = tf.rows[0]
await Feature.delete(row.id)
try {
await database.query('DELETE FROM feature_business_rules WHERE template_id = $1 AND feature_id = $2', [row.template_id, prefixed])
} catch (cleanupErr) { console.error('Failed to cleanup business rules:', cleanupErr.message) }
return res.json({ success: true, message: `Mirrored feature '${row.name}' deleted successfully` })
}
} catch (fallbackErr) {
console.error('Fallback delete check failed:', fallbackErr.message)
}
return res.status(404).json({ success: false, error: 'Not found', message: 'Custom feature not found' });
} catch (e) {
res.status(500).json({ success: false, error: 'Failed to delete custom feature', message: e.message });
}

View File

@ -512,40 +512,85 @@ router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/f
console.log(`✅ Template found: ${templateCheck.rows[0].title} (${templateCheck.rows[0].template_type})`);
let features = [];
// Fetch features from both tables for proper separation
console.log('📋 Fetching features from both template_features and custom_features tables');
if (templateCheck.rows[0].template_type === 'custom') {
// For custom templates, get features from custom_features table
console.log('📋 Fetching features from custom_features table for custom template');
const customFeaturesQuery = `
// Get default/suggested features from template_features table
const defaultFeatures = await Feature.getByTemplateId(id);
console.log(`📊 Found ${defaultFeatures.length} default/suggested features`);
// Get custom features from custom_features table with business rules (if table exists)
// Some environments may not have run the feature_business_rules migration yet. Probe first.
const fbrExistsProbe = await database.query("SELECT to_regclass('public.feature_business_rules') AS tbl");
const hasFbrTable = !!(fbrExistsProbe.rows && fbrExistsProbe.rows[0] && fbrExistsProbe.rows[0].tbl);
const customFeaturesQuery = hasFbrTable
? `
SELECT
cf.id,
cf.template_id,
cf.name,
cf.description,
cf.complexity,
cf.business_rules,
cf.technical_requirements,
'custom' as feature_type,
cf.created_at,
cf.updated_at,
cf.status,
cf.approved
cf.approved,
cf.usage_count,
0 as user_rating,
false as is_default,
true as created_by_user,
fbr.business_rules as additional_business_rules
FROM custom_features cf
LEFT JOIN feature_business_rules fbr
ON cf.template_id = fbr.template_id
AND (
fbr.feature_id = (cf.id::text)
OR fbr.feature_id = ('custom_' || cf.id::text)
)
WHERE cf.template_id = $1
ORDER BY cf.created_at DESC
`
: `
SELECT
cf.id,
cf.template_id,
cf.name,
cf.description,
cf.complexity,
cf.business_rules,
cf.technical_requirements,
'custom' as feature_type,
cf.created_at,
cf.updated_at,
cf.status,
cf.approved,
cf.usage_count,
0 as user_rating,
false as is_default,
true as created_by_user,
NULL::jsonb as additional_business_rules
FROM custom_features cf
WHERE cf.template_id = $1
ORDER BY cf.created_at DESC
`;
const customFeaturesResult = await database.query(customFeaturesQuery, [id]);
features = customFeaturesResult.rows;
} else {
// For default templates, get features from template_features table
console.log('📋 Fetching features from template_features table for default template');
features = await Feature.getByTemplateId(id);
}
const customFeatures = customFeaturesResult.rows;
console.log(`📊 Found ${customFeatures.length} custom features`);
// Combine both types of features
const features = [...defaultFeatures, ...customFeatures];
res.json({
success: true,
data: features,
count: features.length,
message: `Found ${features.length} features for ${templateCheck.rows[0].template_type} template`,
defaultFeaturesCount: defaultFeatures.length,
customFeaturesCount: customFeatures.length,
message: `Found ${defaultFeatures.length} default/suggested features and ${customFeatures.length} custom features`,
templateInfo: templateCheck.rows[0]
});
} catch (error) {

View File

@ -28,7 +28,7 @@ export default function BusinessQuestionsScreen() {
console.log('🚀 Generating comprehensive business questions for integrated system:', selectedFeatures);
// Call the new comprehensive endpoint
const response = await fetch('http://localhost:8001/api/v1/generate-comprehensive-business-questions', {
const response = await fetch('http://localhost:8000/api/v1/generate-comprehensive-business-questions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',