Miravia Connector Bot 09d24aa191 Fix image upload structure for Miravia API compliance
🔧 Bug Fixes:
- Fixed product image structure to match Miravia API requirements
- Updated MiraviaProduct.php getData() method to wrap images in {"Image": [...]} format
- Updated MiraviaCombination.php getData() method to wrap SKU images properly
- Resolved error "[4224] The Main image of the product is required"

📋 Changes:
- Modified getData() methods to transform flat image arrays to nested structure
- Product images: images[] → Images: {"Image": [...]}
- SKU images: images[] → Images: {"Image": [...]}
- Maintains backward compatibility for empty image arrays

🎯 Impact:
- Product uploads will now pass Miravia's image validation
- Both product-level and SKU-level images properly formatted
- Complies with official Miravia API documentation structure

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 09:55:11 +02:00

792 lines
30 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) { exit; }
if( !class_exists('MiraviaCore') ) {
class MiraviaCore {
/**
* Add account miravia
*
* @param array $data
* @return bool
*
* name tinytext NOT NULL,
token varchar(200) NOT NULL,
userid varchar(30) DEFAULT '' NOT NULL,
config TEXT NOT NULL DEFAULT '{}',
lang varchar(10) NOT NULL
*/
static function add_account($args) {
global $wpdb;
$default = array(
'name' => '',
'token' => '',
'userid' => '',
'lang' => '',
'email' => '',
'config' => '{}',
);
$args = wp_parse_args( $args, $default );
if($args['token'] == '') {
return false;
}
$wpdb->insert($wpdb->prefix.'miravia_accounts',$args);
$my_id = $wpdb->insert_id;
return $my_id;
}
static function delete_account($id = false) {
global $wpdb;
if($id and current_user_can( 'manage_options' )) {
return $wpdb->delete($wpdb->prefix.'miravia_accounts', array('id' => $id));
}
return false;
}
static function delete_profile($id = false) {
global $wpdb;
if($id and current_user_can( 'manage_options' )) {
return $wpdb->delete($wpdb->prefix.'miravia_profiles', array('id' => $id));
}
return false;
}
static function delete_rule($id = false) {
global $wpdb;
if($id and current_user_can( 'manage_options' )) {
return $wpdb->delete($wpdb->prefix.'miravia_rules', array('id' => $id));
}
return false;
}
static function debug($v, $json = true) {
if($json) {
die(json_encode(($v)));
}
die("<pre>".print_r($v, true)."</pre>");
}
static function add_profile($args) {
global $wpdb;
$default = array(
'name' => '',
'accounts_id' => '',
'categories' => '',
'miravia_category' => '',
'config' => '{}',
);
$args = wp_parse_args( $args, $default );
$wpdb->insert($wpdb->prefix.'miravia_profiles',$args);
$my_id = $wpdb->insert_id;
return $my_id;
}
static function request_notify($apiKey, $message, $customSeconds = false){
$secondsNotify = !$customSeconds ? get_option('miravia_delay_time', 300) : $customSeconds;
$notify_actual = intval(get_option('miravia_notify_' . $message . '_in', 0));
$time_lost = ($notify_actual + ($secondsNotify * 1.1));
$time_now = time();
if($notify_actual >= 0 and $time_now < $time_lost ) {
LOG::add("El notify actual está establecido y es superior a {$time_now} > {$time_lost}");
return -1;
}
LOG::add("Estableciendo info notify {$secondsNotify}");
update_option('miravia_notify_' . $message . '_in', time());
$link = new MiraviaLink($apiKey);
$result = $link->subscribe($secondsNotify, $message);
if($result) {
return true;
}else{
return false;
}
}
static function add_rule($args) {
global $wpdb;
$default = array(
'name_rule' => 'No name',
'accounts' => 0,
'profile_id' => 0,
'rules_json' => '[]',
'action_json' => '[]'
);
$args = wp_parse_args( $args, $default );
$wpdb->insert($wpdb->prefix.'miravia_rules',$args);
LOG::add($wpdb->last_error);
$my_id = $wpdb->insert_id;
return $my_id;
}
static function check_product_onjob($product) {
global $wpdb;
$check = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}miravia_products WHERE id_woocommerce = {$product}");
if($check) {
LOG::add("DEBUG: Product {$product} found in DB - Status: {$check->status_text}, Job ID: {$check->job_id}, Last Error: {$check->lastError}");
if($check->status_text == 'IN_QUEUE') {
LOG::add("DEBUG: Product {$product} is IN_QUEUE - blocking new submission");
return true;
}
LOG::add("DEBUG: Product {$product} not in queue (status: {$check->status_text}) - allowing submission");
} else {
LOG::add("DEBUG: Product {$product} not found in miravia_products table - allowing submission");
}
return false;
}
static function update_rule($args, $id) {
global $wpdb;
$default = array(
'name_rule' => 'No name',
'accounts' => 0,
'profile_id' => 0,
'rules_json' => '[]',
'action_json' => '[]'
);
$args = wp_parse_args( $args, $default );
if($id) {
$wpdb->update($wpdb->prefix.'miravia_rules',$args, $id);
return $id;
}
return false;
}
static function update_profile($args, $id = false) {
global $wpdb;
if($id) {
$wpdb->update($wpdb->prefix.'miravia_profiles',$args, $id);
}
return $id;
}
static function get_products_by_profile($profile = false, $need = false) {
if(!$profile) {
return false;
}
$profile = self::get_profiles($profile);
$args = array(
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => 'product_cat',
'field' => 'id',
'terms' => array_map('intval', explode(',', $profile['categories'])),
'operator' => 'IN',
'include_children' => false
)
),
'post_type' => 'product',
);
if($need) {
$args['meta_query'] = array(
array(
'key' => '_miravia_need_update',
'compare' => 'EXISTS'
),
array(
'key' => '_miravia_need_update',
'compare' => '!=',
'value' => '0'
)
);
}
$saved = array();
$products = new WP_Query($args);
if($products->have_posts()) {
//Preparare products
foreach($products->posts as $p) {
$prod = new MVProduct($p->ID, $profile);
$pro = $prod->getData();
if($pro) {
$saved[] = $pro;
}
}
return $saved;
}
return false;
}
static function accounts_by_profile($profile) {
global $wpdb;
$profile = self::get_profiles($profile);
if($profile) {
return $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}miravia_accounts WHERE id IN({$profile['accounts_id']})", ARRAY_A );
}
return [];
}
static function get_local_products($page = 1, $limit = 20) {
global $wpdb;
$ofset = ($limit * $page) - $limit;
$query = "SELECT p.*,
IFNULL(COUNT(pw.ID), 0) as variationsTotal,
pp.post_title as `name`
FROM {$wpdb->prefix}miravia_products AS p
LEFT JOIN {$wpdb->prefix}posts AS pp ON pp.ID = p.id_woocommerce
LEFT JOIN {$wpdb->prefix}posts AS pw ON pw.post_parent = p.id_woocommerce AND pw.post_type = 'product_variation'
GROUP BY p.ID
LIMIT {$ofset},{$limit}";
return $wpdb->get_results( $query, ARRAY_A );
}
static function get_profiles($id = false) {
global $wpdb;
if($id) {
return $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}miravia_profiles WHERE id = '{$id}'", ARRAY_A );
}
$profiles = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}miravia_profiles", ARRAY_A );
if($profiles) {
foreach($profiles as &$prof){
$prof['sync'] = self::get_products_by_profile_total($prof['categories']);
}
return $profiles;
}
return [];
}
static function get_products_by_profile_total($categories) {
global $wpdb;
if($categories == "") {
return "No categories selected";
}
$created = (array) $wpdb->get_row("SELECT COUNT(DISTINCT p.ID) AS total_created
FROM {$wpdb->prefix}posts p
INNER JOIN {$wpdb->prefix}term_relationships tr ON p.ID = tr.object_id
INNER JOIN {$wpdb->prefix}term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
INNER JOIN {$wpdb->prefix}terms t ON tt.term_id = t.term_id
WHERE tt.taxonomy = 'product_cat'
AND t.term_id IN ({$categories})");
$sync = (array) $wpdb->get_row("SELECT COUNT(DISTINCT mp.id_woocommerce) AS total_sync
FROM {$wpdb->prefix}posts p
INNER JOIN {$wpdb->prefix}term_relationships tr ON p.ID = tr.object_id
INNER JOIN {$wpdb->prefix}term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
INNER JOIN {$wpdb->prefix}terms t ON tt.term_id = t.term_id
INNER JOIN {$wpdb->prefix}miravia_products mp ON p.ID = mp.id_woocommerce AND mp.id_miravia != 0
WHERE tt.taxonomy = 'product_cat'
AND t.term_id IN ({$categories})");
// MiraviaCore::debug($created);
if($sync) {
$string = "{$sync['total_sync']} / ";
}else{
$string = "0 /";
}
if($created) {
$string .= $created['total_created'];
}else{
$string .= "0";
}
return $string;
}
static function get_profiles_by_seller($account) {
global $wpdb;
return $wpdb->get_results("SELECT
p.id
FROM
{$wpdb->prefix}miravia_profiles AS p
LEFT JOIN {$wpdb->prefix}miravia_accounts AS ma ON ma.id = p.accounts_id
WHERE
ma.userid = '{$account}'", ARRAY_A );
}
static function get_rules($id = false, $customWhere = false) {
global $wpdb;
if($id) {
return $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}miravia_rules WHERE id = '{$id}'", ARRAY_A );
}else{
if($customWhere) {
return $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}miravia_rules WHERE {$customWhere}", ARRAY_A );
}
}
return $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}miravia_rules", ARRAY_A );
}
static function get_profile_by_product($sku = false) {
global $wpdb;
if($sku) {
$_product_id = wc_get_product_id_by_sku($sku);
$categories = get_the_terms( $_product_id, 'product_cat' );
foreach($categories as $c) {
$profiles = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}miravia_profiles WHERE FIND_IN_SET('{$c->term_id}', categories) > 0", ARRAY_A );
if(is_array($profiles) and count($profiles) > 0) {
return $profiles[0]['id'];
}
}
}
return false;
}
static function get_product_miravia($id = false) {
global $wpdb;
if($id) {
return $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}miravia_products WHERE id_woocommerce = '{$id}'", ARRAY_A );
}
//Is a variation...
// $parent =
return false;
}
static function get_accounts($id = false, $field = 'id') {
global $wpdb;
if($id) {
return $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}miravia_accounts WHERE {$field} = '{$id}'", ARRAY_A );
}
return $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}miravia_accounts", ARRAY_A );
}
static function get_miravia_account_default($id = false, $field = 'id') {
global $wpdb;
if($id) {
return $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}miravia_accounts WHERE `{$field}` = '{$id}'", ARRAY_A );
}
return $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}miravia_accounts LIMIT 1", ARRAY_A );
}
static function order_exist($id) {
global $wpdb;
if($id) {
return $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}postmeta WHERE `meta_value` = '{$id}'", ARRAY_A );
}
return true;
}
static function get_product($id, $profile = 0) {
global $wpdb;
$queryProfile = "";
if($profile) {
$queryProfile = " AND profile_id = '{$profile}'";
}
return $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}miravia_products WHERE id_woocommerce = '{$id}' {$queryProfile}" );
}
static function set_job_product($id, $profile, $job = 0) {
global $wpdb;
if(is_array($id)) {
$where = "id_woocommerce IN(".implode(',', $id).")";
LOG::add("DEBUG: Setting job for multiple products: " . implode(',', $id));
}else{
$where = "id_woocommerce = '$id'";
LOG::add("DEBUG: Setting job for single product: {$id}");
}
LOG::add("DEBUG: Setting job ID {$job} for profile {$profile}");
$query = "UPDATE {$wpdb->prefix}miravia_products SET job_id='{$job}', status_text='IN_QUEUE' WHERE profile_id = {$profile} AND ". $where;
$result = $wpdb->query($query);
LOG::add("DEBUG: Set job query executed: " . $query);
LOG::add("DEBUG: Set job query affected {$result} rows");
if($result === false) {
LOG::add("DEBUG: Set job query FAILED - SQL Error: " . $wpdb->last_error);
} elseif($result == 0) {
LOG::add("DEBUG: Set job query affected 0 rows - products may not exist or already have this status");
}
}
static function set_job_product_error($id, $profile, $job = 0, $errorText = 'Generic Error') {
global $wpdb;
if(is_array($id)) {
$where = "id_woocommerce IN(".implode(',', $id).")";
}else{
$where = "id_woocommerce = '$id'";
}
$query = "UPDATE {$wpdb->prefix}miravia_products SET job_id='{$job}', lastError='{$errorText}' status_text='FAIL' WHERE profile_id = {$profile} AND ". $where;
$result = $wpdb->query($query);
LOG::add("SET JOB ON PRODUCTS -> " . $query);
LOG::add($result);
}
static function get_order_woocommerce($id) {
$order = wc_get_order($id);
$order_data = new stdClass();
$items_data = [];
$items = false;
if($order) {
$order_data->id = $id;
$order_data->total = $order->get_total();
$items = $order->get_items();
foreach($items as $item) {
array_push($items_data, wc_get_order_item_meta($item->get_id(), '_miravia_order_item_id', true));
}
}
return [$order_data, $items_data];
}
static function getCurrentRules($account_id = 0, $profile_id = 0){
$rules = self::get_rules(false, "(accounts = {$account_id} OR accounts = 0) AND (profile_id = {$profile_id} OR profile_id = '0')");
return $rules;
}
static function applyFilter($feed, $account_id, $profile_id) {
$rules = self::getCurrentRules($account_id, $profile_id);
foreach ($rules as $rule){
$jsonfilter = $rule['rules_json'];
if($jsonfilter){
if($ids = $feed->applyFilter($jsonfilter)){
switch($rule['action_type']){
case 'remove':
$feed->removeProductsById($ids);
break;
case 'only':
$feed->keepProductsById($ids);
break;
case 'price_stock':
$detail = json_decode($rule['action_json'], true);
if(is_array($detail)){
$field = $detail['field'];
$operator = $detail['operator'];
$value = $detail['value'];
$feed->applyNumericFieldAction($ids, $field, $operator, $value);
}
break;
case 'name':
$detail = json_decode($rule['action_json'], true);
if(is_array($detail)){
$field = $detail['field'];
$stringvalue = $detail['stringvalue'];
$feed->applyTextFieldAction($ids, $field, $stringvalue);
}
break;
case 'logistics':
$detail = json_decode($rule['action_json'], true);
if(is_array($detail)){
$delivery = $detail['delivery'];
$warehouse = $detail['warehouse'];
$feed->applyLogisticsAction($ids, $delivery, $warehouse);
}
break;
}
}
}
}
return $feed;
}
static function set_error_product_job($sku, $job = 0, $error = '') {
global $wpdb;
$query = "UPDATE {$wpdb->prefix}miravia_products SET lastError='{$error}', status_text='ERROR' WHERE sku = '{$sku}' AND job_id='{$job}'";
$result = $wpdb->query($query);
}
static function set_status_job($job = 0, $status = '') {
global $wpdb;
if($job) {
$query = "UPDATE {$wpdb->prefix}miravia_products SET status_text='{$status}' WHERE job_id='{$job}'";
$result = $wpdb->query($query);
}
}
static function clear_job($job = 0) {
global $wpdb;
if($job) {
$query = "UPDATE {$wpdb->prefix}miravia_products SET status_text='ND', job_id='', lastError='' WHERE job_id='{$job}'";
$result = $wpdb->query($query);
}
}
static function disconnect_product($id) {
global $wpdb;
$query = "UPDATE {$wpdb->prefix}miravia_products SET status_text='Disconected', lastError='', last_updated='NOW()', id_miravia='' WHERE id_woocommerce = '{$id}'";
$wpdb->query($query);
}
static function set_id_miravia_product_job($sku, $job = 0, $id = '') {
global $wpdb;
if($id === false) {
$query = "UPDATE {$wpdb->prefix}miravia_products SET status_text='DONE', lastError='', last_updated='NOW()' WHERE sku = '{$sku}' AND job_id='{$job}'";
}else{
$query = "UPDATE {$wpdb->prefix}miravia_products SET id_miravia='{$id}',status_text='DONE', lastError='', last_updated='NOW()' WHERE sku = '{$sku}' AND job_id='{$job}'";
}
$result = $wpdb->query($query);
}
static function get_miravia_category($product, $profile) {
$cat = $product->get_category_ids();
$category_profile_search = explode(',', $profile['categories']);
foreach($cat as $c) {
if(in_array($c, $category_profile_search)) {
return $c;
}
}
return 0;
}
static function get_attributes(WC_Product $_product, $attrs) {
$attrs = $_product->get_attributes();
// (
// [color] => WC_Product_Attribute Object
// (
// [data:protected] => Array
// (
// [id] => 0
// [name] => Color
// [options] => Array
// (
// [0] => Rojo
// )
// [position] => 0
// [visible] => 1
// [variation] =>
// )
// )
// )
// var_dump($attrs);
// die();
$attributes = [];
foreach($attrs as $key => $v) {
if(count($v['options']) == 1) {
$value = $v['options'][0];
}else{
$value = $v['options'];
}
$attributes[self::get_key_miravia_attr($key, $attrs)] = $value;
}
return $attributes;
// $map_attrs = get_term_meta($_product->get_category_ids(), "_miravia_attr", true);
}
static function get_key_miravia_attr($key, $attrs) {
if(isset($attrs->attr)) {
$attrs = $attrs->attr;
}
foreach($attrs as $k => $v) {
// MiraviaCore::debug(array($key, $attrs, $k, 'attribute_pa_'.$k == $key));
if($k == $key or 'pa_'.$k == $key or 'attribute_pa_'.$k == $key) {
return $v;
}
}
return false;
}
static function select_category($categories, $args = array('select' => '0', 'name' => 'category')) {
$html = "<select name='{$args['name']}'>";
$html .= "<option value='0'>Seleccione</option>";
foreach($categories as $c) {
// die(var_dump($c['children']));
if(isset($c['children'])) {
$html .= self::get_children_category_select($c, $args);
}else{
$selected_html = "";
if($args['select'] == $c['category_id']) { $selected_html = "selected='selected'"; }
$html .= "<option {$selected_html} value='{$c['category_id']}'>{$c['name']}</option>";
}
}
$html .= "</select>";
return esc_html($html);
}
static function get_children_category_select($categories, $args = array('select' => '0', 'name' => 'category')) {
// die(var_dump($categories));
$html = "<optgroup label='{$categories['name']}'>";
foreach($categories['children'] as $c) {
// die(var_dump($c));
if(isset($c['children'])) {
$html .= self::get_children_category_select($c, $args);
}else{
$selected_html = "";
if($args['select'] == $c['category_id']) { $selected_html = "selected='selected'"; }
$html .= "<option {$selected_html} value='{$c['category_id']}'>{$c['name']}</option>";
}
}
$html .= "</optgroup>";
return esc_html($html);
}
static function get_jobs() {
global $wpdb;
$query = "SELECT
DISTINCT(p.job_id),
COUNT(DISTINCT(p.id_woocommerce)) as total,
ac.token,
f.id,
p.updated
FROM {$wpdb->prefix}miravia_products AS p
INNER JOIN {$wpdb->prefix}miravia_profiles AS f ON p.profile_id = f.id
INNER JOIN {$wpdb->prefix}miravia_accounts AS ac ON f.accounts_id = ac.id
WHERE
p.job_id != '0' and p.job_id != ''
GROUP BY p.job_id, ac.token, f.id";
return $wpdb->get_results( $query, ARRAY_A );
}
static function get_job_detail($id = false) {
global $wpdb;
if(!$id) {
return [];
}
$query = "SELECT
p.*
FROM {$wpdb->prefix}miravia_products AS p
INNER JOIN {$wpdb->prefix}miravia_profiles AS f ON p.profile_id = f.id
INNER JOIN {$wpdb->prefix}miravia_accounts AS ac ON f.accounts_id = ac.id
WHERE
p.job_id = '{$id}'";
return $wpdb->get_results( $query, ARRAY_A );
}
static function resync_stock() {
global $wpdb;
//Actualizar todos los stocks como que necesitan actualizar y responder con un array de tokens de cuentas
$wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->prefix}postmeta AS pm1
INNER JOIN {$wpdb->prefix}miravia_products AS pm2
ON pm1.post_id = pm2.id_woocommerce
SET pm1.meta_value = %s
WHERE pm1.meta_key = %s",
'1',
'_miravia_need_update'
)
);
return MiraviaCore::get_accounts();
}
static function procesarFeed() {
$document = sanitize_text_field($_REQUEST['document']);
$seller = sanitize_text_field($_REQUEST['seller']);
$account = MiraviaCore::get_accounts($seller,'userid');
if(!$account) {
LOG::add("Accont not found on process feed {$document}", false, 'feeds');
return false;
}
$id_seller = $account['id'];
if($id_seller) {
$link = new MiraviaLink($account['token']);
$data = $link->getFeedResult($document);
if (!empty($data)) {
$info = json_decode($data,true);
if(is_array($info) && isset($info['success']) && $info['success'] == false){
$ret['success'] = false;
$ret['error'] = $info['error'];
$txlog .= $info['error'];
}else {
$r = self::processFeedResult($seller, $data);
if ($r != 'ok') {
LOG::add("Error when process feed {$document}: {$r}", false, 'feeds');
return false;
}
}
}
}else{
LOG::add("ID Accont not found on process feed {$document}", false, 'feeds');
return false;
}
}
static public function processFeedResult($seller, $data){
if(!is_array($data)){
$data = json_decode($data, true);
}
$account = MiraviaCore::get_accounts($seller,'userid');
if(!$account){
return 'error: invalid seller';
}
if(!is_array($data)){
return 'error: invalid data';
}
foreach($data as $id => $info){
global $wpdb;
$status = isset($info['status']) ? $info['status'] : false;
$message = '?';
$item_id = '';
if($status){
$message='';
if($status=='FAIL'){
$status = 'error';
$detail = isset($info['detail']) ? $info['detail'] : false;
$message = self::getFeedDetail($detail);
}elseif($status=='SUCCESS'){
$status = 'created';
$item_id = $info['id'];
}
}else{
$status = 'error';
$message = 'Invalid response from server: ' . json_encode($info);
}
$update_fields = [
'status_text' => $status,
'lastError' => $message,
'job_id' => '',
];
if(!empty($item_id)){
$update_fields['id_miravia'] = $item_id;
}
$wpdb->update($wpdb->prefix.'miravia_products', $update_fields, array('sku' => $id,'job_id' => $_GET['feed']));
}
return 'ok';
}
static protected function getFeedDetail($tx)
{
if(is_array($tx)){
$data = $tx;
}else {
$data = json_decode($tx, true);
}
$msg = isset($data['message']) ? $data['message'] : false;
if($msg){
$data = is_array($msg) ? $msg : json_decode($msg, true);
$errorCode = isset($data['errorCode']) ? $data['errorCode'] : '0';
$errorMsg = isset($data['errorMsg']) ? $data['errorMsg'] : '0';
$errorFirst = isset($data['errors'][0]['code']) ? $data['errors'][0]['code'] : '0';
$detail = "[$errorCode] $errorMsg ($errorFirst)";
return $detail;
}
$detail = $tx;
return $detail;
}
}
}