Skip to main content
UTB Product Builder overrides WooCommerce product pricing at the cart level. Instead of using the price set in the product editor, a flow can compute a price from the data a customer submitted in the enrollment form. This enables graduated discount tiers, matrix pricing by academic level and format, and any other rule-based scheme.

The calculate_dynamic_price() method

Every flow must implement:
public function calculate_dynamic_price(array $cart_item_data): ?float;
  • $cart_item_data — the full WooCommerce cart item array for a single item. It contains all metadata added by prepare_cart_metadata(), plus WooCommerce internals.
  • Return value — a float price in the store currency (Colombian pesos for UTB), or null to leave the WooCommerce product price unchanged.
Return null — not 0.0 — when you want the default product price to apply. Returning 0.0 sets the item price to zero.

Cart metadata flow

The path from form submission to a priced cart item is:
1

Customer submits the form

The form POSTS to /?add-to-cart={product_id}. Raw field values arrive in $_POST.
2

AbstractFlow calls validate_cart_data()

The woocommerce_add_to_cart_validation hook fires. AbstractFlow calls your validate_cart_data($_POST). If it returns a string, the item is blocked and the string is shown as a WooCommerce notice.
3

AbstractFlow calls prepare_cart_metadata()

The woocommerce_add_cart_item_data hook fires. AbstractFlow calls your prepare_cart_metadata($_POST) and merges the returned array into the cart item data. The plugin automatically adds _utb_flow_id (your flow’s ID) and _utb_unique_key (prevents cart item grouping).
4

AbstractFlow calls calculate_dynamic_price()

When WooCommerce recalculates totals (woocommerce_before_calculate_totals, priority 20), AbstractFlow iterates every cart item. For items whose _utb_flow_id matches your flow, it calls calculate_dynamic_price($cart_item) and passes the result to $cart_item['data']->set_price($price).
5

Metadata persisted to order

At checkout, AbstractFlow’s save_order_item_meta() saves every cart item key prefixed with _utb_ to the WooCommerce order line item.

The woocommerce_before_calculate_totals hook

AbstractFlow hooks into this action at priority 20:
add_action('woocommerce_before_calculate_totals', [$this, 'apply_dynamic_pricing'], 20, 1);
AbstractFlow’s implementation iterates the cart and delegates to calculate_dynamic_price():
public function apply_dynamic_pricing($cart): void
{
    foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
        if (!isset($cart_item['_utb_flow_id']) || $cart_item['_utb_flow_id'] !== $this->get_id()) {
            continue;
        }

        $price = $this->calculate_dynamic_price($cart_item);

        if ($price !== null && $price > 0) {
            $cart_item['data']->set_price($price);
        }
    }
}
You do not need to hook into woocommerce_before_calculate_totals yourself — AbstractFlow does it for you.

CEP discount tiers (CEPFlow)

CEPFlow applies a percentage discount to a program’s base price depending on the customer’s relationship with UTB. The discount is looked up via CEPDiscountCalculator, which queries the external Banner API through ApiConnectionManager. The result of that lookup is stored as cart metadata in prepare_cart_metadata():
public function prepare_cart_metadata(array $post_data): array
{
    $program = $this->programs_repo->get_by_codigo($post_data['cep_programa']);

    $metadata = [
        '_utb_cep_primer_nombre'    => $this->sanitize_text($post_data['cep_primer_nombre']),
        '_utb_cep_primer_apellido'  => $this->sanitize_text($post_data['cep_primer_apellido']),
        '_utb_cep_tipo_documento'   => $this->sanitize_text($post_data['cep_tipo_documento']),
        '_utb_cep_documento'        => $this->sanitize_text($post_data['cep_documento']),
        '_utb_cep_programa_codigo'  => $program['codigo'],
        '_utb_cep_programa_nombre'  => $program['nombre'],
        '_utb_cep_precio'           => (float) $program['precio'],
    ];

    // If the customer validated a discount in the form, store the discounted price.
    if (!empty($post_data['cep_discount_data'])) {
        $discount_data = json_decode(stripslashes($post_data['cep_discount_data']), true);
        if (is_array($discount_data)) {
            foreach (['descuento_porcentaje', 'descuento_monto', 'precio_con_descuento', 'rol_detectado', 'periodo', 'concepto'] as $f) {
                if (isset($discount_data[$f])) {
                    $metadata['_utb_cep_' . $f] = $discount_data[$f];
                }
            }
        }
    }

    return $metadata;
}
calculate_dynamic_price() then reads those keys:
public function calculate_dynamic_price(array $cart_item_data): ?float
{
    // Use the discounted price if the customer qualified for one.
    if (!empty($cart_item_data['_utb_cep_precio_con_descuento'])) {
        return (float) $cart_item_data['_utb_cep_precio_con_descuento'];
    }

    // Fall back to the program's base price.
    if (isset($cart_item_data['_utb_cep_precio'])) {
        return (float) $cart_item_data['_utb_cep_precio'];
    }

    // Return null to keep the WooCommerce product price.
    return null;
}

Certificate matrix pricing (CertificadosFlow)

CertificadosFlow uses CertificatePricesRepository to look up a price from a matrix of certificate × academic level × delivery format. The repository’s key method:
public function get_price(int $cert_id, string $formato, string $nivel = ''): int
  • $cert_id — database ID of the certificate type.
  • $formato — delivery format: 'digital' or 'fisico'.
  • $nivel — academic level of the applicant. Accepted raw values are normalised to 'pregrado' or 'posgrado' using an internal mapping:
Raw valueNormalised
profesional, tecnico, tecnologia, tyt, tecnica, tecnologicapregrado
especializacion, maestria, doctorado, postgrado, pos-gradoposgrado
general (or empty)Matches any level
The repository queries wp_utb_certificate_prices and returns the matched price in Colombian pesos. A CertificadosFlow implementation of calculate_dynamic_price() would read the certificate ID, format, and level from cart metadata and pass them to get_price():
public function calculate_dynamic_price(array $cart_item_data): ?float
{
    $cert_id = (int) ($cart_item_data['_utb_cert_id'] ?? 0);
    $formato = $cart_item_data['_utb_cert_formato'] ?? 'digital';
    $nivel   = $cart_item_data['_utb_cert_nivel']   ?? '';

    if ($cert_id <= 0) {
        return null; // No certificate selected — keep product price.
    }

    $price = $this->prices_repo->get_price($cert_id, $formato, $nivel);

    return $price > 0 ? (float) $price : null;
}

Returning null to keep the default price

When calculate_dynamic_price() returns null, AbstractFlow skips calling set_price() and WooCommerce uses the price set in the product editor. Use this as your fallback when no pricing data is available:
public function calculate_dynamic_price(array $cart_item_data): ?float
{
    $price = (float) ($cart_item_data['_utb_computed_price'] ?? 0);
    return $price > 0 ? $price : null;
}
Prices are set at cart recalculation time, not at add-to-cart time. If the customer modifies the cart (changes quantity, applies a coupon), WooCommerce will fire woocommerce_before_calculate_totals again and your method will run again. Ensure calculate_dynamic_price() is idempotent — always returns the same price for the same metadata.

Build docs developers (and LLMs) love