/** * Optimized BTS Stock & Price Sync * * CHANGES FROM ORIGINAL: * 1. Memory management: Cache flush and garbage collection every 500 products * 2. Chunking: Process products in batches instead of all at once * 3. Optimized queries: Single JOIN query to fetch all meta data at once * 4. Verbose logging: Memory usage and detailed progress reporting * 5. Configurable chunk size via --chunk-size parameter * * @param bool $dry_run * @param bool $safe_mode * @param bool $zero_missing * @param int $chunk_size Number of products to process per chunk (default: 500) * @return array */ function master_sync_bts_optimized($dry_run = false, $safe_mode = false, $zero_missing = false, $chunk_size = 500) { WP_CLI::log(''); WP_CLI::log('📦 BTS WHOLESALER SYNC (OPTIMIZED)'); WP_CLI::log('================================='); $start_time = microtime(true); $start_memory = memory_get_usage(true); try { require_once __DIR__ . '/../api/class-parfumselect-bts-api.php'; $bts_api = new Parfumselect_BTS_API(); // STEP 1: Fetch API products WP_CLI::log('📥 Fetching products from BTS API...'); $api_products = $bts_api->getProducts(); if (empty($api_products)) { throw new Exception("Geen producten ontvangen van BTS API"); } WP_CLI::log('✅ Received ' . number_format(count($api_products)) . ' products from BTS API'); // STEP 2: Build EAN map WP_CLI::log('🗂️ Building EAN map...'); $ean_map = []; foreach ($api_products as $product) { $ean = trim($product['ean'] ?? ''); if (!empty($ean)) { $ean_map[$ean] = $product; } } WP_CLI::log('✅ EAN map built with ' . number_format(count($ean_map)) . ' entries'); // Free up memory from API products array unset($api_products); gc_collect_cycles(); // STEP 3: Fetch products with EAN using optimized JOIN query WP_CLI::log('🔍 Fetching local products with EAN...'); global $wpdb; $local_products = $wpdb->get_results( "SELECT DISTINCT p.ID as post_id, ean.meta_value as ean, bts_stock.meta_value as current_bts_stock, bts_cost.meta_value as current_bts_cost FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} ean ON ( p.ID = ean.post_id AND ean.meta_key = '_global_unique_id' AND ean.meta_value != '' ) LEFT JOIN {$wpdb->postmeta} bts_stock ON ( p.ID = bts_stock.post_id AND bts_stock.meta_key = '_supplier_bts_stock' ) LEFT JOIN {$wpdb->postmeta} bts_cost ON ( p.ID = bts_cost.post_id AND bts_cost.meta_key = '_supplier_bts_cost' ) WHERE p.post_type = 'product' AND p.post_status = 'publish'", OBJECT_K ); WP_CLI::log('✅ Found ' . number_format(count($local_products)) . ' local products with EAN'); $results = [ 'updated' => 0, 'skipped' => 0, 'unchanged' => 0, ]; // STEP 4: Process in chunks WP_CLI::log(''); WP_CLI::log('🔄 Processing products in chunks of ' . number_format($chunk_size)); WP_CLI::log(' Memory limit: ' . ini_get('memory_limit')); WP_CLI::log(' Start memory: ' . size_format(memory_get_usage(true))); WP_CLI::log(''); $product_ids = array_keys($local_products); $total_products = count($product_ids); $chunks = array_chunk($product_ids, $chunk_size); $total_chunks = count($chunks); foreach ($chunks as $chunk_index => $chunk_product_ids) { $chunk_num = $chunk_index + 1; $chunk_start = $chunk_index * $chunk_size + 1; $chunk_end = min($chunk_num * $chunk_size, $total_products); WP_CLI::log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); WP_CLI::log("📦 Processing Chunk {$chunk_num}/{$total_chunks} (products {$chunk_start}-{$chunk_end})"); WP_CLI::log(" Memory before: " . size_format(memory_get_usage(true))); $chunk_updated = 0; $chunk_unchanged = 0; $chunk_skipped = 0; foreach ($chunk_product_ids as $product_id) { if (!isset($local_products[$product_id])) { $chunk_skipped++; continue; } $product_data = $local_products[$product_id]; $ean = trim($product_data->ean); if (empty($ean) || !isset($ean_map[$ean])) { $chunk_skipped++; continue; } $api_product = $ean_map[$ean]; // Clean price string (e.g., "13.31€" -> 13.31) $price_string = $api_product['price'] ?? '0'; $cleaned_string = preg_replace('/[^0-9,.]/', '', $price_string); $cleaned_string = str_replace(',', '.', $cleaned_string); $new_cost = (float) $cleaned_string; $new_stock = (int) ($api_product['stock'] ?? 0); $current_bts_stock = (int) ($product_data->current_bts_stock ?? 0); $current_bts_cost = (float) ($product_data->current_bts_cost ?? 0); if ($current_bts_stock !== $new_stock || $current_bts_cost !== $new_cost) { if (!$dry_run) { update_post_meta($product_id, '_supplier_bts_stock', $new_stock); update_post_meta($product_id, '_supplier_bts_cost', $new_cost); update_post_meta($product_id, '_supplier_bts_id', $ean); if (function_exists('calculate_best_supplier')) { calculate_best_supplier($product_id, $safe_mode); } wc_delete_product_transients($product_id); } $chunk_updated++; } else { $chunk_unchanged++; } } $results['updated'] += $chunk_updated; $results['unchanged'] += $chunk_unchanged; $results['skipped'] += $chunk_skipped; // MEMORY MANAGEMENT: Clear cache and run garbage collection wp_cache_flush(); if (function_exists('gc_collect_cycles')) { gc_collect_cycles(); } $peak_memory = memory_get_usage(true); WP_CLI::log(" Updated: {$chunk_updated} | Unchanged: {$chunk_unchanged} | Skipped: {$chunk_skipped}"); WP_CLI::log(" Memory after: " . size_format($peak_memory)); WP_CLI::log(" Peak memory: " . size_format(memory_get_peak_usage(true))); } WP_CLI::log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); WP_CLI::log(''); // STEP 5: Zero missing products (if enabled) if ($zero_missing && !$dry_run) { WP_CLI::log('🔍 Controleren op ontbrekende producten in BTS API...'); $zeroed_count = 0; // Get all products with BTS ID (not just those with EAN) $bts_products = $wpdb->get_results( "SELECT p.ID as post_id, bts_id.meta_value as bts_id, bts_stock.meta_value as current_stock FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} bts_id ON (p.ID = bts_id.post_id AND bts_id.meta_key = '_supplier_bts_id' AND bts_id.meta_value != '') LEFT JOIN {$wpdb->postmeta} bts_stock ON (p.ID = bts_stock.post_id AND bts_stock.meta_key = '_supplier_bts_stock') WHERE p.post_type = 'product' AND p.post_status = 'publish'", OBJECT_K ); WP_CLI::log(' Found ' . count($bts_products) . ' products with BTS ID'); foreach ($bts_products as $product_data) { $ean = trim($product_data->bts_id); // BTS ID is the EAN // Check if this EAN is NOT in API if (!isset($ean_map[$ean])) { $product_id = $product_data->post_id; $current_bts_stock = (int) ($product_data->current_stock ?? 0); // Only zero out if current stock > 0 (avoid unnecessary updates) if ($current_bts_stock > 0) { // Update BTS supplier stock to 0 update_post_meta($product_id, '_supplier_bts_stock', 0); // Use calculate_best_supplier to handle full price/stock recalculation if (function_exists('calculate_best_supplier')) { // Always use save() for zero-missing (safe mode) calculate_best_supplier($product_id, true); } wc_delete_product_transients($product_id); $zeroed_count++; } } } if ($zeroed_count > 0) { WP_CLI::log("⚠️ {$zeroed_count} producten niet gevonden in API → Supplier stock op 0 gezet"); } else { WP_CLI::log("✅ Alle producten gevonden in API"); } // Add to results $results['zeroed_missing'] = $zeroed_count; // Memory cleanup wp_cache_flush(); if (function_exists('gc_collect_cycles')) { gc_collect_cycles(); } } $duration = round(microtime(true) - $start_time, 2); $end_memory = memory_get_usage(true); $memory_used = $end_memory - $start_memory; WP_CLI::log(''); WP_CLI::log("✅ BTS sync voltooid in {$duration}s"); WP_CLI::log('📊 Memory used: ' . size_format($memory_used)); WP_CLI::log('📊 Peak memory: ' . size_format(memory_get_peak_usage(true))); $log_message = " Bijgewerkt: {$results['updated']} | Ongewijzigd: {$results['unchanged']} | Overgeslagen: {$results['skipped']}"; if ($zero_missing && isset($results['zeroed_missing'])) { $log_message .= " | Nieuw op 0: {$results['zeroed_missing']}"; } WP_CLI::log($log_message); return [ 'success' => true, 'duration' => $duration, 'results' => $results, 'memory_used' => $memory_used, 'peak_memory' => memory_get_peak_usage(true), ]; } catch (Exception $e) { WP_CLI::warning("⚠️ BTS sync mislukt: " . $e->getMessage()); return [ 'success' => false, 'error' => $e->getMessage(), 'results' => [ 'updated' => 0, 'skipped' => 0, 'unchanged' => 0, 'zeroed_missing' => 0, ] ]; } } Manic Panic After Midnight Haarkleur | Parfumselect
Gratis verzending vanaf €80,-
12.000+ tevreden klanten
Bekend van TikTok

Manic Panic After Midnight Haarkleur | Diep Blauw Zwart | Semi-Permanent | 118ml | Mysterieus

Uitverkocht

3 verkocht afgelopen week
€4,95 verzending, boven de €80,- gratis verzending.
Snel & veilig betalen met IDEAL, Creditcard, Bancontact & nog veel meer.
Gegarandeerd veilig betalen
Gratis verzending vanaf €80,-
12.000+ tevreden klanten
Specialist in luxe merken

Voordelen

  • Intens diepblauwe kleur voor een dramatische look.
  • Semi-permanente formule die zacht is voor je haar.
  • Romige textuur voor eenvoudige en gelijkmatige applicatie.
  • Geeft een schitterende glans en laat het haar zijdezacht aanvoelen.
  • Perfect voor creatieve en unieke haarstijlen.

Beschrijving

Creëer een mysterieuze look met Manic Panic CLASSIC After Midnight

Op zoek naar een diepblauwe, mysterieuze gloed voor je haar? Ontdek Manic Panic CLASSIC After Midnight. Deze iconische, semi-permanente haarkleur is de ultieme keuze om je haar te transformeren met een intense, donkerblauwe tint die gegarandeerd hoofden doet omdraaien. Laat je persoonlijkheid spreken en geef je look een vleugje mysterie en elegantie. Met 118 ml van deze levendige, romige formule breng je moeiteloos een verbluffende kleur aan die je haar zacht en verzorgd achterlaat. Ideaal voor wie durft te experimenteren met een gedurfde en opvallende stijl.

De voordelen van Manic Panic After Midnight

  • Diepblauwe, mysterieuze tint voor een gedurfde uitstraling.
  • Semi-permanente formule die langdurig mooi blijft.
  • Verzorgende eigenschappen die het haar zacht en glanzend maken.
  • Eenvoudig aan te brengen dankzij de romige textuur.
  • Veganistisch en dierproefvrij voor een bewuste haarverzorging.

Een vleugje magie voor je haar

Manic Panic CLASSIC After Midnight is perfect voor het creëren van unieke looks. Of je nu gaat voor een all-over kleur, highlights, of een subtiele dip-dye, deze blauwe tint voegt een vleugje magie toe. De 118 ml tube is ontworpen voor optimaal gebruiksgemak en consistente resultaten.

Specificaties en details

Merk: Manic Panic
Lijn: CLASSIC
Kleur: After Midnight (Donkerblauw)
Inhoud: 118 ml
Type: Semi-permanente haarkleur

Waarom kiezen voor Manic Panic CLASSIC After Midnight

Kies voor Manic Panic CLASSIC After Midnight als je op zoek bent naar een intense, langdurige donkerblauwe kleur met een verzorgende werking. De betrouwbare kwaliteit van Manic Panic garandeert een spectaculair resultaat dat je look compleet maakt.

Bestel nu Manic Panic CLASSIC After Midnight en omarm de nachtelijke charme!

Productinformatie

SKU202179
EAN0612600110012
merknaamManic panic
genderUniseks
inhoud118 ml
lijn841518
variantClassic
Producttypehaarkleuring

Klant Beoordelingen

Nog geen reviews.

Viral op TikTok

Meestgestelde vragen over Manic Panic CLASSIC #AfterMidnight 118 ml

Wat is de levertijd?
De standaard levertijd voor dit prodct bedraagt 3 tot 5 werkdagen na het plaatsen van je bestelling. We doen ons best om je zo snel mogelijk van dienst te zijn.
De verzendkosten bedragen €4,95 vanaf €22,50. Bij bestellingen boven de €80,- ontvang je van gratis verzending binnen Nederland en België.
Wij leveren in zowel Nederland als België.
Bij het afrekenen hebben we een optie waar je jouw couponscode kan toeveoegen en je bestelling kan plaatsen.
Ontvang coupons en persoonlijke aanbiedingen door je aan te melden voor onze nieuwsbrief. Je kunt je op ieder moment weer uitschrijven via de link in de mail.
Je kunt bij ons betalen met iDeal, Creditcard, Bancontact, Apple Pay, Google Pay & Klarna.
Jouw bestelling
7.44%
Nog  74,05 tot GRATIS VERZENDING
Free Shipping Bar Attributes