HEX
Server: Apache
System: Linux vpshost11508.publiccloud.com.br 5.15.179-grsec-vpshost-10.lc.el8.x86_64 #1 SMP Mon Apr 7 12:04:45 -03 2025 x86_64
User: wicomm2 (10002)
PHP: 8.3.0
Disabled: apache_child_terminate,dl,escapeshellarg,escapeshellcmd,exec,link,mail,openlog,passthru,pcntl_alarm,pcntl_exec,pcntl_fork,pcntl_get_last_error,pcntl_getpriority,pcntl_setpriority,pcntl_signal,pcntl_signal_dispatch,pcntl_sigprocmask,pcntl_sigtimedwait,pcntl_sigwaitinfo,pcntl_strerror,pcntl_wait,pcntl_waitpid,pcntl_wexitstatus,pcntl_wifexited,pcntl_wifsignaled,pcntl_wifstopped,pcntl_wstopsig,pcntl_wtermsig,php_check_syntax,php_strip_whitespace,popen,proc_close,proc_open,shell_exec,symlink,system
Upload Files
File: /home/storage/5/78/dd/wicomm2/public_html/clientes/flavioscalcados/latest-products-cron.php
<?php
/**
 * add_low_stock_skus_to_subcollection_cron.php
 *
 * CLI-only para execução via CRON.
 * - Processa SKUs em janelas lógicas (TOTAL_TARGET e CHUNK).
 * - Pausa configurável entre páginas (wait-seconds, padrão 20s).
 * - Logs em STDOUT (texto) e opcionalmente em arquivo (JSON).
 * - Inclusão na Subcollection com body { "SkuId": <id> }.
 *
 * Parâmetros (somente CLI):
 *   --account, --env (vtexcommercestable|myvtex), --appKey, --appToken
 *   --subcollection (int), --threshold (int)
 *   --pageSize / pagesize / target / total / opagesize    (TOTAL alvo)
 *   --limit / chunk / perPage / per_page                  (lote por página, máx 1000)
 *   --wait-seconds / wait / pause                         (pausa entre páginas)
 *   --dry-run (true/false), --silent (true/false)
 *   --log-file (path), --log-max-size (bytes, default 5MB)
 */

if (PHP_SAPI !== 'cli') {
    fwrite(STDERR, "Este script é CLI-only (use via cron/terminal).\n");
    exit(2);
}
date_default_timezone_set('America/Sao_Paulo');

/* ========= Leitura de argumentos ========= */
$args = getopt('', [
    'account::','env::','appKey::','appToken::',
    'subcollection::','threshold::',
    'pageSize::','pagesize::','target::','total::','opagesize::',
    'limit::','chunk::','perPage::','per_page::',
    'wait-seconds::','wait::','pause::',
    'dry-run::','silent::','log-file::','log-max-size::'
]);

function argInt($arr, $keys, $default) {
    foreach ($keys as $k) if (isset($arr[$k]) && $arr[$k] !== '') return (int)$arr[$k];
    return (int)$default;
}
function argBool($arr, $key, $default=false) {
    if (!isset($arr[$key])) return (bool)$default;
    return in_array(strtolower((string)$arr[$key]), ['1','true','on','yes'], true);
}

/* ========= Config ========= */
$ACCOUNT        = $args['account']  ?? getenv('VTEX_ACCOUNT') ?? 'seuaccount';
$ENV            = $args['env']      ?? getenv('VTEX_ENV')     ?? 'vtexcommercestable';
$APP_KEY        = $args['appKey']   ?? getenv('VTEX_APP_KEY') ?? '';
$APP_TOKEN      = $args['appToken'] ?? getenv('VTEX_APP_TOKEN') ?? '';
$SUBCOLLECTION  = (int)($args['subcollection'] ?? 137);
$THRESHOLD      = (int)($args['threshold']    ?? 5);

$TOTAL_TARGET   = argInt($args, ['pageSize','pagesize','target','total','opagesize'], 1000);
$CHUNK          = argInt($args, ['limit','chunk','perPage','per_page'], 1000);
$CHUNK          = max(1, min($CHUNK, 1000)); // cap 1000

$WAIT_SECONDS   = argInt($args, ['wait-seconds','wait','pause'], 20);
$DRY_RUN        = argBool($args, 'dry-run', false);
$SILENT         = argBool($args, 'silent', false);
$LOG_FILE       = $args['log-file'] ?? '';
$LOG_MAX_SIZE   = (int)($args['log-max-size'] ?? (5 * 1024 * 1024));

$HOST           = "{$ACCOUNT}.{$ENV}.com.br";
$LOG_PREFIX     = "[low-stock->subcollection{$SUBCOLLECTION}]";

/* ========= Logging ========= */
function log_json_file($level, $msg, $context=[]) {
    global $LOG_FILE, $LOG_MAX_SIZE;
    if (!$LOG_FILE) return;
    $entry = ['ts'=>date('c'),'level'=>strtoupper($level),'message'=>$msg,'context'=>$context];
    $line = json_encode($entry, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES).PHP_EOL;
    if (@file_exists($LOG_FILE) && @filesize($LOG_FILE) > $LOG_MAX_SIZE) {
        @rename($LOG_FILE, $LOG_FILE.'.1'); // rotação simples
    }
    @file_put_contents($LOG_FILE, $line, FILE_APPEND);
}
function out($msg, $level='INFO', $context=[]) {
    global $SILENT;
    $prefix = sprintf("[%s] %s", date('H:i:s'), $level);
    if (!$SILENT) echo "{$prefix} - {$msg}\n";
    log_json_file($level, $msg, $context);
}

/* ========= HTTP base com retry ========= */
function vtexHeaders($appKey, $appToken, $extra=[]) {
    return array_merge([
        'X-VTEX-API-AppKey'   => $appKey,
        'X-VTEX-API-AppToken' => $appToken,
        'Accept'              => 'application/json',
    ], $extra);
}
function vtexRequest($method, $url, $headers=[], $body=null, $retry=3, $sleepBaseMs=400) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER         => true,
        CURLOPT_TIMEOUT        => 60,
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, is_string($body) ? $body : json_encode($body));
        if (!isset($headers['Content-Type'])) $headers['Content-Type'] = 'application/json';
    }
    $h = [];
    foreach ($headers as $k=>$v) $h[] = $k.': '.$v;
    curl_setopt($ch, CURLOPT_HTTPHEADER, $h);

    $attempt=0;
    do {
        $attempt++;
        $resp = curl_exec($ch);
        if ($resp === false) {
            $err = curl_error($ch);
            if ($attempt <= $retry) {
                out("cURL error, retrying: {$err}", 'WARN', ['attempt'=>$attempt,'url'=>$url]);
                usleep($sleepBaseMs*1000*$attempt);
                continue;
            }
            throw new RuntimeException("cURL error: {$err}");
        }
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $status     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $bodyStr    = substr($resp, $headerSize);

        if (in_array($status, [429,503], true) && $attempt <= $retry) {
            out("HTTP {$status} — retrying", 'WARN', ['attempt'=>$attempt,'url'=>$url]);
            $waitMs = $sleepBaseMs*$attempt + rand(0,250);
            usleep($waitMs*1000);
            continue;
        }
        return [$status, $bodyStr];
    } while ($attempt <= $retry);

    throw new RuntimeException("Unexpected HTTP flow");
}

/* ========= VTEX helpers ========= */
// Busca janela de SKUs cruzando páginas reais (pagesize real = 1000)
function fetchSkuWindow($host, $appKey, $appToken, $startIndex, $length) {
    $result = [];
    $maxPerPage = 1000;
    $remaining = $length;
    $cursor = $startIndex;

    while ($remaining > 0) {
        $apiPage = (int)floor($cursor / $maxPerPage) + 1; // 1-based
        $offset  = $cursor % $maxPerPage;

        $url = "https://{$host}/api/catalog_system/pvt/sku/stockkeepingunitids?page={$apiPage}&pagesize={$maxPerPage}";
        list($status, $body) = vtexRequest('GET', $url, vtexHeaders($appKey, $appToken));
        if ($status !== 200) throw new RuntimeException("List SKU IDs falhou (HTTP {$status}) body={$body}");

        $data = json_decode($body, true);
        if (!is_array($data) || empty($data)) break;

        $take = min($remaining, max(0, count($data) - $offset));
        if ($take <= 0) { $cursor = $apiPage * $maxPerPage; continue; }

        $slice = array_slice($data, $offset, $take);
        $result = array_merge($result, $slice);

        $remaining -= $take;
        $cursor    += $take;
    }
    return $result;
}

function getAvailableStockForSku($host, $appKey, $appToken, $skuId) {
    $url = "https://{$host}/api/logistics/pvt/inventory/skus/{$skuId}";
    list($status, $body) = vtexRequest('GET', $url, vtexHeaders($appKey, $appToken));
    if ($status === 404) return 0;
    if ($status !== 200) throw new RuntimeException("Inventory lookup failed for SKU {$skuId} (HTTP {$status}) body={$body}");
    $data = json_decode($body, true);

    $warehouses = [];
    if (isset($data['balance']))                  $warehouses = $data['balance'];
    elseif (isset($data['balances']))             $warehouses = $data['balances'];
    elseif (isset($data['warehouses']))           $warehouses = $data['warehouses'];
    elseif (isset($data['inventoryByWarehouse'])) $warehouses = $data['inventoryByWarehouse'];
    elseif (isset($data['items']))                $warehouses = $data['items'];

    $available = 0;
    foreach ($warehouses as $w) {
        $total    = $w['totalQuantity']    ?? ($w['quantity'] ?? 0);
        $reserved = $w['reservedQuantity'] ?? 0;
        $available += max($total - $reserved, 0);
    }
    return $available;
}

function addSkuToSubcollection($host, $appKey, $appToken, $subcollectionId, $skuId) {
    // rota principal: body JSON {SkuId}
    $url1 = "https://{$host}/api/catalog/pvt/subcollection/{$subcollectionId}/stockkeepingunit";
    $headers = vtexHeaders($appKey, $appToken, ['Content-Type'=>'application/json']);
    $body    = ['SkuId' => (int)$skuId];

    out("POST add SKU -> subcollection", 'INFO', ['sku'=>$skuId,'subcollection'=>$subcollectionId,'endpoint'=>$url1,'body'=>$body]);
    list($status1, $resp1) = vtexRequest('POST', $url1, $headers, json_encode($body));

    if (($status1 >= 200 && $status1 < 300) || in_array($status1, [409,422], true)) return true;

    // fallback por path
    if (in_array($status1, [404,405], true)) {
        $url2 = "https://{$host}/api/catalog/pvt/subcollection/{$subcollectionId}/sku/{$skuId}";
        out("POST fallback path", 'WARN', ['sku'=>$skuId,'endpoint'=>$url2]);
        list($status2, $resp2) = vtexRequest('POST', $url2, vtexHeaders($appKey, $appToken));
        if (($status2 >= 200 && $status2 < 300) || in_array($status2, [409,422], true)) return true;
        throw new RuntimeException("Fallback falhou ({$status2}) body={$resp2}");
    }
    throw new RuntimeException("Add SKU body JSON falhou ({$status1}) body={$resp1}");
}

/* ========= Processar 1 página lógica ========= */
function processLogicalPage($host, $appKey, $appToken, $subcollectionId, $threshold, $logicalPage, $chunk, $totalTarget, $dryRun) {
    $startIndex = ($logicalPage - 1) * $chunk;
    if ($startIndex >= $totalTarget) {
        out("Página lógica {$logicalPage} fora do alvo total", 'WARN');
        return ['checked'=>0,'added'=>0];
    }
    $length = min($chunk, $totalTarget - $startIndex);
    out("Janela lógica {$logicalPage}: start={$startIndex}, len={$length}");

    $ids = fetchSkuWindow($host, $appKey, $appToken, $startIndex, $length);
    if (empty($ids)) { out("Janela vazia", 'WARN'); return ['checked'=>0,'added'=>0]; }

    $checked=0; $added=0;
    foreach ($ids as $skuId) {
        $checked++;
        try {
            $available = getAvailableStockForSku($host, $appKey, $appToken, $skuId);
        } catch (Throwable $e) {
            out("Falha estoque SKU {$skuId}: ".$e->getMessage(), 'WARN', ['sku'=>$skuId]);
            usleep(20000);
            continue;
        }

        if ($available < $threshold && $available > 0) {
            if ($dryRun) {
                out("[DRY-RUN] Adicionaria SKU {$skuId} (disp={$available})", 'INFO', ['sku'=>$skuId]);
            } else {
                try {
                    addSkuToSubcollection($host, $appKey, $appToken, $subcollectionId, $skuId);
                    $added++;
                    out("SKU {$skuId} adicionado (disp={$available})", 'INFO');
                } catch (Throwable $e) {
                    out("Erro add SKU {$skuId}: ".$e->getMessage(), 'ERROR', ['sku'=>$skuId]);
                }
            }
        }
        usleep(25000); // reduzir 429/503
    }
    return ['checked'=>$checked,'added'=>$added];
}

/* ========= Execução principal ========= */
try {
    $totalPages = (int)ceil(max(1, $TOTAL_TARGET) / $CHUNK);
    out("Start {$LOG_PREFIX}", 'INFO', [
        'account'=>$ACCOUNT,'env'=>$ENV,'subcollection'=>$SUBCOLLECTION,'threshold'=>$THRESHOLD,
        'total_target'=>$TOTAL_TARGET,'chunk_limit'=>$CHUNK,'total_pages'=>$totalPages,'dry_run'=>$DRY_RUN
    ]);

    $sumChecked=0; $sumAdded=0;
    for ($page=1; $page <= $totalPages; $page++) {
        out("== Página {$page}/{$totalPages} ==");
        $res = processLogicalPage($HOST, $APP_KEY, $APP_TOKEN, $SUBCOLLECTION, $THRESHOLD, $page, $CHUNK, $TOTAL_TARGET, $DRY_RUN);
        $sumChecked += $res['checked'];
        $sumAdded   += $res['added'];
        out("Página {$page} concluída: verificados+={$res['checked']}, adicionados+={$res['added']}", 'INFO');

        if ($page < $totalPages) {
            out("Aguardando {$WAIT_SECONDS}s antes da próxima página...", 'INFO');
            sleep($WAIT_SECONDS);
        }
    }
    out("Concluído. Verificados={$sumChecked}, adicionados={$sumAdded}.", 'INFO');
    exit(0);

} catch (Throwable $e) {
    out("FATAL: ".$e->getMessage(), 'ERROR');
    exit(1);
}