Skip to main content

Documentation Index

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

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

Next.js Server Actions (introduced in Next.js 14 and stabilised in 16) let you define async functions that run exclusively on the server and can be called directly from React components without building a separate API layer. In B2B Import ERP every Server Action is declared with the "use server" directive, resolves the caller’s tenant via getTenantId(), and performs all database mutations through supabaseAdmin — the Supabase service-role client — so that Row-Level Security is never a bottleneck for trusted back-end operations.
supabaseAdmin holds the service-role key, which bypasses all PostgreSQL RLS policies. Never import or expose this client in a Client Component ("use client"). It must only be used inside files that carry the "use server" directive or in Node.js-only server utilities.

Common Pattern

Every action follows the same three-step scaffold:
  1. Resolve the tenant — call getTenantId(tenantCode) to turn a human-readable slug ("apex", null for the default tenant, etc.) into a stable UUID.
  2. Execute the query — use supabaseAdmin to read or write the resolved tenant’s data.
  3. Return typed data or throw — on Supabase error, the action throws an Error whose message is safe to surface to the client.

getTenantId implementation

// src/app/actions.ts
"use server";

import { supabaseAdmin } from "@/utils/supabase";

export async function getTenantId(tenantCode?: string | null): Promise<string> {
  if (tenantCode === "apex") {
    return "b0000000-0000-0000-0000-000000000000";
  }
  return "a0000000-0000-0000-0000-000000000000"; // default → acme
}
The current implementation uses deterministic UUID constants for the two demo tenants. In a production multi-tenant deployment, replace this with a database lookup against the tenants table using the tenant_code column.

Clients & Contacts

Actions live in src/app/actions.ts.

getClients

Returns all active clients for a tenant, enriched with their total invoiced amount.
export async function getClients(
  tenantCode?: string | null
): Promise<ClientRow[]>
tenantCode
string | null | undefined
The tenant slug (e.g. "apex"). Pass null or omit to use the default tenant.
Returns an array of objects shaped as:
{
  id: string;
  taxId: string;
  name: string;         // legal_name
  segment: string;      // industry
  totalInvoiced: number; // sum of EMITIDA + PAGADA invoice amounts
  status: "ACTIVO" | "SUSPENDIDO" | "PENDIENTE";
}
Only records where deleted_at IS NULL are returned. Invoices are filtered to statuses EMITIDA, PARCIALMENTE_PAGADA, and PAGADA.

createClient

Creates a new client record for the tenant. Checks for a duplicate tax_id within the same tenant before inserting.
export async function createClient(
  tenantCode: string | null,
  clientData: {
    taxId: string;
    name: string;
    segment: string;
    email: string;
  }
): Promise<Client>
tenantCode
string | null
The tenant slug.
clientData.taxId
string
required
RFC / NIT / tax identifier. Must be unique within the tenant.
clientData.name
string
required
Legal name (legal_name).
clientData.segment
string
required
Industry segment, stored in the industry column.
clientData.email
string
required
Primary contact email.
Throws "Ya existe un cliente con este RFC para este tenant." when the tax_id already exists.

Jobs

Actions live in src/app/actions.ts.

getJobs

Returns all work orders for the tenant, ordered newest-first. Priority and status values are mapped to display-friendly labels.
export async function getJobs(
  tenantCode?: string | null
): Promise<JobRow[]>
tenantCode
string | null | undefined
Tenant slug.
Returns an array shaped as:
{
  id: string;
  code: string;           // job_code
  description: string;    // title
  assignedTech: string;   // display name derived from tenant
  priority: "BAJA" | "MEDIA" | "ALTA";
  startDate: string;      // YYYY-MM-DD
  endDate: string;        // YYYY-MM-DD
  status: "PENDIENTE" | "EN_PROGRESO" | "COMPLETADA" | "CANCELADA";
}

createJob

Creates a new work order, auto-generating a JOB-2026-XXX code and resolving default client, requirement, site, and area references from the tenant.
export async function createJob(
  tenantCode: string | null,
  jobData: {
    description: string;
    assignedTech: string;
    priority: string;
    startDate: string;
    endDate: string;
  }
): Promise<Job>
jobData.description
string
required
Work order description (also used as title; truncated to 100 chars).
jobData.priority
string
required
"ALTA", "MEDIA", or "BAJA".
jobData.startDate
string
required
ISO date string for planned start (YYYY-MM-DD).
jobData.endDate
string
required
ISO date string for planned end (YYYY-MM-DD).

Inventory

Actions live in src/app/actions.ts.

getInventoryStock

Returns all warehouse stock positions for the tenant, joined with warehouse and item metadata.
export async function getInventoryStock(
  tenantCode?: string | null
): Promise<StockRow[]>
Returns an array shaped as:
{
  id: string;             // "{warehouseId}-{itemId}"
  warehouseCode: string;
  warehouseName: string;
  itemCode: string;
  itemName: string;
  sku: string;            // alias for itemCode
  category: string;
  unit: string;
  quantity: number;
  reserved: number;
  available: number;
}

createInventoryMovement

Creates a stock movement record (Entrada, Salida, or Transferencia) and sets status to Aplicado.
export async function createInventoryMovement(
  tenantCode: string | null,
  movement: {
    type: "Entrada" | "Salida" | "Transferencia";
    itemCode: string;
    quantity: number;
    notes: string;
    sourceWarehouse: string;
    destWarehouse?: string;
  }
): Promise<InventoryMovement>
movement.type
"Entrada" | "Salida" | "Transferencia"
required
Movement direction. Transferencia requires destWarehouse.
movement.itemCode
string
required
The item_code of the inventory_items record.
movement.sourceWarehouse
string
required
The warehouse_code of the source warehouse.
movement.destWarehouse
string
Required when type is "Transferencia".
Throws if the item or warehouse codes cannot be resolved.

Invoices

Actions live in src/app/actions.ts.

getInvoices

Returns all invoices for the tenant, newest-first, with the linked client’s legal_name joined.
export async function getInvoices(
  tenantCode?: string | null
): Promise<InvoiceRow[]>
Returns an array shaped as:
{
  id: string;
  code: string;           // invoice_code
  clientName: string;
  totalAmount: number;
  paidAmount: number;
  status: "BORRADOR" | "EMITIDA" | "PARCIALMENTE_PAGADA" | "PAGADA" | "ANULADA";
  date: string;           // YYYY-MM-DD
}

createInvoice

Creates an invoice header in EMITIDA status, adding a single line item for the concept amount. Matches the client by legal_name; falls back to the first active client if no match is found.
export async function createInvoice(
  tenantCode: string | null,
  invoiceData: {
    clientName: string;
    concept: string;
    amount: number;
  }
): Promise<Invoice>
invoiceData.clientName
string
required
Legal name of the client. Used to look up clients.legal_name.
invoiceData.concept
string
required
Line item description.
invoiceData.amount
number
required
Total amount in COP. Used as both subtotal_amount and total_amount.

Tenant Settings

Actions live in src/app/actions.ts.

getTenantSettings

Fetches all settings for the tenant as a flat key-value map, decrypting any encrypted fields via the get_tenant_setting RPC.
export async function getTenantSettings(
  tenantCode?: string | null
): Promise<Record<string, unknown>>
Returns a plain object where each key is a config_key and each value is the resolved (and decrypted where applicable) config_value.

updateTenantSettings

Upserts a single setting key within a given module. If the row exists it is updated; otherwise a new row is inserted.
export async function updateTenantSettings(
  tenantCode: string | null,
  module: string,
  key: string,
  value: unknown,
  isEncrypted?: boolean
): Promise<TenantSetting>
module
string
required
Logical grouping: "EMPRESA", "LOCALIZACION", "IDENTIDAD", "DOCUMENTOS", etc.
key
string
required
The config_key to set (e.g. "color_primario").
value
unknown
required
Any JSON-serialisable value.
isEncrypted
boolean
Defaults to false. When true, the value is stored with is_encrypted = true and decrypted via RPC on read.

Leads

Actions live in src/app/actions/leads.ts.

calculateLeadScore

Pure scoring function — no database writes. Computes a 0–100 priority score and assigns a Spanish-language risk level.
export async function calculateLeadScore(
  email: string,
  role: string,
  urgency: string
): Promise<LeadScoreResult>
email
string
required
Contact email. Public domains (gmail, yahoo, etc.) incur a −20-point penalty. Disposable domains (yopmail, mailinator, tempmail) force SPAM classification.
role
string
required
Professional role. Decision-maker roles ("Director de Planta", "Gerente de Mantenimiento", "Supervisor de HVAC / Operaciones") add 40 points. Mid-level roles add 25 points.
urgency
"baja" | "media" | "alta"
required
Urgency level. alta adds 40 points, media adds 20, baja adds 5.
Returns:
interface LeadScoreResult {
  score: number;                                       // 0–100
  riskLevel: "CALIENTE" | "TIBIO" | "FRIO" | "SPAM"; // ≥70 CALIENTE, ≥40 TIBIO
}

createLeadWithScore

Scores the lead and persists it to the leads table with status = "NUEVO".
export async function createLeadWithScore(
  tenantCode: string | null,
  leadData: {
    clientId: string;
    contactId: string;
    notes?: string;
    role: string;
    urgency: string;
    email: string;
  }
): Promise<Lead>
tenantCode
string | null
required
Tenant slug.
leadData.clientId
string
required
UUID of the existing clients record.
leadData.contactId
string
required
UUID of the existing client_contacts record.
leadData.role
string
required
Contact’s professional role — fed to calculateLeadScore.
leadData.urgency
string
required
Urgency level — fed to calculateLeadScore.
leadData.email
string
required
Contact email — fed to calculateLeadScore.

submitContactForm

Registers a lead originating from the B2B landing page contact form, without requiring an existing clients record. Scores the lead using calculateLeadScore with role "Otro".
export async function submitContactForm(
  tenantCode: string | null,
  leadData: {
    name: string;
    companyName: string;
    phone: string;
    email: string;
    urgency: string;
    description: string;
  }
): Promise<Lead>
leadData.name
string
required
Full name of the contact.
leadData.companyName
string
required
Company name — stored in leads.company_name.
leadData.urgency
string
required
Urgency level fed to calculateLeadScore.
leadData.description
string
required
Free-text message — stored in leads.notes.
This action writes directly to the leads table using the extended columns added by migration 20260619000036_fix_leads_schema.sql (name, company_name, phone, email, urgency, lead_score). Unlike createLeadWithScore, no client_id or contact_id is required.

Requirements

Actions live in src/app/actions/requirements.ts.

getRequirements

Returns all requirements for the tenant, ordered newest-first, with the linked client’s legal_name and city joined.
export async function getRequirements(
  tenantCode?: string | null
): Promise<RequirementRow[]>
tenantCode
string | null | undefined
Tenant slug.
Returns RequirementRow[] where each row includes:
interface RequirementRow {
  id: string;
  requirement_code: string;
  title: string;
  category: string;
  priority: "LOW" | "MEDIUM" | "HIGH";
  status: "BORRADOR" | "NUEVO" | "EN_REVISION" | "DIAGNOSTICO" | "COTIZACION" | "COMPLETADO" | "CANCELADO";
  client_id: string;
  engineering_user_id: string | null;
  sales_user_id: string | null;
  created_at: string;
  client: { legal_name: string; city: string | null } | null;
}

createRequirement

Inserts a new requirement in BORRADOR status.
export async function createRequirement(
  tenantCode: string | null,
  reqData: {
    title: string;
    clientId: string;
    category: string;
    priority: string;
  }
): Promise<Requirement>
reqData.title
string
required
Short descriptive title.
reqData.clientId
string
required
UUID of the parent client.
reqData.category
string
required
Service category (e.g. "fabricacion", "mantenimiento").
reqData.priority
string
required
Priority level: "LOW", "MEDIUM", or "HIGH".

updateRequirementStatus

Updates the status (and optionally other fields) of an existing requirement. Used to advance requirements through the workflow.
export async function updateRequirementStatus(
  reqId: string,
  newStatus: string,
  extra?: Record<string, unknown>
): Promise<Requirement>
reqId
string
required
UUID of the requirement to update.
newStatus
string
required
New status value: "BORRADOR", "NUEVO", "EN_REVISION", "DIAGNOSTICO", "COTIZACION", "COMPLETADO", or "CANCELADO".
extra
Record<string, unknown>
Optional additional column values to patch on the same row (e.g. { engineering_user_id: "..." }).

Quotes

Actions live in src/app/actions/quotes.ts.

getQuotes

Returns all quotes for the tenant, newest-first, with the linked client’s legal_name joined.
export async function getQuotes(
  tenantCode?: string | null
): Promise<QuoteRow[]>
tenantCode
string | null | undefined
Tenant slug.
Returns QuoteRow[]:
interface QuoteRow {
  id: string;
  quote_code: string;
  client_id: string;
  requirement_id: string | null;
  assigned_user_id: string | null;
  valid_until: string;
  subtotal: number;
  total_amount: number;
  status: "BORRADOR" | "EN_REVISION" | "ENVIADA" | "APROBADA" | "RECHAZADA" | "VENCIDA";
  created_at: string;
  client: { legal_name: string } | null;
}

createQuote

Creates a new quote header in BORRADOR status.
export async function createQuote(
  tenantCode: string | null,
  quoteData: {
    clientId: string;
    requirementId?: string;
    validUntil: string;
  }
): Promise<Quote>
quoteData.clientId
string
required
UUID of the client.
quoteData.requirementId
string
Optional UUID of a linked requirement.
quoteData.validUntil
string
required
ISO date string for the quote expiry date (YYYY-MM-DD).

getQuoteItems

Returns all line items for a specific quote, ordered by item_order.
export async function getQuoteItems(
  quoteId: string
): Promise<QuoteItem[]>
quoteId
string
required
UUID of the parent quote.

addQuoteItem

Adds a single line item to an existing quote.
export async function addQuoteItem(
  tenantCode: string | null,
  itemData: {
    quoteId: string;
    description: string;
    itemType: string;
    quantity: number;
    unitPrice: number;
    discountAmount: number;
    taxPercent: number;
    itemOrder: number;
  }
): Promise<QuoteItem>
itemData.quoteId
string
required
UUID of the parent quote.
itemData.itemType
string
required
Type label for the line item (e.g. "PRODUCTO", "SERVICIO").
itemData.itemOrder
number
required
Integer display order for this line within the quote.

updateQuoteStatus

Updates the status of an existing quote.
export async function updateQuoteStatus(
  quoteId: string,
  status: string
): Promise<Quote>
quoteId
string
required
UUID of the quote.
status
string
required
New status: "BORRADOR", "EN_REVISION", "ENVIADA", "APROBADA", "RECHAZADA", or "VENCIDA".

Wizard

Actions live in src/app/actions/wizard.ts.

submitWizardData

The wizard is the main public-facing intake form. This single action orchestrates the entire lead-capture pipeline: engineering calculations → client upsert → contact upsert → lead scoring → diagnostic report creation.
export async function submitWizardData(
  tenantCode: string | null,
  data: WizardSubmission
): Promise<WizardResult>
tenantCode
string | null
required
Tenant slug for the landing page the visitor came from.
data
WizardSubmission
required
Full submission from the multi-step wizard form:
interface WizardSubmission {
  servicio: "fabricacion" | "venta" | "mantenimiento" | "reparacion" | "otro";
  length: number;   // room length in metres
  width: number;    // room width in metres
  height: number;   // room height in metres
  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;
}
Processing steps (in order):
  1. calculateRequiredCfm() computes airflow from room dimensions + environment.
  2. estimatePrice() produces a COP/USD price range.
  3. Client is upserted by legal_name — existing clients are reused, not duplicated.
  4. Contact is upserted by email within the client — reused if already present.
  5. createLeadWithScore() scores the lead and persists it.
  6. A diagnostic_reports record is created, linking all calculated data to the lead.
Returns WizardResult:
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;
}

Payments

Actions live in src/app/actions/payments.ts.

getPaymentGateway

Fetches the active Wompi gateway configuration for the tenant.
export async function getPaymentGateway(
  tenantCode?: string | null
): Promise<PaymentGateway | null>
Returns null (never throws) when no active gateway is configured, so callers should guard for the null case before attempting checkout.

createWompiCheckout

Creates a payment_transactions record, stamps the invoice with a payment link, and returns the checkout URL.
export async function createWompiCheckout(
  tenantCode: string | null,
  invoiceId: string,
  amount: number,
  customerEmail: string,
  customerName: string
): Promise<WompiCheckout>
invoiceId
string
required
UUID of the invoice being paid.
amount
number
required
Amount in COP (Colombian Peso).
customerEmail
string
required
Payer email — stored on the transaction record.
customerName
string
required
Payer display name.
Returns:
{
  transactionId: string;
  reference: string;       // unique reference: "INV-{invoiceId[0..8]}-{timestamp}"
  checkoutUrl: string;     // Wompi hosted checkout URL
  publicKey: string;
  environment: string;     // "sandbox" | "production"
  amount: number;
  currency: "COP";
}
Throws "Pasarela de pagos no configurada para este tenant." when no active gateway exists.

processWompiWebhook

Called by POST /api/webhooks/wompi. Finds the matching transaction by reference_id, maps the Wompi status, and — when APPROVED — creates a payments record, updates the invoice balance, and creates a payment_reconciliations entry.
export async function processWompiWebhook(
  payload: {
    event: string;
    data: {
      id: string;
      reference_id?: string;
      status: string;
      amount_in_cents: number;
      currency: string;
      customer_email?: string;
      payment_method_type?: string;
    };
  }
): Promise<{ success: boolean; status: string }>
After processing, the action calls revalidatePath("/portal") and revalidatePath("/dashboard/invoices") to invalidate Next.js caches.

getPaymentHistory

Returns the 50 most recent applied payments for the tenant, ordered by payment date descending.
export async function getPaymentHistory(
  tenantCode?: string | null
): Promise<PaymentHistoryRow[]>
Returns an array shaped as:
{
  id: string;
  code: string;           // payment_code
  date: string;           // YYYY-MM-DD
  method: string;         // e.g. "Tarjeta" | "PSE"
  reference: string;      // reference_number
  amount: number;
  status: string;
  invoiceCode: string;    // joined from invoices.invoice_code
}

Catalog

Actions live in src/app/actions/catalog.ts.

getIndustrialCatalog

Fetches the full five-level product hierarchy for the tenant: Category → Subcategory → Family → Series → Products. Each product includes specifications, images, documents, CAD files, and SEO metadata.
export async function getIndustrialCatalog(
  tenantCode?: string | null
): Promise<CatalogCategory[]>
tenantCode
string | null | undefined
Tenant slug.
Returns a nested CatalogCategory[] tree:
interface CatalogCategory {
  id: string;
  categoryCode: string;
  name: string;
  description: string;
  subcategories: CatalogSubcategory[];  // → families → series → products
}
This function issues sequential waterfall queries for each level of the hierarchy. For large catalogs consider caching the result with unstable_cache or implementing a single recursive SQL view.

getProductDetail

Fetches the full detail for a single product including specifications, images, documents, CAD files, and SEO metadata.
// Resolved inside getIndustrialCatalog per-product; use the catalog traversal
// to retrieve individual ProductDetail objects.
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;
  };
}

addProductImage

Inserts a media asset record into media_assets and links it to a product via product_images.
export async function addProductImage(
  tenantCode: string | null,
  productId: string,
  image: {
    fileName: string;
    filePath: string;
    fileSize: number;
    mimeType: string;
    altText?: string;
  }
): Promise<{ asset: MediaAsset; prodImg: ProductImage }>
productId
string
required
UUID of the product to attach the image to.
image.filePath
string
required
Supabase Storage path (e.g. "products/abc123/hero.png").

saveProduct

Creates or updates a product and replaces its specifications in a single operation. Pass product.id to update; omit it to insert.
export async function 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 }>
product.seriesId
string
required
UUID of the product_series record this product belongs to.
product.specifications
Record<string, string>
required
Key-value map of technical specifications. Existing specs are deleted and replaced on every save.

deleteProduct

Soft-deletes a product by setting deleted_at.
export async function deleteProduct(
  tenantCode: string | null,
  productId: string
): Promise<{ success: boolean; error?: string }>

saveCategory

Creates or updates a product category. Pass category.id to update; omit it to insert.
export async function saveCategory(
  tenantCode: string | null,
  category: {
    id?: string;
    categoryCode: string;
    name: string;
    description: string;
  }
): Promise<{ success: boolean; error?: string }>

deleteCategory

Soft-deletes a product category by setting deleted_at.
export async function deleteCategory(
  tenantCode: string | null,
  categoryId: string
): Promise<{ success: boolean; error?: string }>

Branding

Actions live in src/app/actions/branding.ts.

getTenantBranding

Returns the merged branding configuration: hardcoded defaults from getBrandingDefaults() are overridden by any keys stored in tenant_settings for modules EMPRESA, LOCALIZACION, IDENTIDAD, and DOCUMENTOS.
export async function getTenantBranding(
  tenantCode?: string | null
): Promise<BrandingConfig>
tenantCode
string | null | undefined
Tenant slug. Affects which defaults are applied ("apex" vs AeroMax).
Returns the full BrandingConfig object (50+ keys covering company info, logos, colours, typography, localization, email/PDF templates, and portal labels). See src/utils/branding-defaults.ts for the complete interface.

Build docs developers (and LLMs) love