Unknown VS code: ' . vs01_h($vs_code) . ''; } $schema = $schemas[$vs_code]; // Determine perspective $perspective_map = [ 'participant' => 'homeowner', 'professional' => 'professional', 'operator' => 'public_record', ]; $perspective_key = $perspective_map[$access] ?? null; if (!$perspective_key) { return '
Invalid access state for submission.
'; } $perspective = $schema['perspectives'][$perspective_key] ?? null; if (!$perspective) { return '
No perspective defined for this access state.
'; } // Collect and sanitize submitted fields $raw_fields = $_POST['fields'] ?? []; $fields = []; foreach ($perspective['fields'] ?? [] as $field_def) { $id = $field_def['id'] ?? ''; if (!$id) continue; $type = $field_def['type'] ?? 'text'; if ($type === 'multiselect') { $val = isset($raw_fields[$id]) && is_array($raw_fields[$id]) ? array_map('strval', $raw_fields[$id]) : []; $fields[$id] = json_encode($val); } else { $fields[$id] = isset($raw_fields[$id]) ? substr(strip_tags((string) $raw_fields[$id]), 0, 8192) : ''; } } // Validate required fields $errors = vs01_validate_required($perspective, $fields); if ($errors) { $out = '
Please complete the required fields:
'; // Re-render form with submitted values and errors $out .= vs01_render_vs_form_with_values($association_slug, $vs_code, $access, $fields); return $out; } // Build spool envelope $config = vs01_load_config(); $assoc = $config['associations'][$association_slug] ?? []; $envelope = vs01_build_spool_envelope( $vs_code, $perspective_key, $fields, $association_slug, $assoc['channel_id'] ?? '' ); // POST to orchestrator $result = vs01_post_to_orchestrator($envelope); if ($result === true) { // THROWAWAY — replace with TMP review redirect or confirmation page return '
Your record for ' . vs01_h($vs_code) . ' has been submitted. Return to ' . vs01_h($assoc['name'] ?? $association_slug) . '
'; } return '
Submission failed. Please try again or contact the operator.
' . vs01_h($result) . '
'; } // ---------------------------------------------------------------------------- // VALIDATION // ---------------------------------------------------------------------------- function vs01_validate_required($perspective, $fields) { $errors = []; foreach ($perspective['fields'] ?? [] as $field_def) { if (empty($field_def['required'])) continue; $id = $field_def['id'] ?? ''; $value = trim($fields[$id] ?? ''); if ($value === '' || $value === '[]') { $errors[] = $field_def['label'] ?? $id; } } return $errors; } // ---------------------------------------------------------------------------- // SPOOL ENVELOPE // ---------------------------------------------------------------------------- function vs01_build_spool_envelope($vs_code, $perspective, $fields, $association_slug, $channel_id) { return [ 'addon' => 'vs01', 'contract_version' => '1.0', 'vs_code' => $vs_code, 'perspective' => $perspective, 'association_slug' => $association_slug, 'association_channel_id' => (string) $channel_id, 'submitted_at' => date('c'), 'standing' => vs01_access_state($association_slug), 'fields' => $fields, ]; } // ---------------------------------------------------------------------------- // ORCHESTRATOR POST // ---------------------------------------------------------------------------- function vs01_post_to_orchestrator($envelope) { $config = vs01_load_config(); $receiver_url = $config['receiver_url'] ?? ''; $node_token = $config['node_token'] ?? ''; if (!$receiver_url) { logger('vs01_spool: receiver_url not configured'); return 'Orchestrator receiver URL not configured.'; } $payload = json_encode($envelope); if ($payload === false) { logger('vs01_spool: failed to encode envelope'); return 'Failed to encode submission.'; } $ch = curl_init($receiver_url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Node-Token: ' . $node_token, ], ]); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); curl_close($ch); if ($curl_error) { logger('vs01_spool: curl error: ' . $curl_error); return 'Network error: ' . $curl_error; } if ($http_code === 200 || $http_code === 201) { return true; } logger('vs01_spool: orchestrator returned HTTP ' . $http_code . ': ' . $response); return 'Orchestrator error (HTTP ' . $http_code . ').'; } // ---------------------------------------------------------------------------- // RE-RENDER FORM WITH SUBMITTED VALUES // ---------------------------------------------------------------------------- function vs01_render_vs_form_with_values($association_slug, $vs_code, $access, $submitted_values) { $schemas = vs01_load_schemas(); if (!isset($schemas[$vs_code])) return ''; $schema = $schemas[$vs_code]; $config = vs01_load_config(); $assoc = $config['associations'][$association_slug]; $perspective_map = [ 'participant' => 'homeowner', 'professional' => 'professional', 'operator' => 'public_record', ]; $perspective_key = $perspective_map[$access] ?? 'homeowner'; $perspective = $schema['perspectives'][$perspective_key] ?? null; if (!$perspective) return ''; $form_url = z_root() . '/vs01/' . vs01_h($association_slug) . '/' . vs01_h($vs_code); $meta = $schema['_meta']; $out = '
'; $out .= '

' . vs01_h($vs_code) . ' — ' . vs01_h($meta['title'] ?? '') . '

'; $out .= vs01_render_verification_sources($meta); $out .= '

' . vs01_h($perspective['instruction'] ?? '') . '

'; $out .= '
'; $out .= vs01_csrf_token(); $out .= ''; $out .= ''; foreach ($perspective['fields'] ?? [] as $field) { $field_html = vs01_render_field($field, $submitted_values); if (!empty($field['depends_on'])) { $field_html = vs01_wrap_depends_on($field, $field_html); } $out .= $field_html; } $out .= '
'; $out .= '
'; $out .= vs01_render_vs_nav($association_slug, $vs_code); $out .= '
'; return $out; }