Fix wizard-submitted profile data being dropped and wizard schemas saving with no fields
All checks were successful
build-and-release / build (customers) (push) Successful in 11s
build-and-release / build (catering-services) (push) Successful in 13s
build-and-release / build (companies) (push) Successful in 16s
build-and-release / build (developers) (push) Successful in 16s
build-and-release / build (cron) (push) Successful in 17s
build-and-release / build (employees) (push) Successful in 16s
build-and-release / build (fitness-trainers) (push) Successful in 6s
build-and-release / build (gateway) (push) Successful in 5s
build-and-release / build (graphic-designers) (push) Successful in 7s
build-and-release / build (job-seekers) (push) Successful in 7s
build-and-release / build (jobs) (push) Successful in 5s
build-and-release / build (makeup-artists) (push) Successful in 5s
build-and-release / build (payments) (push) Successful in 7s
build-and-release / build (photographers) (push) Successful in 4s
build-and-release / build (tutors) (push) Successful in 5s
build-and-release / build (social-media-managers) (push) Successful in 6s
build-and-release / build (ugc-content-creators) (push) Successful in 5s
build-and-release / build (video-editors) (push) Successful in 4s
build-and-release / build (users) (push) Successful in 2m31s
All checks were successful
build-and-release / build (customers) (push) Successful in 11s
build-and-release / build (catering-services) (push) Successful in 13s
build-and-release / build (companies) (push) Successful in 16s
build-and-release / build (developers) (push) Successful in 16s
build-and-release / build (cron) (push) Successful in 17s
build-and-release / build (employees) (push) Successful in 16s
build-and-release / build (fitness-trainers) (push) Successful in 6s
build-and-release / build (gateway) (push) Successful in 5s
build-and-release / build (graphic-designers) (push) Successful in 7s
build-and-release / build (job-seekers) (push) Successful in 7s
build-and-release / build (jobs) (push) Successful in 5s
build-and-release / build (makeup-artists) (push) Successful in 5s
build-and-release / build (payments) (push) Successful in 7s
build-and-release / build (photographers) (push) Successful in 4s
build-and-release / build (tutors) (push) Successful in 5s
build-and-release / build (social-media-managers) (push) Successful in 6s
build-and-release / build (ugc-content-creators) (push) Successful in 5s
build-and-release / build (video-editors) (push) Successful in 4s
build-and-release / build (users) (push) Successful in 2m31s
- GET /api/profile for COMPANY only ever returned company_name, silently discarding every other field the wizard (and PATCH) already saved — the profile page looked empty after approval. Select and return all company_profiles columns. - The generic professional-role PATCH path (photographer, tutor, etc.) overwrote custom_data wholesale instead of merging, so RoleWizard's two sequential PATCH calls (portfolio step, then basic+documents step) clobbered each other. Read-merge-write instead, matching the existing JOB_SEEKER/CUSTOMER pattern. - create_onboarding_config now rejects schemas with enableWizardFlow=true but no steps, or a non-review step with no fields — the root cause of a wizard rendering with nothing to fill in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a24e03fd02
commit
fd889efa22
2 changed files with 102 additions and 8 deletions
|
|
@ -485,6 +485,51 @@ mod tests {
|
|||
|
||||
|
||||
|
||||
/// Rejects schemas that would enable the wizard flow with nothing for the
|
||||
/// user to fill in: an empty `steps` array, or any non-review step whose
|
||||
/// `fields` array is empty. Guards against the admin editor's "Enable
|
||||
/// verification wizard flow" checkbox being saved before any fields are
|
||||
/// added to a step.
|
||||
fn validate_onboarding_schema(schema_json: &serde_json::Value) -> Result<(), String> {
|
||||
let enable_wizard_flow = schema_json
|
||||
.get("enableWizardFlow")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
if !enable_wizard_flow {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let steps = schema_json
|
||||
.get("steps")
|
||||
.and_then(|v| v.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if steps.is_empty() {
|
||||
return Err("Cannot enable the wizard flow with no steps configured.".to_string());
|
||||
}
|
||||
|
||||
for step in &steps {
|
||||
let step_type = step.get("type").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if step_type == "review" {
|
||||
continue;
|
||||
}
|
||||
let fields_empty = step
|
||||
.get("fields")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|f| f.is_empty())
|
||||
.unwrap_or(true);
|
||||
if fields_empty {
|
||||
let title = step.get("title").and_then(|v| v.as_str()).unwrap_or("(untitled step)");
|
||||
return Err(format!(
|
||||
"Cannot enable the wizard flow: step \"{}\" has no fields.",
|
||||
title
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_onboarding_config(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -493,6 +538,9 @@ async fn create_onboarding_config(
|
|||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
if let Err(msg) = validate_onboarding_schema(&payload.schema_json) {
|
||||
return Err((StatusCode::BAD_REQUEST, msg));
|
||||
}
|
||||
match ConfigRepository::create_onboarding_config(&state.pool, payload).await {
|
||||
Ok(config) => Ok((StatusCode::CREATED, Json(config))),
|
||||
Err(e) => Err((
|
||||
|
|
|
|||
|
|
@ -242,7 +242,10 @@ async fn get_profile(
|
|||
|
||||
if role_key == "COMPANY" {
|
||||
let row = sqlx::query(
|
||||
r#"SELECT company_name, status, updated_at FROM company_profiles WHERE user_id = $1"#,
|
||||
r#"SELECT company_name, contact_email, contact_phone, website_url,
|
||||
address_line1, city, state, postal_code, gst_number,
|
||||
status, updated_at
|
||||
FROM company_profiles WHERE user_id = $1"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
@ -252,12 +255,30 @@ async fn get_profile(
|
|||
Ok(Some(r)) => {
|
||||
use sqlx::Row;
|
||||
let name: Option<String> = r.try_get("company_name").ok();
|
||||
let email: Option<String> = r.try_get("contact_email").ok();
|
||||
let phone: Option<String> = r.try_get("contact_phone").ok();
|
||||
let website: Option<String> = r.try_get("website_url").ok();
|
||||
let address: Option<String> = r.try_get("address_line1").ok();
|
||||
let city: Option<String> = r.try_get("city").ok();
|
||||
let state_val: Option<String> = r.try_get("state").ok();
|
||||
let postal: Option<String> = r.try_get("postal_code").ok();
|
||||
let gst: Option<String> = r.try_get("gst_number").ok();
|
||||
let status: String = r.try_get("status").unwrap_or_default();
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"role_key": role_key,
|
||||
"profile_data": { "company_name": name },
|
||||
"profile_data": {
|
||||
"company_name": name,
|
||||
"company_email": email,
|
||||
"company_phone": phone,
|
||||
"website": website,
|
||||
"address": address,
|
||||
"location": city,
|
||||
"state": state_val,
|
||||
"pin_code": postal,
|
||||
"gst_number": gst,
|
||||
},
|
||||
"verification_status": status,
|
||||
})),
|
||||
)
|
||||
|
|
@ -560,6 +581,36 @@ async fn save_profile(
|
|||
}
|
||||
};
|
||||
|
||||
let user_role_profile_id = match get_or_create_user_role_profile_id(&state.pool, auth.user_id, &role_key).await {
|
||||
Ok(id) => id,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
// Shallow-merge onto existing custom_data instead of overwriting: RoleWizard
|
||||
// issues separate PATCH calls for the portfolio step and the basic/documents
|
||||
// steps, so a wholesale overwrite here would let the second call silently
|
||||
// wipe out whatever the first one just saved.
|
||||
let existing_custom_data: serde_json::Value = sqlx::query_scalar(
|
||||
&format!(r#"SELECT custom_data FROM {} WHERE user_role_profile_id = $1"#, table),
|
||||
)
|
||||
.bind(user_role_profile_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
|
||||
let mut merged = match existing_custom_data {
|
||||
serde_json::Value::Object(map) => map,
|
||||
_ => Default::default(),
|
||||
};
|
||||
if let serde_json::Value::Object(incoming) = &input.profile_data {
|
||||
for (k, v) in incoming {
|
||||
merged.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
let merged_custom_data = serde_json::Value::Object(merged);
|
||||
|
||||
let query = format!(
|
||||
r#"
|
||||
INSERT INTO {table} (user_id, user_role_profile_id, custom_data, status, updated_at)
|
||||
|
|
@ -570,15 +621,10 @@ async fn save_profile(
|
|||
"#
|
||||
);
|
||||
|
||||
let user_role_profile_id = match get_or_create_user_role_profile_id(&state.pool, auth.user_id, &role_key).await {
|
||||
Ok(id) => id,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
match sqlx::query(&query)
|
||||
.bind(auth.user_id)
|
||||
.bind(user_role_profile_id)
|
||||
.bind(&input.profile_data)
|
||||
.bind(&merged_custom_data)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue