/* Plugin Name: AEX for WooCommerce Description: Integración completa con la API de AEX (v1.5.4): cálculo de flete, solicitud/confirmación de servicio, impresión de guía, tracking, webhooks e inventario. Compatible con checkout clásico y Blocks. Version: 1.0.0 Author: Khaldun + GPT-5 Thinking Requires at least: 6.0 Tested up to: 6.6 WC requires at least: 7.6 WC tested up to: 9.2 License: GPLv2 or later Text Domain: aex-woocommerce */ if ( ! defined( 'ABSPATH' ) ) exit; // ---------- CONSTANTES ---------- const AEX_WC_VERSION = '1.0.0'; const AEX_WC_SLUG = 'aex-woocommerce'; const AEX_WC_NS = 'AEX_WC'; const AEX_WC_DIR = __DIR__; const AEX_WC_URL = __FILE__ ? plugin_dir_url( __FILE__ ) : ''; // Autocarga simple por namespaces de este archivo único (cada "archivo" estará embebido en cadenas) // Para facilitar mantenimiento, todo el código está aquí con separadores. Puedes dividirlo en archivos reales si prefieres. // ============================================================= // helpers.php // ============================================================= if ( ! function_exists( 'aex_wc_log' ) ) { function aex_wc_log( $msg, $context = [] ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { $logger = wc_get_logger(); $logger->info( is_string( $msg ) ? $msg : wp_json_encode( $msg ), ['source' => 'aex-woocommerce', 'context' => $context] ); } } } if ( ! function_exists( 'aex_wc_array_get' ) ) { function aex_wc_array_get( $arr, $key, $default = null ) { return isset( $arr[ $key ] ) ? $arr[ $key ] : $default; } } // ============================================================= // class ApiClient // ============================================================= class AEX_WC_ApiClient { private $public_key; private $private_key; private $base_url; public function __construct( $public_key, $private_key, $env = 'prod' ) { $this->public_key = trim( (string) $public_key ); $this->private_key = trim( (string) $private_key ); $this->base_url = ( $env === 'sandbox' ) ? 'https://sandbox.aex.com.py/api/v1' : 'https://aex.com.py/api/v1'; } private function get_token_transient_key() { return 'aex_wc_token_' . md5( $this->public_key . '|' . $this->base_url ); } private function generate_session_code( $length = 16 ) { $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'; $code = ''; for ( $i = 0; $i < $length; $i++ ) { $code .= $chars[ random_int(0, strlen($chars)-1) ]; } return $code; } public function get_token( $force_refresh = false ) { $key = $this->get_token_transient_key(); if ( ! $force_refresh ) { $cached = get_transient( $key ); if ( $cached ) { return $cached; } } $codigo_sesion = $this->generate_session_code(); $payload = [ 'clave_publica' => $this->public_key, 'clave_privada' => md5( $this->private_key . $codigo_sesion ), 'codigo_sesion' => $codigo_sesion, ]; $res = $this->post_json( '/autorizacion-acceso/generar', $payload, false ); if ( is_wp_error( $res ) ) return $res; $code = (int) aex_wc_array_get( $res, 'codigo', -1 ); if ( $code !== 0 ) { return new WP_Error( 'aex_token_error', 'No se pudo obtener token: ' . aex_wc_array_get( $res, 'mensaje', 'Error desconocido' ), $res ); } $token = aex_wc_array_get( $res, 'codigo_autorizacion' ); if ( ! $token ) return new WP_Error( 'aex_token_empty', 'Token vacío' ); // El token dura 10 minutos; cachear 8m para holgura set_transient( $key, $token, 8 * MINUTE_IN_SECONDS ); return $token; } private function build_headers( $needs_token = true ) { $headers = [ 'Content-Type' => 'application/json' ]; if ( $needs_token ) { $token = $this->get_token(); if ( is_wp_error( $token ) ) return $token; // La API usa el token en body, no como header, pero guardamos por si se requiere. } return $headers; } private function post_json( $path, $body, $auto_token = true, $is_file = false ) { $url = rtrim( $this->base_url, '/' ) . $path; $headers = $this->build_headers( $auto_token ); if ( is_wp_error( $headers ) ) return $headers; $args = [ 'headers' => $headers, 'body' => wp_json_encode( $body ), 'timeout' => 30, ]; $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) return $response; $code = wp_remote_retrieve_response_code( $response ); $ct = wp_remote_retrieve_header( $response, 'content-type' ); $raw = wp_remote_retrieve_body( $response ); if ( $is_file && $code === 200 && strpos( $ct, 'application/pdf' ) !== false ) { return [ 'pdf' => $raw ]; } $data = json_decode( $raw, true ); if ( $data === null ) { return new WP_Error( 'aex_json', 'Respuesta no JSON de AEX', [ 'code' => $code, 'body' => $raw ] ); } return $data; } private function add_auth( $payload ) { $token = $this->get_token(); if ( is_wp_error( $token ) ) return $token; $payload['clave_publica'] = $this->public_key; $payload['codigo_autorizacion'] = $token; return $payload; } public function ciudades( $origen = null ) { $payload = $this->add_auth( [] ); if ( is_wp_error( $payload ) ) return $payload; if ( $origen ) $payload['origen'] = $origen; return $this->post_json( '/envios/ciudades', $payload ); } public function puntos_entrega( $id_tipo_servicio, $origen, $destino ) { $payload = $this->add_auth( compact('id_tipo_servicio','origen','destino') ); if ( is_wp_error( $payload ) ) return $payload; return $this->post_json( '/envios/puntos_entrega', $payload ); } public function imprimir( $guia, $formato = 'guia' ) { $payload = $this->add_auth( [ 'guia' => $guia, 'formato' => $formato ] ); if ( is_wp_error( $payload ) ) return $payload; return $this->post_json( '/envios/imprimir', $payload, true, true ); } public function calcular( $origen, $destino, $paquetes, $codigo_tipo_carga = 'P' ) { $payload = $this->add_auth( [ 'origen' => $origen, 'destino' => $destino, 'paquetes' => $paquetes, 'codigo_tipo_carga' => $codigo_tipo_carga ] ); if ( is_wp_error( $payload ) ) return $payload; return $this->post_json( '/envios/calcular', $payload ); } public function solicitar_servicio( $origen, $destino, $codigo_operacion, $paquetes, $codigo_tipo_carga = 'P', $importe_cobro = 0 ) { $payload = $this->add_auth( compact('origen','destino','codigo_operacion','paquetes','codigo_tipo_carga','importe_cobro') ); if ( is_wp_error( $payload ) ) return $payload; return $this->post_json( '/envios/solicitar_servicio', $payload ); } public function confirmar_servicio( $id_solicitud, $id_tipo_servicio, $remitente, $pickup, $destinatario, $entrega, $adicionales = [], $codigo_forma_pago = 'C', $total_cobro = null ) { $payload = $this->add_auth( [ 'Id_solicitud' => (int) $id_solicitud, 'id_tipo_servicio' => (int) $id_tipo_servicio, 'remitente' => $remitente, 'pickup' => $pickup, 'destinatario' => $destinatario, 'entrega' => $entrega, 'adicionales' => array_values( array_map( 'intval', (array) $adicionales ) ), 'codigo_forma_pago'=> $codigo_forma_pago, ] ); if ( ! is_null( $total_cobro ) ) { $payload['total_cobro'] = (int) $total_cobro; } if ( is_wp_error( $payload ) ) return $payload; return $this->post_json( '/envios/confirmar_servicio', $payload ); } public function cancelar( $numero_guia ) { $payload = $this->add_auth( [ 'numero_guia' => $numero_guia ] ); if ( is_wp_error( $payload ) ) return $payload; return $this->post_json( '/envios/cancelar', $payload ); } public function tracking( $numero_guia = null, $codigo_operacion = null ) { $payload = $this->add_auth( [] ); if ( is_wp_error( $payload ) ) return $payload; if ( $numero_guia ) $payload['numero_guia'] = $numero_guia; if ( $codigo_operacion ) $payload['codigo_operacion'] = $codigo_operacion; return $this->post_json( '/envios/tracking', $payload ); } public function inventario_existencia( $codigos_producto = [] ) { $payload = $this->add_auth( [] ); if ( is_wp_error( $payload ) ) return $payload; if ( ! empty( $codigos_producto ) ) $payload['codigos_producto'] = array_values( (array) $codigos_producto ); return $this->post_json( '/inventario/existencia', $payload ); } } // ============================================================= // class AdminSettings // ============================================================= class AEX_WC_AdminSettings { public static function init() { add_action( 'admin_init', [ __CLASS__, 'register' ] ); add_action( 'admin_menu', [ __CLASS__, 'menu' ] ); } public static function register() { register_setting( 'aex_wc_settings', 'aex_wc_settings', [ __CLASS__, 'sanitize' ] ); add_settings_section( 'aex_wc_main', __( 'Credenciales y Configuración', 'aex-woocommerce' ), '__return_false', 'aex_wc' ); $fields = [ 'env' => [ 'label' => 'Entorno', 'type' => 'select', 'options' => [ 'prod' => 'Producción', 'sandbox' => 'Sandbox' ] ], 'public_key' => [ 'label' => 'Clave pública', 'type' => 'text' ], 'private_key'=> [ 'label' => 'Clave privada', 'type' => 'password' ], 'origen_ciudad'=> [ 'label' => 'Código ciudad de ORIGEN', 'type' => 'text' ], 'map_ciudad_strict'=> [ 'label' => 'Match estricto ciudad destino (por nombre)', 'type' => 'checkbox' ], 'default_forma_pago'=> [ 'label' => 'Forma de pago AEX (C crédito / D destino)', 'type' => 'text', 'placeholder' => 'C' ], 'auto_confirm_status'=> [ 'label' => 'Confirmar servicio al cambiar a este estado de pedido', 'type' => 'text', 'placeholder' => 'processing' ], 'webhook_secret' => [ 'label' => 'Secreto Webhook (validación opcional)', 'type' => 'text' ], ]; foreach ( $fields as $key => $cfg ) { add_settings_field( $key, esc_html( $cfg['label'] ), [ __CLASS__, 'render_field' ], 'aex_wc', 'aex_wc_main', [ 'key' => $key, 'cfg' => $cfg ] ); } } public static function sanitize( $input ) { $clean = []; $clean['env'] = in_array( aex_wc_array_get($input,'env','prod'), ['prod','sandbox'], true ) ? $input['env'] : 'prod'; $clean['public_key'] = sanitize_text_field( aex_wc_array_get($input,'public_key','') ); $clean['private_key']= sanitize_text_field( aex_wc_array_get($input,'private_key','') ); $clean['origen_ciudad'] = sanitize_text_field( aex_wc_array_get($input,'origen_ciudad','') ); $clean['map_ciudad_strict'] = ! empty( $input['map_ciudad_strict'] ) ? 1 : 0; $clean['default_forma_pago'] = strtoupper( sanitize_text_field( aex_wc_array_get($input,'default_forma_pago','C') ) ) === 'D' ? 'D' : 'C'; $clean['auto_confirm_status'] = sanitize_text_field( aex_wc_array_get($input,'auto_confirm_status','') ); $clean['webhook_secret'] = sanitize_text_field( aex_wc_array_get($input,'webhook_secret','') ); return $clean; } public static function render_field( $args ) { $opts = get_option( 'aex_wc_settings', [] ); $key = $args['key']; $cfg = $args['cfg']; $val = aex_wc_array_get( $opts, $key ); if ( $cfg['type'] === 'select' ) { echo ''; } elseif ( $cfg['type'] === 'checkbox' ) { echo ''; } else { printf('', esc_attr($cfg['type']), esc_attr($key), esc_attr($val), esc_attr( aex_wc_array_get($cfg,'placeholder','') ) ); } } public static function menu() { add_options_page( 'AEX', 'AEX', 'manage_woocommerce', 'aex_wc', [ __CLASS__, 'page' ] ); } public static function page() { echo '

AEX – WooCommerce

'; settings_fields( 'aex_wc_settings' ); do_settings_sections( 'aex_wc' ); submit_button(); echo '

'; echo '

Endpoints Webhook: ' . esc_html( site_url( '?wc-api=aex_webhook' ) ) . '

'; echo '
'; } } // ============================================================= // class ShippingMethod // ============================================================= class WC_Shipping_AEX extends WC_Shipping_Method { public function __construct( $instance_id = 0 ) { $this->id = 'aex_shipping'; $this->instance_id = absint( $instance_id ); $this->method_title = 'AEX Envíos'; $this->method_description = 'Cotización online con AEX /envios/calcular'; $this->supports = [ 'shipping-zones', 'instance-settings', 'settings' ]; $this->enabled = 'yes'; $this->title = 'AEX'; $this->init(); } public function init() { $this->init_form_fields(); $this->init_settings(); add_action( 'woocommerce_update_options_shipping_' . $this->id, [ $this, 'process_admin_options' ] ); } public function init_form_fields() { $this->form_fields = [ 'enabled' => [ 'title' => 'Habilitar', 'type' => 'checkbox', 'default' => 'yes' ], 'title' => [ 'title' => 'Título', 'type' => 'text', 'default' => 'AEX' ], 'markup' => [ 'title' => 'Markup fijo Gs (opcional)', 'type' => 'number', 'default' => '0' ], ]; } public function calculate_shipping( $package = [] ) { $global = get_option( 'aex_wc_settings', [] ); $origin_code = aex_wc_array_get( $global, 'origen_ciudad' ); if ( ! $origin_code ) return; $dest_city_name = aex_wc_array_get( $package, 'destination', [] )['city'] ?? ''; $env = aex_wc_array_get( $global, 'env', 'prod' ); $client = new AEX_WC_ApiClient( aex_wc_array_get($global,'public_key'), aex_wc_array_get($global,'private_key'), $env ); $cities = $client->ciudades(); if ( is_wp_error( $cities ) ) return; $datos = aex_wc_array_get( $cities, 'datos', [] ); $dest_code = ''; foreach ( $datos as $row ) { $name = trim( mb_strtolower( aex_wc_array_get($row,'denominacion','') ) ); if ( $name && $name === mb_strtolower( $dest_city_name ) ) { $dest_code = aex_wc_array_get($row,'codigo_ciudad'); break; } } if ( ! $dest_code && empty( $global['map_ciudad_strict'] ) && $dest_city_name ) { // fuzzy: contiene o inicia con foreach ( $datos as $row ) { $name = mb_strtolower( aex_wc_array_get($row,'denominacion','') ); if ( $name && ( str_starts_with( $name, mb_strtolower($dest_city_name) ) || str_contains( $name, mb_strtolower($dest_city_name) ) ) ) { $dest_code = aex_wc_array_get($row,'codigo_ciudad'); break; } } } if ( ! $dest_code ) return; $paquetes = $this->build_packages_from_cart( $package ); $res = $client->calcular( $origin_code, $dest_code, $paquetes ); if ( is_wp_error( $res ) ) return; if ( (int) aex_wc_array_get( $res, 'codigo', -1 ) !== 0 ) return; foreach ( aex_wc_array_get( $res, 'datos', [] ) as $service ) { $cost = floatval( aex_wc_array_get( $service, 'costo_flete', 0 ) ); $label = aex_wc_array_get( $service, 'tipo_servicio', 'AEX' ); $desc = aex_wc_array_get( $service, 'descripcion', '' ); $timeh = intval( aex_wc_array_get( $service, 'tiempo_entrega', 0 ) ); $markup = floatval( $this->get_option('markup','0') ); $rate = [ 'id' => $this->id . ':' . (int) aex_wc_array_get($service,'id_tipo_servicio'), 'label' => $label . ( $timeh ? " ({$timeh}h est.)" : '' ), 'cost' => max( 0, $cost + $markup ), 'meta_data' => [ 'aex_id_tipo_servicio' => (int) aex_wc_array_get($service,'id_tipo_servicio'), 'aex_desc' => $desc ], ]; $this->add_rate( $rate ); } } private function build_packages_from_cart( $package ) { $items = $package['contents'] ?? []; $out = []; foreach ( $items as $line ) { $product = $line['data']; $q = (int) $line['quantity']; $w = max( 0.01, wc_get_weight( $product->get_weight() ?: 0.5, 'kg' ) ); $dim_l = wc_get_dimension( $product->get_length() ?: 10, 'cm' ); $dim_w = wc_get_dimension( $product->get_width() ?: 10, 'cm' ); $dim_h = wc_get_dimension( $product->get_height() ?: 10, 'cm' ); $out[] = [ 'descripcion' => $product->get_name(), 'codigo_externo' => (string) ( $product->get_sku() ?: $product->get_id() ), 'cantidad' => $q, 'peso' => (float) $w, 'largo' => (float) $dim_l, 'alto' => (float) $dim_h, 'ancho' => (float) $dim_w, 'valor' => (float) wc_get_price_excluding_tax( $product, [ 'qty' => $q ] ), ]; } if ( empty( $out ) ) { $out[] = [ 'descripcion' => 'Paquete', 'codigo_externo' => 'GEN', 'cantidad' => 1, 'peso' => 0.5, 'largo' => 10, 'alto' => 10, 'ancho' => 10, 'valor' => 0 ]; } return $out; } } // Registrar método de envío add_action( 'woocommerce_shipping_init', function(){ /* class defined above */ } ); add_filter( 'woocommerce_shipping_methods', function( $methods ) { $methods['aex_shipping'] = 'WC_Shipping_AEX'; return $methods; } ); // ============================================================= // class OrderActions (solicitar + confirmar + imprimir + tracking) // ============================================================= class AEX_WC_OrderActions { public static function init() { add_action( 'add_meta_boxes', [ __CLASS__, 'metabox' ] ); add_action( 'woocommerce_order_status_changed', [ __CLASS__, 'maybe_auto_confirm' ], 10, 4 ); add_action( 'admin_post_aex_create_shipment', [ __CLASS__, 'handle_create_shipment' ] ); add_action( 'admin_post_aex_print_label', [ __CLASS__, 'handle_print_label' ] ); } public static function metabox() { add_meta_box( 'aex_wc_box', 'AEX', [ __CLASS__, 'render_box' ], 'shop_order', 'side', 'default' ); } public static function render_box( $post ) { $order = wc_get_order( $post->ID ); $guia = $order->get_meta( '_aex_guia' ); echo '

Guía: ' . ( $guia ? esc_html( $guia ) : 'No generada' ) . '

'; $nonce = wp_create_nonce( 'aex_wc_nonce' ); echo '

Solicitar + Confirmar

'; if ( $guia ) { echo '

Imprimir Guía (PDF)

'; self::render_tracking_table( $order ); } } private static function client() { $opts = get_option( 'aex_wc_settings', [] ); return new AEX_WC_ApiClient( aex_wc_array_get($opts,'public_key'), aex_wc_array_get($opts,'private_key'), aex_wc_array_get($opts,'env','prod') ); } private static function map_city_code( $name ) { $opts = get_option( 'aex_wc_settings', [] ); $client = self::client(); $res = $client->ciudades(); if ( is_wp_error($res) ) return ''; foreach ( aex_wc_array_get($res,'datos',[]) as $row ) { if ( mb_strtolower( $row['denominacion'] ) === mb_strtolower( $name ) ) return $row['codigo_ciudad']; } if ( empty( $opts['map_ciudad_strict'] ) ) { foreach ( aex_wc_array_get($res,'datos',[]) as $row ) { $n = mb_strtolower( $row['denominacion'] ); if ( str_starts_with( $n, mb_strtolower($name) ) || str_contains( $n, mb_strtolower($name) ) ) return $row['codigo_ciudad']; } } return ''; } private static function build_party_from_order( WC_Order $order, $type = 'destinatario' ) { $first = $type === 'destinatario' ? $order->get_shipping_first_name() : get_bloginfo('name'); $last = $type === 'destinatario' ? $order->get_shipping_last_name() : ''; $email = $type === 'destinatario' ? $order->get_billing_email() : get_option('admin_email'); $phone = preg_replace('/\D+/','', $type === 'destinatario' ? $order->get_billing_phone() : get_option('admin_phone','') ); $city = $type === 'destinatario' ? $order->get_shipping_city() : ''; $city_code = $city ? self::map_city_code( $city ) : ''; $code = (string) ( $type === 'destinatario' ? ( $order->get_customer_id() ?: ( 'GUEST-' . $order->get_id() ) ) : 'STORE-1' ); $tel_arr = $phone ? [['numero' => (int) $phone, 'denominacion' => 'Principal']] : [['numero' => 595000000, 'denominacion' => 'Default']]; return [ 'codigo' => $code, 'tipo_documento' => 'CIP', 'numero_documento' => $code, 'nombre' => $first ?: 'Cliente', 'apellido' => $last, 'email' => $email ?: 'no-reply@example.com', 'personería' => 'F', 'telefonos' => $tel_arr, 'fecha_nacimiento' => '1990-01-01', // direccion/entrega se arma en pickup/entrega ]; } private static function build_location_from_order( WC_Order $order, $for = 'entrega' ) { $street = $order->get_shipping_address_1(); $number = 0; $cross1 = $order->get_shipping_address_2(); $city = $order->get_shipping_city(); $code = strtoupper( substr( md5( $order->get_id() . $for ), 0, 10 ) ); $city_code = $city ? self::map_city_code( $city ) : ''; return [ 'codigo' => $code, 'calle_principal'=> $street ?: 'S/N', 'numero_casa' => (int) $number, 'calle_transversal_1' => $cross1 ?: '-', 'calle_transversal_2' => '-', 'codigo_ciudad' => $city_code ?: 'ASU', 'telefono' => (int) preg_replace('/\D+/','', $order->get_billing_phone() ?: '000' ), 'telefono_movil' => (int) preg_replace('/\D+/','', $order->get_billing_phone() ?: '000' ), 'referencias' => 'Pedido #' . $order->get_order_number(), ]; } public static function maybe_auto_confirm( $order_id, $old, $new, $order ) { $opts = get_option( 'aex_wc_settings', [] ); if ( $new !== aex_wc_array_get( $opts, 'auto_confirm_status', '' ) ) return; self::create_and_confirm( $order ); } public static function handle_create_shipment() { if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( $_GET['_wpnonce'] ?? '', 'aex_wc_nonce' ) ) wp_die('No permitido'); $order = wc_get_order( absint( $_GET['order_id'] ?? 0 ) ); self::create_and_confirm( $order, true ); wp_safe_redirect( wp_get_referer() ); exit; } private static function create_and_confirm( WC_Order $order = null, $add_note = false ) { if ( ! $order ) return; if ( $order->get_meta('_aex_guia') ) { if ( $add_note ) $order->add_order_note('AEX: ya tenía guía.'); return; } $opts = get_option( 'aex_wc_settings', [] ); $client = self::client(); $origen = aex_wc_array_get( $opts, 'origen_ciudad' ); $destino= self::map_city_code( $order->get_shipping_city() ); $codigo_operacion = (string) $order->get_id(); $paquetes = []; foreach ( $order->get_items() as $item ) { $p = $item->get_product(); if ( ! $p ) continue; $q = (int) $item->get_quantity(); $w = max( 0.01, wc_get_weight( $p->get_weight() ?: 0.5, 'kg' ) ); $L = wc_get_dimension( $p->get_length() ?: 10, 'cm' ); $W = wc_get_dimension( $p->get_width() ?: 10, 'cm' ); $H = wc_get_dimension( $p->get_height() ?: 10, 'cm' ); $paquetes[] = [ 'descripcion' => $p->get_name(), 'codigo_externo' => (string) ( $p->get_sku() ?: $p->get_id() ), 'cantidad' => $q, 'peso' => (float)$w, 'largo' => (float)$L, 'alto' => (float)$H, 'ancho' => (float)$W, 'valor' => (float) $item->get_total() ]; } if ( empty( $paquetes ) ) $paquetes[] = [ 'descripcion' => 'Paquete', 'codigo_externo' => 'GEN', 'cantidad' => 1, 'peso' => 0.5, 'largo' => 10, 'alto' => 10, 'ancho' => 10, 'valor' => (float) $order->get_total() ]; $sol = $client->solicitar_servicio( $origen, $destino, $codigo_operacion, $paquetes ); if ( is_wp_error( $sol ) || (int) aex_wc_array_get($sol,'codigo',-1) !== 0 ) { $order->add_order_note('AEX solicitar_servicio ERROR: ' . ( is_wp_error($sol)?$sol->get_error_message():aex_wc_array_get($sol,'mensaje','') ) ); return; } $id_solicitud = (int) aex_wc_array_get( $sol['datos'] ?? [], 'id_solicitud', 0 ); $condiciones = aex_wc_array_get( $sol['datos'] ?? [], 'condiciones', [] ); $elegida = null; $min = INF; foreach ( $condiciones as $c ) { $costo = floatval( aex_wc_array_get($c,'costo_flete',INF) ); if ( $costo < $min ) { $min = $costo; $elegida = $c; } } if ( ! $elegida ) { $order->add_order_note('AEX: no se hallaron condiciones'); return; } $remitente = self::build_party_from_order( $order, 'remitente' ); $destinatario = self::build_party_from_order( $order, 'destinatario' ); $pickup = self::build_location_from_order( $order, 'pickup' ); $entrega = self::build_location_from_order( $order, 'entrega' ); $adics = []; $forma = aex_wc_array_get( $opts, 'default_forma_pago', 'C' ); $conf = $client->confirmar_servicio( $id_solicitud, (int) $elegida['id_tipo_servicio'], $remitente, $pickup, $destinatario, $entrega, $adics, $forma ); if ( is_wp_error( $conf ) || (int) aex_wc_array_get($conf,'codigo',-1) !== 0 ) { $order->add_order_note('AEX confirmar_servicio ERROR: ' . ( is_wp_error($conf)?$conf->get_error_message():aex_wc_array_get($conf,'mensaje','') ) ); return; } $guia = aex_wc_array_get( $conf['datos'] ?? [], 'numero_guia' ); if ( $guia ) { $order->update_meta_data( '_aex_guia', $guia ); $order->save(); if ( $add_note ) $order->add_order_note( 'AEX: Guía generada ' . $guia ); } } public static function handle_print_label() { if ( ! current_user_can( 'manage_woocommerce' ) || ! wp_verify_nonce( $_GET['_wpnonce'] ?? '', 'aex_wc_nonce' ) ) wp_die('No permitido'); $order = wc_get_order( absint( $_GET['order_id'] ?? 0 ) ); $guia = $order ? $order->get_meta('_aex_guia') : ''; if ( ! $guia ) wp_die('No hay guía'); $client = self::client(); $res = $client->imprimir( $guia, 'guia_A4' ); if ( is_wp_error( $res ) || empty( $res['pdf'] ) ) wp_die('Error al recuperar PDF'); header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="AEX-'.$guia.'.pdf"'); echo $res['pdf']; exit; } private static function render_tracking_table( WC_Order $order ) { $client = self::client(); $guia = $order->get_meta('_aex_guia'); $res = $client->tracking( $guia, null ); if ( is_wp_error($res) || (int) aex_wc_array_get($res,'codigo',-1) !== 0 ) { echo '

Tracking no disponible.

'; return; } echo ''; foreach ( aex_wc_array_get($res,'datos',[]) as $ev ) { printf('', esc_html($ev['fecha']??''), esc_html($ev['estado']??''), esc_html($ev['tipo_evento']??'') ); } echo '
FechaEstadoTipo
%s%s%s
'; } } // ============================================================= // class WebhookController // ============================================================= class AEX_WC_WebhookController { public static function init() { add_action( 'init', function(){ add_rewrite_tag( '%aex_wc_webhook%', '([^&]+)' ); } ); add_action( 'woocommerce_api_aex_webhook', [ __CLASS__, 'handle' ] ); } public static function handle() { $body = file_get_contents('php://input'); $data = json_decode( $body, true ); $status = 200; $ok = false; $msg = ''; try { $opts = get_option('aex_wc_settings',[]); $secret = aex_wc_array_get($opts,'webhook_secret',''); if ( $secret ) { $hdr = $_SERVER['HTTP_X_AEX_SECRET'] ?? ''; if ( ! hash_equals( $secret, $hdr ) ) { throw new Exception('Secreto inválido'); } } if ( empty( $data['guia'] ) ) throw new Exception('Payload inválido'); $guia = (string) $data['guia']; // Buscar pedido por meta _aex_guia $orders = wc_get_orders([ 'limit'=>1, 'meta_key'=>'_aex_guia', 'meta_value'=>$guia, 'return'=>'objects' ]); if ( $orders ) { $order = $orders[0]; $state = $data['estado'] ?? ''; $type = $data['tipo_evento'] ?? ''; $obs = $data['observacion'] ?? ''; $order->add_order_note( 'AEX Webhook: '. $state . ' / ' . $type . ( $obs ? ' — ' . $obs : '' ) ); if ( isset($data['estado']) && strtolower($data['estado']) === 'entregado' ) { $order->update_status('completed'); } } $ok = true; $msg = 'OK'; } catch ( Throwable $e ) { $status = 400; $ok = false; $msg = $e->getMessage(); } http_response_code( $status ); header('Content-Type: application/json'); echo wp_json_encode([ 'status'=>$status, 'message'=>$msg, 'isSuccess'=>$ok ]); exit; } } // ============================================================= // Inventario – utilidades simples (WP-CLI opcional) // ============================================================= if ( defined('WP_CLI') && WP_CLI ) { WP_CLI::add_command( 'aex:inventario', function( $args, $assoc ){ $client = new AEX_WC_ApiClient( aex_wc_array_get(get_option('aex_wc_settings',[]),'public_key'), aex_wc_array_get(get_option('aex_wc_settings',[]),'private_key'), aex_wc_array_get(get_option('aex_wc_settings',[]),'env','prod') ); $skus = array_filter( array_map('trim', explode(',', $assoc['skus'] ?? '') ) ); $res = $client->inventario_existencia( $skus ); WP_CLI::log( wp_json_encode( $res, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE ) ); }); } // ============================================================= // Bootstrap // ============================================================= add_action( 'plugins_loaded', function(){ // Requisitos if ( ! class_exists( 'WooCommerce' ) ) return; AEX_WC_AdminSettings::init(); AEX_WC_OrderActions::init(); AEX_WC_WebhookController::init(); }); // Desinstalación limpia register_uninstall_hook( __FILE__, function(){ delete_option('aex_wc_settings'); } ); Página no encontrada – PY Online
404

¡Oops! No se encontró la página.

Parece que no se encontró nada en este sitio. ¿Quizás deba probar uno de los links inferiores o una búsqueda?

WordPress Appliance - Powered by TurnKey Linux