Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ency07/B2B/llms.txt

Use this file to discover all available pages before exploring further.

The wizard at /wizard is a 5-step guided form that walks industrial buyers through a pre-engineering assessment, automatically calculates airflow requirements using ASHRAE ACH standards, generates a budget price estimate in both COP and USD, and creates a fully qualified CRM lead in Supabase. The entire flow is backed by the submitWizardData() Server Action defined in src/web/actions/wizard.ts.

Wizard steps

1

Service selection

The user selects the type of service they need. The servicio field accepts one of: fabricacion | venta | mantenimiento | reparacion | otro.When otro is selected, an optional otroDetalle free-text field is revealed to capture additional context (up to 2,000 characters).
2

Technical dimensions

The user enters the physical dimensions of their industrial space:
  • length, width, height — all in meters, required positive numbers.
  • environment — the operational classification of the space, which determines the ASHRAE ACH multiplier used in the CFM calculation.
Environment valueDescriptionACH (ASHRAE)
heavy_plantHeavy manufacturing / smelting45
data_centerServer room / data center30
miningMining operations10 (default rate — not separately mapped)
warehouseStorage / warehouse8
defaultGeneral industrial10
3

Corporate info

The CorporateInfoStep component (src/web/components/wizard/CorporateInfoStep.tsx) collects B2B contact data used for both the diagnostic report and CRM record creation:
  • nombre — full name of the contact person
  • empresa — legal company name (used as the legal_name key for client upsert)
  • cargo — job title, selected from a scored list
  • telefono — corporate phone number
  • email — corporate email address
  • ciudad — city
  • urgencia — project urgency: baja | media | alta
When a public email domain (@gmail.com, @hotmail.com, @outlook.com, @yahoo.com) is detected, a visible warning banner notifies the user that a score penalty will be applied and recommends using a corporate address.
4

Summary

A read-only review screen computed entirely from the data entered in the previous steps. It displays:
  • Calculated CFM value and CFM category (COMPACT / STANDARD / HIGH / CRITICAL)
  • Estimated price range in COP and USD (±15% deviation band)
  • Materials recommendation based on environment type
  • Space volume in m³
No server call is made at this step; values are derived client-side from the engineering and pricing engines.
5

Success

Triggered after a successful submitWizardData() call. Displays:
  • The unique diagnosticCode generated by Supabase for the diagnostic_reports record
  • The assigned engineer information
  • A summary of the CFM category and price estimate
The leadId returned from the Server Action links this session to the CRM lead record.

submitWizardData() Server Action

Signature:
submitWizardData(
  tenantCode: string | null,
  rawData: WizardSubmission
): Promise<WizardResult>
Location: src/web/actions/wizard.ts

Processing pipeline

  1. Zod validationwizardSubmissionSchema validates all fields. On failure, field-level error messages are concatenated and thrown as a single Error.
  2. XSS sanitizationsanitizeObject() from src/lib/utils/sanitize.ts is applied to all string fields in the parsed data.
  3. Rate limitingcheckRateLimit(wizard:$) enforces a maximum of 10 requests per minute per email address (default limits). Exceeding the limit throws "Demasiadas solicitudes. Intente nuevamente en un minuto.".
  4. Tenant validation — The tenant must exist in the tenants table with status = 'Activo'.
  5. Engineering calculationcalculateRequiredCfm() from src/utils/engineering.ts computes cfm and cubicMeters from the submitted dimensions and environment.
  6. Price estimationestimatePrice() from src/utils/pricing.ts computes COP and USD ranges using the service type, urgency multiplier, and volume.
  7. Client upsert — looks up clients by (tenant_id, legal_name). If not found, inserts a new client record with status = 'PROSPECTO'.
  8. Contact upsert — looks up client_contacts by (tenant_id, client_id, email). If not found, inserts a new contact.
  9. Lead creation with scoring — delegates to createLeadWithScore() in src/web/actions/leads.ts, which calculates a numeric score and classifies the lead as CALIENTE | TIBIO | FRIO | SPAM.
  10. Diagnostic report — inserts a row into diagnostic_reports with all calculated values and returns the auto-generated diagnostic_code.

Input and output types

interface WizardSubmission {
  servicio: "fabricacion" | "venta" | "mantenimiento" | "reparacion" | "otro";
  length: number;     // meters
  width: number;      // meters
  height: number;     // meters
  environment: "heavy_plant" | "data_center" | "mining" | "warehouse" | "default";
  nombre: string;
  empresa: string;
  cargo: string;
  telefono: string;
  email: string;
  ciudad: string;
  urgencia: "baja" | "media" | "alta";
  otroDetalle?: string;
}
interface WizardResult {
  diagnosticCode: string;
  requiredCfm: number;
  cfmCategory: "CRITICAL" | "HIGH" | "STANDARD" | "COMPACT";
  calculatedVolumeM3: number;
  estimatedPriceMinCop: number;
  estimatedPriceMaxCop: number;
  estimatedPriceMinUsd: number;
  estimatedPriceMaxUsd: number;
  materialsRecommendation: string;
  leadId: string;
}

CFM category thresholds

After calculateRequiredCfm() returns the raw CFM value, submitWizardData() classifies it into one of four categories:
CategoryCFM RangeDescription
COMPACT< 2,000Small enclosed spaces
STANDARD2,000 – 7,999General industrial
HIGH8,000 – 14,999Heavy manufacturing
CRITICAL≥ 15,000Smelting, heavy plant

Materials recommendation

The materialsRecommendation string in the result is computed from the environment value at runtime — not stored as static copy:
EnvironmentRecommendation
heavy_plant, miningIndustrial Blower or Mushroom extractor with anti-corrosive epoxy coating and extruded aluminum blades
data_centerAir injection system with EPA/HEPA particle filters and low-vibration acoustic control
All othersMulti-purpose or high-capacity axial extractor with 22-gauge galvanized steel ductwork

Lead scoring

Lead scoring is performed by calculateLeadScore() in src/web/actions/leads.ts. Scores range from 0 to 100 and are built from three signals:
SignalPoints
cargo = Director de Planta / Gerente de Mantenimiento / Supervisor HVAC+40
cargo = Ingeniero de Proyectos / Compras+25
cargo = Otro+10
urgencia = alta+40
urgencia = media+20
urgencia = baja+5
Public email domain (Gmail, Hotmail, Yahoo, Outlook)−20
Disposable / known spam domainScore forced to 0, classified as SPAM
The final riskLevel label assigned to the leads row:
ScoreRisk level
≥ 70CALIENTE
40 – 69TIBIO
< 40FRIO
Disposable domainSPAM

Data persistence flow

submitWizardData()
  ├── calculateRequiredCfm()        → CFM + m³
  ├── estimatePrice()               → COP / USD ranges
  ├── clients          UPSERT       (keyed by legal_name)
  ├── client_contacts  UPSERT       (keyed by email + client_id)
  ├── leads            INSERT       (score + risk_level via createLeadWithScore)
  └── diagnostic_reports INSERT     → returns diagnostic_code
All four database writes use getPublicServerClient() (anonymous key + RLS). The RLS policies on each table allow INSERT for anonymous sessions scoped to the active tenant, but deny any SELECT of other tenants’ data.

Error handling

Error conditionThrown message
Zod validation failure"Datos de entrada inválidos: <field>: <message>"
Rate limit exceeded"Demasiadas solicitudes. Intente nuevamente en un minuto."
Tenant not found or inactive"El servicio no está disponible para este tenant."
Client insert failure"Error al registrar el cliente. Intente nuevamente."
Contact insert failure"Error al registrar el contacto. Intente nuevamente."
Diagnostic report insert failure"Error al generar el reporte de diagnóstico. Intente nuevamente."
The wizard uses getPublicServerClient() (anon key + RLS), not the admin client. All database writes go through anonymous RLS policies that allow INSERT only — no SELECT of other tenants’ data is possible from the wizard flow.

Pricing engine

estimatePrice() from src/utils/pricing.ts applies three multipliers on top of a per-service base price (in COP):
ServiceBase price (COP)
fabricacion$1,200,000
venta$800,000
mantenimiento$300,000
reparacion$500,000
A volume modifier adds 5% per 100 m³ of physical space. An urgency multiplier (alta = 1.35×, media = 1.10×, baja = 1.00×) is capped at 2.0×. The final price is presented as a ±15% range:
rangeMinCop = estimatedTotalCop × 0.85
rangeMaxCop = estimatedTotalCop × 1.15
USD values are derived by dividing by the COP_TO_USD_RATE constant defined in src/lib/constants.ts.

Build docs developers (and LLMs) love