🛒 Personalizar el checkout — campos, pasos y validaciones

Actualizado: 2025-01-15

#Hooks disponibles en el checkout

HookPosicionUso tipico
displayPersonalInformationTopArriba del paso de loginBanner, aviso legal, promocion
additionalCustomerFormFieldsFormulario de registroCampos: NIF, fecha nacimiento, empresa
displayCheckoutSummaryTopArriba del resumenCupones destacados, mensaje
displayBeforeCarrierAntes de seleccion de envioInfo de envio, estimacion
displayAfterCarrierDespues de seleccion de envioSeguro de envio, recogida en tienda
displayPaymentTopAntes de metodos de pagoMensaje de seguridad
displayPaymentByBinariesMetodos de pagoPagos propios
displayOrderConfirmationPagina de confirmacionTracking, upsell, encuesta
actionValidateOrderAl confirmar pedidoLogica post-compra, integraciones
displayExpressCheckoutBoton de compra rapidaPayPal Express, Apple Pay, Google Pay

#Anadir campo al formulario de direccion

Campo NIF/CIF en el formulario de direccion
php
<?php
/**
 * Hook additionalCustomerFormFields
 * Anadir campos al formulario de registro/direccion
 */
public function hookAdditionalCustomerFormFields($params)
{
    // Campo NIF para facturacion
    $nifField = (new FormField())
        ->setName('nif')
        ->setType('text')
        ->setLabel($this->trans('NIF / CIF', [], 'Modules.Ecomcheckout.Shop'))
        ->setRequired(true)
        ->addConstraint('isGenericName')   // Validacion PS
        ->setAvailableValues([])           // Sin opciones predefinidas
        ->setMaxLength(20);

    // Campo tipo de cliente
    $typeField = (new FormField())
        ->setName('customer_type')
        ->setType('select')
        ->setLabel($this->trans('Tipo de cliente', [], 'Modules.Ecomcheckout.Shop'))
        ->setRequired(true)
        ->setAvailableValues([
            'particular' => 'Particular',
            'empresa'    => 'Empresa',
            'autonomo'   => 'Autonomo',
        ]);

    return [$nifField, $typeField];
}

/**
 * Hook actionSubmitAccountBefore — validar antes de guardar
 */
public function hookActionSubmitAccountBefore($params)
{
    $nif = Tools::getValue('nif');

    // Validar formato NIF/CIF espanol
    if (!$this->isValidNif($nif)) {
        // Anadir error al formulario
        $params['errors'][] = $this->trans(
            'El NIF/CIF no tiene un formato valido.',
            [], 'Modules.Ecomcheckout.Shop'
        );
    }
}

/**
 * Hook actionObjectCustomerAddAfter — guardar campo custom
 */
public function hookActionObjectCustomerAddAfter($params)
{
    $customer = $params['object'];
    $nif = Tools::getValue('nif');
    $type = Tools::getValue('customer_type');

    if ($nif) {
        // Guardar en tabla propia
        Db::getInstance()->insert('ecom_customer_extra', [
            'id_customer'   => (int) $customer->id,
            'nif'           => pSQL($nif),
            'customer_type' => pSQL($type),
            'date_add'      => date('Y-m-d H:i:s'),
        ], false, true, Db::ON_DUPLICATE_KEY);
    }
}

protected function isValidNif(string $nif): bool
{
    $nif = strtoupper(trim($nif));
    // DNI: 8 digitos + letra
    if (preg_match('/^[0-9]{8}[A-Z]$/', $nif)) return true;
    // NIE: X/Y/Z + 7 digitos + letra
    if (preg_match('/^[XYZ][0-9]{7}[A-Z]$/', $nif)) return true;
    // CIF: letra + 8 digitos
    if (preg_match('/^[ABCDEFGHJNPQRSUVW][0-9]{7}[0-9A-J]$/', $nif)) return true;
    return false;
}

#Paso de checkout personalizado

En PS 1.7+ el checkout es modular. Cada paso es una clase que extiende AbstractCheckoutStep. Puedes anadir pasos nuevos sobreescribiendo el CheckoutProcess o via hooks que inyectan contenido en pasos existentes.

Inyectar contenido en el paso de envio
php
<?php
/**
 * displayAfterCarrier — opciones extra despues de elegir transportista
 */
public function hookDisplayAfterCarrier($params)
{
    $cart = $this->context->cart;
    $carrier = new Carrier((int) $cart->id_carrier);

    // Solo mostrar para transportistas con recogida
    if (!$carrier->is_module) {
        return '';
    }

    $this->context->smarty->assign([
        'carrier_name'  => $carrier->name,
        'delivery_days' => $carrier->delay[$this->context->language->id] ?? '',
        'show_insurance' => true,
        'insurance_price' => Tools::displayPrice(4.99),
    ]);

    return $this->display(__FILE__, 'views/templates/hook/after-carrier.tpl');
}

#Validar datos antes de confirmar pedido

actionValidateOrder — validacion y logica post-pedido
php
<?php
/**
 * Este hook se ejecuta DESPUES de que el pedido se crea en la BD.
 * No puedes cancelar el pedido aqui, pero puedes:
 * - Enviar datos a ERP/CRM
 * - Registrar la venta en analytics
 * - Asignar puntos de fidelidad
 * - Actualizar inventario externo
 */
public function hookActionValidateOrder($params)
{
    /** @var Order $order */
    $order    = $params['order'];
    /** @var Customer $customer */
    $customer = $params['customer'];
    /** @var Currency $currency */
    $currency = $params['currency'];
    /** @var OrderState $orderStatus */
    $orderStatus = $params['orderStatus'];

    // Ejemplo 1: Enviar a webhook externo (ERP, Zapier, etc.)
    $payload = json_encode([
        'event'     => 'order.created',
        'order_id'  => $order->id,
        'reference' => $order->reference,
        'total'     => $order->total_paid_tax_incl,
        'currency'  => $currency->iso_code,
        'customer'  => [
            'id'    => $customer->id,
            'email' => $customer->email,
            'name'  => $customer->firstname . ' ' . $customer->lastname,
        ],
        'products'  => $this->getOrderProducts($order),
    ]);

    // Enviar async (no bloquear el checkout)
    $webhookUrl = Configuration::get('ECOM_WEBHOOK_URL');
    if ($webhookUrl) {
        $this->sendWebhookAsync($webhookUrl, $payload);
    }

    // Ejemplo 2: Asignar puntos de fidelidad
    $points = (int) floor($order->total_paid_tax_incl);
    Db::getInstance()->insert('ecom_loyalty_points', [
        'id_customer' => (int) $customer->id,
        'id_order'    => (int) $order->id,
        'points'      => $points,
        'type'        => 'earn',
        'date_add'    => date('Y-m-d H:i:s'),
    ]);
}

protected function sendWebhookAsync(string $url, string $payload): void
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,       // Max 5 segundos
        CURLOPT_CONNECTTIMEOUT => 3,
    ]);
    curl_exec($ch);
    curl_close($ch);
}

#Modificar la pagina de confirmacion

displayOrderConfirmation — contenido post-compra
php
<?php
public function hookDisplayOrderConfirmation($params)
{
    $order = $params['order'];

    // Productos comprados para upselling
    $products = $order->getProducts();
    $categoryIds = array_unique(array_column($products, 'id_category_default'));

    // Sugerir productos de las mismas categorias
    $suggestions = [];
    foreach ($categoryIds as $idCat) {
        $catProducts = Product::getProducts(
            $this->context->language->id,
            0, 4,  // offset, limit
            'position', 'ASC',
            $idCat,
            true   // solo activos
        );
        $suggestions = array_merge($suggestions, $catProducts);
    }

    // Quitar productos ya comprados
    $boughtIds = array_column($products, 'product_id');
    $suggestions = array_filter($suggestions, fn($p) => !in_array($p['id_product'], $boughtIds));
    $suggestions = array_slice($suggestions, 0, 4);

    $this->context->smarty->assign([
        'order_reference' => $order->reference,
        'suggestions'     => $suggestions,
        'module_dir'      => $this->_path,
    ]);

    return $this->display(__FILE__, 'views/templates/hook/order-confirmation.tpl');
}

#Carrito: anadir info custom

hookActionCartSave — guardar datos custom en el carrito
php
<?php
/**
 * Guardar datos adicionales cada vez que el carrito se actualiza
 * Ejemplo: mensaje de regalo, fecha de entrega deseada
 */
public function hookActionCartSave($params)
{
    $cart = $params['cart'] ?? $this->context->cart;
    if (!$cart || !$cart->id) return;

    // Solo procesar si viene del checkout
    $giftMessage = Tools::getValue('gift_message');
    $desiredDate = Tools::getValue('desired_delivery_date');

    if ($giftMessage || $desiredDate) {
        Db::getInstance()->insert('ecom_cart_extra', [
            'id_cart'       => (int) $cart->id,
            'gift_message'  => pSQL($giftMessage),
            'desired_date'  => pSQL($desiredDate),
            'date_upd'      => date('Y-m-d H:i:s'),
        ], false, true, Db::ON_DUPLICATE_KEY);
    }
}

/**
 * Recuperar datos en actionValidateOrder para copiarlos al pedido
 */
public function hookActionValidateOrder($params)
{
    $order = $params['order'];

    $extra = Db::getInstance()->getRow(
        'SELECT * FROM `' . _DB_PREFIX_ . 'ecom_cart_extra`
         WHERE id_cart = ' . (int) $order->id_cart
    );

    if ($extra) {
        Db::getInstance()->insert('ecom_order_extra', [
            'id_order'      => (int) $order->id,
            'gift_message'  => pSQL($extra['gift_message']),
            'desired_date'  => pSQL($extra['desired_date']),
            'date_add'      => date('Y-m-d H:i:s'),
        ]);
    }
}
Descargar en Markdown Pensado para pegar en ChatGPT, Claude u otra IA. Incluye solo el contenido de esta pagina.