_utb_form_config) or in a certificate record’s form_config_json column. The form renderer reads this array and generates the HTML, JavaScript event bindings, and AJAX handlers at runtime.
JSON configuration schema
Each element in the fields array is a field object. Common properties:Supported field types
| Type | Description |
|---|---|
text | Standard text input |
email | Email input with browser validation |
tel | Telephone input |
number | Numeric input; supports min, max |
select | Dropdown; static options via options object |
textarea | Multi-line text area |
checkbox | Single checkbox |
file | File upload; supports accept and max_size (KB) |
hidden | Hidden input (e.g., computed price) |
heading / section | Visual section separator with an <h3> label |
notice | Informational callout with icon, title, and message |
system_program_selector | Special: loads program options from DataSourceManager |
system_certificate_selector | Special: loads certificate options from DataSourceManager |
Validation attributes
Text-based fields support inline HTML validation:minlength, maxlength, and pattern.
Logic Binding system
The Logic Binding system is the mechanism that makes forms dynamic without per-form JavaScript. Instead of writing custom$('#field').on('change', ...) handlers, you declare the rule as a JSON object on the field that should respond.
The generic frontend script (form-builder.js) reads all logic_binding objects on page load, then constructs event listeners dynamically.
Declarative binding example
The following field only appears whenutb_campo_a equals "ESTUDIANTE":
Logic binding properties
| Property | Description |
|---|---|
action | What to do when the rule matches: show or hide |
target_field | The id of the field whose value is observed |
operator | Comparison to apply: equals, not_equals, contains, empty, not_empty |
value | The value to compare against |
Special binding values
Beyond the generic declarative format, some fields use named binding strings that trigger specialized rendering logic:| Binding string | Effect |
|---|---|
cert_selector | Field renders the certificate <select> with AJAX reload on tipo/nivel change |
cert_qty | Field is the quantity input; hidden by default, shown only when the selected certificate has qty_enabled = 1 |
cert_program | Field renders the academic program <select> filtered by nivel |
cep_program | Field renders the CEP program selector with real-time price display |
How the form renderer works
FormConfigManager resolves the JSON
FormConfigManager::get_config(int $product_id, string $flow_id) checks, in order:- Product post meta key
_utb_form_config(JSON string) - Linked certificate record’s
form_config_jsoncolumn via_utb_linked_cert_idmeta - The flow’s own
get_default_config()method
Flow calls render_dynamic_form
The concrete flow receives the resolved config array and iterates
$config['fields']. For each field object it calls its internal render_field() method.render_field dispatches on type and binding
The renderer checks
logic_binding and type to decide which HTML block to output. Special types such as system_program_selector delegate to dedicated private methods that query DataSourceManager.Custom CSS is injected
If
$config['custom_css'] is set, a scoped <style> block is output inside the form wrapper before the fields.dynamic_select AJAX loading pattern
For certificate and program selectors, the form renders an initial <select> on page load. When the user changes a filter field (tipo, nivel, formato), the frontend fires an AJAX request to reload dependent options.
CertificadosFlow example — reloading certificates after tipo/nivel change:
certs array and re-populates the <select> without a page reload.
Real-time price updates
For CEP programs, price changes occur entirely client-side. Each<option> in the program selector carries a data-precio attribute populated from wp_utb_cep_programs.precio. The frontend reads this attribute on change and updates the price display without an AJAX round-trip.
For Academic Certificates, price depends on three variables: certificate ID, delivery format, and academic level. These are resolved server-side:
formatted) that the frontend injects directly into the DOM.
Server-side validation flow
Validation happens in two stages: Stage 1 — WooCommerce add-to-cart hookAbstractFlow::validate_add_to_cart is bound to woocommerce_add_to_cart_validation. It calls the concrete flow’s validate_cart_data(array $post_data), which returns true or an error string. On error, wc_add_notice() is called and the add-to-cart action is blocked.
Stage 2 — RulesEngine (dynamic validation)
For fields that have API-backed validation rules configured from the dashboard, CertificadosFlow::validate_cart_data delegates to RulesEngine:
should_show_field evaluates logic_binding rules using the submitted POST data so that hidden (non-visible) fields are not validated. This keeps validation consistent with what the user actually sees.
CEP discount validation
The CEPFlow applies an additional AJAX-driven validation step before the user can add to cart. When the user enters their ID number, the frontend calls thecep_calculate_discount action:
- The server checks identity against Banner via
CEPRolesValidator. - If found, it compares the submitted names against the Banner record to prevent impersonation.
- If roles are found,
CEPDiscountCalculatorqueries the discount table and returnsdescuento_porcentaje,precio_con_descuento, androl_detectado. - The result is cached in a WordPress Transient for 15 minutes keyed by
md5(document + program + period + names_hash). - The discount data is serialized to JSON and stored in a hidden input
cep_discount_data, which is validated again server-side duringvalidate_cart_data.