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.

Web Server Actions in src/web/actions/ power the public-facing parts of the platform. They use getPublicServerClient() (anon key + RLS), not the admin client, ensuring all writes go through anonymous RLS policies and that unauthenticated visitors cannot escalate privileges. Only catalog mutation and branding actions that require authentication switch to supabaseAdmin after passing an RBAC guard.

Wizard

submitWizardData

src/web/actions/wizard.ts
submitWizardData(
  tenantCode: string | null,
  rawData: WizardSubmission
): Promise<WizardResult>
The main entry point for the public pre-engineering wizard. Validates and sanitizes all input with Zod + XSS sanitization, enforces a per-email rate limit, then executes the following pipeline:
  1. Verify the tenant exists and its status is "Activo".
  2. Calculate required CFM and price estimate via the engineering utilities (calculateRequiredCfm, estimatePrice).
  3. Upsert the client record by legal_name (creates if absent, reuses if present).
  4. Upsert the contact record by email within the client (creates if absent, reuses if present).
  5. Create a scored lead via createLeadWithScore (see below).
  6. Insert a diagnostic report (diagnostic_reports) linking the lead, dimensions, CFM result, and price ranges.
tenantCode
string | null
required
Tenant subdomain code. Used to scope all database writes to the correct tenant.
rawData.servicio
"fabricacion" | "venta" | "mantenimiento" | "reparacion" | "otro"
required
Type of industrial service being requested.
rawData.length
number
required
Space length in meters. Must be a positive number.
rawData.width
number
required
Space width in meters. Must be a positive number.
rawData.height
number
required
Space height in meters. Must be a positive number.
rawData.environment
"heavy_plant" | "data_center" | "mining" | "warehouse" | "default"
required
Industrial environment type. Affects the CFM multiplier and materials recommendation.
rawData.nombre
string
required
Contact’s full name. Minimum 2 characters.
rawData.empresa
string
required
Company legal name. Used as the legal_name for upsert. Minimum 2 characters.
rawData.cargo
string
required
Contact’s job title. Used in lead scoring. Minimum 2 characters.
rawData.telefono
string
required
Contact phone number. Minimum 7 characters.
rawData.email
string
required
Contact email address. Must be a valid email format. Used as the rate-limiting key and for contact deduplication.
rawData.ciudad
string
required
City where the project is located. Minimum 2 characters.
rawData.urgencia
"baja" | "media" | "alta"
required
Project urgency level. Affects lead scoring and price estimation.
rawData.otroDetalle
string
Free-text detail for the service type. Only relevant when servicio is "otro".
Returns Promise<WizardResult>:
diagnosticCode
string
Auto-generated code for the diagnostic report, e.g. "DX-2026-0001".
requiredCfm
number
Calculated airflow requirement in CFM (cubic feet per minute).
cfmCategory
"CRITICAL" | "HIGH" | "STANDARD" | "COMPACT"
CFM classification tier: CRITICAL (≥ 15,000 CFM), HIGH (≥ 8,000 CFM), COMPACT (< 2,000 CFM), or STANDARD (everything in between).
calculatedVolumeM3
number
Space volume in cubic meters (length × width × height).
estimatedPriceMinCop
number
Minimum price estimate in Colombian Pesos (COP).
estimatedPriceMaxCop
number
Maximum price estimate in Colombian Pesos (COP).
estimatedPriceMinUsd
number
Minimum price estimate in US Dollars (USD).
estimatedPriceMaxUsd
number
Maximum price estimate in US Dollars (USD).
materialsRecommendation
string
Dynamically generated materials recommendation based on the environment type. Examples:
  • heavy_plant / mining → epoxy-coated industrial blower with extruded aluminium blades.
  • data_center → EPA/HEPA air injection system with low-vibration acoustic control.
  • All others → general-purpose axial extractor with galvanized steel ducting.
leadId
string
UUID of the created lead record in the leads table.
Throws:
  • "Datos de entrada inválidos: ..." — Zod validation failed; message includes field-level details.
  • "Demasiadas solicitudes. Intente nuevamente en un minuto." — rate limit exceeded.
  • "El servicio no está disponible para este tenant." — tenant not found or not Activo.
  • Generic Supabase errors on write failures.

Catalog

getIndustrialCatalog

src/web/actions/catalog.ts
getIndustrialCatalog(tenantCode?: string | null): Promise<CatalogCategory[]>
Fetches the complete catalog hierarchy for the tenant in a single joined Supabase query (categories → subcategories → families → series → products → specifications / images / documents / CAD files). SEO metadata is fetched in a parallel query and merged into each product. Results are cached with Next.js unstable_cache:
  • Revalidation: 60 seconds
  • Cache tag: catalog-all
All catalog mutation actions call _invalidateCache(tenantCode) to bust the tag on write.
tenantCode
string | null
Tenant subdomain code.
Returns Promise<CatalogCategory[]> — a fully nested hierarchy:
interface CatalogCategory {
  id: string;
  categoryCode: string;
  name: string;
  description: string;
  subcategories: CatalogSubcategory[];
}

interface CatalogSubcategory {
  id: string;
  subcategoryCode: string;
  name: string;
  description: string;
  families: CatalogFamily[];
}

interface CatalogFamily {
  id: string;
  familyCode: string;
  name: string;
  description: string;
  series: CatalogSeries[];
}

interface CatalogSeries {
  id: string;
  seriesCode: string;
  name: string;
  description: string;
  products: ProductDetail[];
}

interface ProductDetail {
  id: string;
  productCode: string;
  name: string;
  description: string;
  status: string;
  specifications: Record<string, string>;
  images: ProductMedia[];
  documents: ProductMedia[];
  cadFiles: ProductMedia[];
  seo?: {
    metaTitle: string;
    metaDescription: string;
    metaKeywords: string;
    slug: string;
  };
}

saveProduct

saveProduct(
  tenantCode: string | null,
  product: {
    id?: string;
    productCode: string;
    name: string;
    description: string;
    status: string;
    seriesId: string;
    specifications: Record<string, string>;
  }
): Promise<{ success: boolean; productId?: string; error?: string }>
Creates or updates a product and its technical specifications. When id is provided the product is updated; otherwise a new product is inserted. Specifications are replaced atomically: existing rows for the product are deleted before the new set is inserted. Invalidates the catalog-all cache on success. Required permission: items.manage

deleteProduct

deleteProduct(
  tenantCode: string | null,
  productId: string
): Promise<{ success: boolean; error?: string }>
Soft-deletes a product by setting deleted_at to the current timestamp. The product remains in the database but is excluded from all catalog reads. Invalidates the catalog-all cache on success. Required permission: items.manage

saveCategory

saveCategory(
  tenantCode: string | null,
  category: {
    id?: string;
    categoryCode: string;
    name: string;
    description: string;
  }
): Promise<{ success: boolean; error?: string }>
Creates or updates a top-level product category. When id is present the category is updated; otherwise a new row is inserted into product_categories. Invalidates the catalog-all cache on success. Required permission: items.manage

deleteCategory

deleteCategory(
  tenantCode: string | null,
  categoryId: string
): Promise<{ success: boolean; error?: string }>
Soft-deletes a category by setting deleted_at. The category and all its descendants remain in the database but are excluded from getIndustrialCatalog. Invalidates the catalog-all cache on success. Required permission: items.manage

addProductImage

addProductImage(
  tenantCode: string | null,
  productId: string,
  image: {
    fileName: string;
    filePath: string;
    fileSize: number;
    mimeType: string;
    altText?: string;
  }
): Promise<{ asset: MediaAsset; prodImg: ProductImage }>
Registers a media asset in the media_assets table and then creates a join row in product_images linking the asset to the product. The sort_order defaults to 10.
tenantCode
string | null
required
Tenant subdomain code.
productId
string
required
UUID of the product to attach the image to.
image.fileName
string
required
Original file name (e.g. "extractor-axial-ev300.png").
image.filePath
string
required
Storage path or public URL of the uploaded file.
image.fileSize
number
required
File size in bytes.
image.mimeType
string
required
MIME type (e.g. "image/png").
image.altText
string
Accessibility alt text. Defaults to an empty string if omitted.
Returns Promise<{ asset: MediaAsset; prodImg: ProductImage }> — both the newly created media asset row and the product–image join row. Invalidates the catalog-all cache on success. Required permission: items.manage

Leads

createLeadWithScore

src/web/actions/leads.ts
createLeadWithScore(
  tenantCode: string | null,
  rawLeadData: {
    clientId: string;
    contactId: string;
    notes?: string;
    role: string;
    urgency: string;
    email: string;
  }
): Promise<Lead>
Exported server action that scores an incoming lead and inserts it into the leads table. Called internally by submitWizardData at step 5 of the wizard pipeline, and also available for direct invocation. Validates input with Zod, applies rate limiting per email, and verifies the tenant is "Activo" before writing. Inserts using the public anon client (RLS enforced). Scoring factors (via calculateLeadScore):
SignalParameterImpact
Job title seniorityroleDirector de Planta / Gerente de Mantenimiento / Supervisor HVAC: +40 pts. Ingeniero de Proyectos / Compras: +25 pts. Other: +10 pts.
Urgencyurgencyalta: +40 pts. media: +20 pts. baja: +5 pts.
Email domainemailPublic domain (Gmail, Hotmail, etc.): −20 pts penalty. Disposable/invalid domain: SPAM, score zeroed.
Score is clamped to [0, 100]. risk_level classification: CALIENTE (≥ 70), TIBIO (≥ 40), FRIO (< 40), or SPAM (disposable/invalid email). The lead is inserted with status: "NUEVO" and lead_source: "WEBSITE".

Branding

src/web/actions/branding.ts

getTenantBranding

getTenantBranding(tenantCode?: string | null): Promise<BrandingConfig>
Returns the full consolidated branding configuration for the tenant by reading settings from modules EMPRESA, LOCALIZACION, IDENTIDAD, and DOCUMENTOS in tenant_settings. Values from the database are merged on top of the hardcoded getBrandingDefaults(tenantCode) so that any key not yet saved in the database still returns a sensible default.
tenantCode
string | null
Tenant subdomain code.
Returns Promise<BrandingConfig> — complete branding config object. Falls back to getBrandingDefaults on Supabase error.

saveTenantBranding

saveTenantBranding(
  tenantCode: string | null,
  data: Partial<BrandingConfig>,
  versionDescription?: string
): Promise<{ success: boolean; error?: string }>
Saves one or more branding keys by performing a bulk upsert into tenant_settings and creating a full configuration snapshot in tenant_branding_version. Every successful save increments the version number.
tenantCode
string | null
required
Tenant subdomain code.
data
Partial<BrandingConfig>
required
Partial branding config with only the keys to update. Routing to the correct module is automatic based on the key name.
versionDescription
string
Human-readable description for the version snapshot. Defaults to "Actualización de Branding (Versión N)".
Required permission: branding.manage

getBrandingHistory

getBrandingHistory(tenantCode?: string | null): Promise<BrandingVersion[]>
Returns all historical branding version snapshots for the tenant, ordered by version number descending (newest first). Returns Promise<BrandingVersion[]> where each item has:
FieldTypeDescription
idstringVersion UUID
version_numbernumberSequential version number
config_valuesBrandingConfigFull configuration snapshot at time of save
descriptionstringHuman-readable change description
created_atstringISO timestamp

restoreBrandingVersion

restoreBrandingVersion(
  tenantCode: string | null,
  versionId: string
): Promise<{ success: boolean; error?: string }>
Restores a historical branding version by re-upserting all its config_values into tenant_settings and creating a new version snapshot with a description of "Restaurado a la Versión N".
tenantCode
string | null
required
Tenant subdomain code.
versionId
string
required
UUID of the tenant_branding_version row to restore.
Required permission: branding.manage
uploadBrandingLogo(
  tenantCode: string | null,
  fileType: string,
  base64Data: string,
  fileName: string,
  mimeType: string
): Promise<{ success: boolean; url?: string; error?: string }>
Uploads a branding image (logo, favicon, etc.) to the tenant-logos Supabase Storage bucket and returns its public URL. Creates the bucket if it does not yet exist.
tenantCode
string | null
required
Tenant subdomain code.
fileType
string
required
Logical file type label used to construct the storage path (e.g. "logo_url", "favicon_url"). The final path is {tenantId}/{fileType}-{timestamp}.{ext}.
base64Data
string
required
Base64-encoded file content. Decoded to a Buffer before uploading.
fileName
string
required
Original file name used to derive the extension.
mimeType
string
required
MIME type of the image (e.g. "image/png", "image/svg+xml"). Allowed: image/png, image/jpeg, image/svg+xml, image/gif, image/x-icon, image/vnd.microsoft.icon. Maximum file size: 2 MB.
Returns Promise<{ success: boolean; url?: string; error?: string }>. On success, url contains the publicly accessible URL from Supabase Storage. Required permission: branding.manage

getPublicBranding

getPublicBranding(tenantCode?: string | null): Promise<BrandingConfig>
Convenience alias for getTenantBranding. Used in public-facing pages and middleware that need branding data without an authenticated session context. Internally calls getTenantBranding with the same arguments.

Build docs developers (and LLMs) love