first commit
This commit is contained in:
commit
29c27dae04
|
|
@ -0,0 +1,28 @@
|
||||||
|
jQuery(function ($) {
|
||||||
|
$("#import_api").on("click", function () {
|
||||||
|
$.post(
|
||||||
|
ajaxurl,
|
||||||
|
{
|
||||||
|
action: "wpi_import_api",
|
||||||
|
url: $("#api_url").val(),
|
||||||
|
},
|
||||||
|
alert
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#import_csv").on("click", function () {
|
||||||
|
const file = $("#csv_file")[0].files[0];
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("action", "wpi_import_csv");
|
||||||
|
form.append("file", file);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: "POST",
|
||||||
|
data: form,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
success: alert,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class WPI_Admin_Page
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
add_action('admin_menu', [$this, 'menu']);
|
||||||
|
add_action('wp_ajax_wpi_import_api', [$this, 'import_api']);
|
||||||
|
add_action('wp_ajax_wpi_import_csv', [$this, 'import_csv']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function menu()
|
||||||
|
{
|
||||||
|
add_menu_page(
|
||||||
|
'Product Importer',
|
||||||
|
'Product Importer',
|
||||||
|
'manage_woocommerce',
|
||||||
|
'wpi-importer',
|
||||||
|
[$this, 'render']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
?>
|
||||||
|
<h1>Woo Product Importer</h1>
|
||||||
|
|
||||||
|
<h2>Import from API</h2>
|
||||||
|
<input type="text" id="api_url" placeholder="API URL">
|
||||||
|
<button id="import_api">Import API</button>
|
||||||
|
|
||||||
|
<h2>Import from CSV</h2>
|
||||||
|
<input type="file" id="csv_file">
|
||||||
|
<button id="import_csv">Import CSV</button>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import_api()
|
||||||
|
{
|
||||||
|
$url = sanitize_text_field($_POST['url']);
|
||||||
|
$importer = new WPI_API_Importer();
|
||||||
|
$result = $importer->import($url);
|
||||||
|
wp_send_json_success($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import_csv()
|
||||||
|
{
|
||||||
|
if (empty($_FILES['file'])) {
|
||||||
|
wp_send_json_error('No file');
|
||||||
|
}
|
||||||
|
$importer = new WPI_CSV_Importer();
|
||||||
|
$result = $importer->import($_FILES['file']['tmp_name']);
|
||||||
|
wp_send_json_success($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class WPI_API_Importer
|
||||||
|
{
|
||||||
|
|
||||||
|
private static function api_url($url)
|
||||||
|
{
|
||||||
|
$base_url = WPI_Config_Model::get_config('base_api');
|
||||||
|
return $base_url . $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function products(array $args = [])
|
||||||
|
{
|
||||||
|
$page = isset($args['page'])
|
||||||
|
? (int) $args['page']
|
||||||
|
: (int) WPI_Config_Model::get_config('current_page', 1);
|
||||||
|
|
||||||
|
$limit = isset($args['limit'])
|
||||||
|
? (int) $args['limit']
|
||||||
|
: (int) WPI_Config_Model::get_config('limit_per_time', 20);
|
||||||
|
|
||||||
|
$import_api = WPI_Config_Model::get_config('import_api');
|
||||||
|
$authen_key = WPI_Config_Model::get_config('authen_key');
|
||||||
|
|
||||||
|
if (!$import_api || !$authen_key) {
|
||||||
|
WPI_Logger::log('Missing API config');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = add_query_arg([
|
||||||
|
'page' => $page,
|
||||||
|
'perPage' => $limit,
|
||||||
|
], $import_api);
|
||||||
|
|
||||||
|
WPI_Logger::log("Calling API: {$url}");
|
||||||
|
|
||||||
|
return wp_remote_get(self::api_url($url), [
|
||||||
|
'timeout' => 30,
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Authorization' => 'Bearer ' . $authen_key,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function categories()
|
||||||
|
{
|
||||||
|
|
||||||
|
$url = WPI_Config_Model::get_config('categories_api');
|
||||||
|
$authen_key = WPI_Config_Model::get_config('authen_key');
|
||||||
|
|
||||||
|
if (!$url || !$authen_key) {
|
||||||
|
WPI_Logger::log('Missing API config');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wp_remote_get(self::api_url($url), [
|
||||||
|
'timeout' => 30,
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Authorization' => 'Bearer ' . $authen_key,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class WPI_Cron
|
||||||
|
{
|
||||||
|
const HOOK = 'wpi_import_products_cron';
|
||||||
|
const TRIGGER_HOOK = 'wpi_trigger_import_cron';
|
||||||
|
const SYNC_CATEGORIES_HOOK = 'wpi_sync_categories_cron';
|
||||||
|
|
||||||
|
public static function init()
|
||||||
|
{
|
||||||
|
add_filter('cron_schedules', [self::class, 'add_schedule']);
|
||||||
|
add_action(self::HOOK, [self::class, 'run']);
|
||||||
|
add_action(self::TRIGGER_HOOK, [self::class, 'trigger']);
|
||||||
|
add_action(self::SYNC_CATEGORIES_HOOK, [self::class, 'sync_categories']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function add_schedule($schedules)
|
||||||
|
{
|
||||||
|
if (!isset($schedules['every_minute'])) {
|
||||||
|
$schedules['every_minute'] = [
|
||||||
|
'interval' => 60,
|
||||||
|
'display' => 'Every Minute'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $schedules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function activate()
|
||||||
|
{
|
||||||
|
// if (!wp_next_scheduled(self::HOOK)) {
|
||||||
|
// wp_schedule_event(time(), 'every_minute', self::HOOK);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Trigger cron (2h sáng mỗi ngày)
|
||||||
|
// if (!wp_next_scheduled(self::TRIGGER_HOOK)) {
|
||||||
|
// $timestamp = strtotime('tomorrow 02:00');
|
||||||
|
|
||||||
|
// wp_schedule_event(
|
||||||
|
// $timestamp,
|
||||||
|
// 'daily',
|
||||||
|
// self::TRIGGER_HOOK
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deactivate()
|
||||||
|
{
|
||||||
|
wp_clear_scheduled_hook(self::HOOK);
|
||||||
|
wp_clear_scheduled_hook(self::TRIGGER_HOOK);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function run()
|
||||||
|
{
|
||||||
|
if (!class_exists('WooCommerce')) {
|
||||||
|
WPI_Logger::log('WooCommerce not active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WPI_Config_Model::get_config('status_cron')) {
|
||||||
|
WPI_Logger::log('Status cron is OFF');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WPI_Config_Model::get_config('sync_status')) {
|
||||||
|
WPI_Logger::log('Sync already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$import_api = WPI_Config_Model::get_config('import_api');
|
||||||
|
$authen_key = WPI_Config_Model::get_config('authen_key');
|
||||||
|
|
||||||
|
if (!$import_api || !$authen_key) {
|
||||||
|
WPI_Logger::log('Missing API config');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = (int) WPI_Config_Model::get_config('limit_per_time', 20);
|
||||||
|
$page = (int) WPI_Config_Model::get_config('current_page', 1);
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config('sync_status', 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
$response = WPI_API_Importer::products([
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
WPI_Logger::log('API Error: ' . $response->get_error_message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code($response);
|
||||||
|
$body = wp_remote_retrieve_body($response);
|
||||||
|
|
||||||
|
WPI_Logger::log("HTTP Code: {$code}");
|
||||||
|
|
||||||
|
if ($code !== 200) {
|
||||||
|
WPI_Logger::log("API failed: {$body}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body, true)['data'];
|
||||||
|
|
||||||
|
if (!is_array($data)) {
|
||||||
|
WPI_Logger::log('Invalid JSON response');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WPI_Logger::log('Fetched ' . count($data) . ' items');
|
||||||
|
|
||||||
|
$category_map = WPI_Category_Mapper::get_category_map();
|
||||||
|
|
||||||
|
|
||||||
|
WPI_Logger::log(json_encode($category_map));
|
||||||
|
|
||||||
|
foreach ($data as $item) {
|
||||||
|
|
||||||
|
$import_data = WPI_Product_Map::from_api($item, $category_map);
|
||||||
|
|
||||||
|
$validation = WPI_Product_Validator::validate($import_data);
|
||||||
|
|
||||||
|
if (!$validation['valid']) {
|
||||||
|
WPI_Logger::log(
|
||||||
|
'Skip product SKU ' . ($import_data['sku'] ?? 'N/A') .
|
||||||
|
' | Errors: ' . implode(' | ', $validation['errors'])
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
WPI_Logger::log_product_item($import_data);
|
||||||
|
|
||||||
|
WPI_Product_Mapper::upsert_from_api($import_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config('current_page', $page + 1);
|
||||||
|
|
||||||
|
if (count($data) < $limit) {
|
||||||
|
WPI_Config_Model::set_config('status_cron', 0);
|
||||||
|
WPI_Config_Model::set_config('current_page', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
WPI_Logger::log("Page {$page} done");
|
||||||
|
} finally {
|
||||||
|
WPI_Config_Model::set_config('sync_status', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function trigger()
|
||||||
|
{
|
||||||
|
if (WPI_Config_Model::get_config('sync_status')) {
|
||||||
|
WPI_Logger::log('Trigger skipped: sync is running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WPI_Logger::log('Trigger import cron ON');
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config('status_cron', 1);
|
||||||
|
WPI_Config_Model::set_config('current_page', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function sync_categories()
|
||||||
|
{
|
||||||
|
if (!class_exists('WooCommerce')) {
|
||||||
|
WPI_Logger::log('WooCommerce not active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WPI_Config_Model::get_config('categories_status_cron')) {
|
||||||
|
WPI_Logger::log('Status sync categories cron is OFF');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WPI_Config_Model::get_config('categories_sync_status')) {
|
||||||
|
WPI_Logger::log('Sync categories already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock
|
||||||
|
WPI_Config_Model::set_config('categories_sync_status', 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = WPI_API_Importer::categories();
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
WPI_Logger::log('API Error: ' . $response->get_error_message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code($response);
|
||||||
|
$body = wp_remote_retrieve_body($response);
|
||||||
|
|
||||||
|
WPI_Logger::log("HTTP Code: {$code}");
|
||||||
|
|
||||||
|
if ($code !== 200) {
|
||||||
|
WPI_Logger::log("API failed: {$body}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_decode($body, true);
|
||||||
|
|
||||||
|
if (empty($json['status']) || empty($json['data']) || !is_array($json['data'])) {
|
||||||
|
WPI_Logger::log('Invalid API response structure');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WPI_Logger::log('Fetched ' . count($json['data']) . ' root categories');
|
||||||
|
|
||||||
|
|
||||||
|
WPI_Category_Mapper::upsert_wp_categories_from_tree(
|
||||||
|
$json['data'],
|
||||||
|
0,
|
||||||
|
'product_cat'
|
||||||
|
);
|
||||||
|
|
||||||
|
WPI_Logger::log('Category sync completed successfully');
|
||||||
|
} finally {
|
||||||
|
// Unlock ONLY
|
||||||
|
WPI_Config_Model::set_config('categories_sync_status', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class WPI_Logger
|
||||||
|
{
|
||||||
|
|
||||||
|
public static function log($message)
|
||||||
|
{
|
||||||
|
$dir = WP_CONTENT_DIR . '/uploads/wpi-import';
|
||||||
|
if (!file_exists($dir)) {
|
||||||
|
wp_mkdir_p($dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $dir . '/wpi-cron.log';
|
||||||
|
$time = current_time('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
$file,
|
||||||
|
"[{$time}] {$message}\n",
|
||||||
|
FILE_APPEND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static function log_product_item($item)
|
||||||
|
{
|
||||||
|
$log_file = WP_CONTENT_DIR . '/uploads/wpi-import/api-product-preview.log';
|
||||||
|
|
||||||
|
if (!file_exists(dirname($log_file))) {
|
||||||
|
wp_mkdir_p(dirname($log_file));
|
||||||
|
}
|
||||||
|
|
||||||
|
$time = current_time('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
$log_file,
|
||||||
|
"================ ITEM {$item['sku']} | {$time} ================\n" .
|
||||||
|
print_r($item, true) . "\n\n",
|
||||||
|
FILE_APPEND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function log_category_item(array $category)
|
||||||
|
{
|
||||||
|
$log_file = WP_CONTENT_DIR . '/uploads/wpi-import/api-category-preview.log';
|
||||||
|
|
||||||
|
if (!file_exists(dirname($log_file))) {
|
||||||
|
wp_mkdir_p(dirname($log_file));
|
||||||
|
}
|
||||||
|
|
||||||
|
$time = current_time('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$id = $category['id'] ?? 'N/A';
|
||||||
|
$name = $category['name'] ?? 'N/A';
|
||||||
|
$parent_id = $category['parent_id'] ?? 0;
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
$log_file,
|
||||||
|
"================ CATEGORY {$id} | {$name} | {$time} ================\n" .
|
||||||
|
print_r($category, true) . "\n\n",
|
||||||
|
FILE_APPEND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class WPI_Product_Map
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Map raw API item → standardized product data
|
||||||
|
*/
|
||||||
|
public static function from_api(
|
||||||
|
array $item,
|
||||||
|
array $category_map
|
||||||
|
): array {
|
||||||
|
|
||||||
|
return [
|
||||||
|
'sku' => $item['sku'] ?? null,
|
||||||
|
'name' => $item['name'] ?? '',
|
||||||
|
|
||||||
|
'price' => (float) ($item['price'] ?? 0),
|
||||||
|
'stock' => (int) ($item['stock'] ?? 0),
|
||||||
|
|
||||||
|
'description' => $item['description'] ?? '',
|
||||||
|
'short_description' => $item['short_description'] ?? '',
|
||||||
|
|
||||||
|
'brand' => $item['brand'] ?? null,
|
||||||
|
'categories' => self::map_categories($item, $category_map),
|
||||||
|
'images' => self::map_images($item),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected static function map_categories(
|
||||||
|
array $item,
|
||||||
|
array $category_map
|
||||||
|
): array {
|
||||||
|
|
||||||
|
if (empty($item['categories']) || !is_array($item['categories'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$wp_category_ids = [];
|
||||||
|
|
||||||
|
foreach ($item['categories'] as $cat) {
|
||||||
|
if (empty($cat['id'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$external_id = (string) $cat['id'];
|
||||||
|
|
||||||
|
// map external id → wp term id
|
||||||
|
if (isset($category_map[$external_id])) {
|
||||||
|
$wp_category_ids[] = $category_map[$external_id];
|
||||||
|
} else {
|
||||||
|
// log category chưa map được (rất quan trọng)
|
||||||
|
WPI_Logger::log(
|
||||||
|
"[CATEGORY_MAP] Missing category external_id={$external_id} ({$cat['name']})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($wp_category_ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected static function map_images(array $item): array
|
||||||
|
{
|
||||||
|
if (empty($item['images']) || !is_array($item['images'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$default = [];
|
||||||
|
$others = [];
|
||||||
|
|
||||||
|
foreach ($item['images'] as $img) {
|
||||||
|
if (empty($img['url'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($img['is_default'])) {
|
||||||
|
$default[] = $img['url'];
|
||||||
|
} else {
|
||||||
|
$others[] = $img['url'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default image lên trước
|
||||||
|
return array_merge($default, $others);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class WPI_Product_Validator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate product data after mapping
|
||||||
|
*
|
||||||
|
* @return array [bool $is_valid, array $errors]
|
||||||
|
*/
|
||||||
|
public static function validate(array $data): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Required fields
|
||||||
|
if (empty($data['sku'])) {
|
||||||
|
$errors[] = 'SKU is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($data['name'])) {
|
||||||
|
$errors[] = 'Product name is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price
|
||||||
|
if (!isset($data['price']) || !is_numeric($data['price'])) {
|
||||||
|
$errors[] = 'Price must be numeric';
|
||||||
|
} elseif ($data['price'] < 0) {
|
||||||
|
$errors[] = 'Price must be >= 0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock
|
||||||
|
if (!isset($data['stock']) || !is_numeric($data['stock'])) {
|
||||||
|
$errors[] = 'Stock must be numeric';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Images
|
||||||
|
if (!empty($data['images']) && !is_array($data['images'])) {
|
||||||
|
$errors[] = 'Images must be an array';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
if (!empty($data['categories']) && !is_array($data['categories'])) {
|
||||||
|
$errors[] = 'Categories must be an array';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'valid' => empty($errors),
|
||||||
|
'errors' => $errors
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
class WPI_Category_Mapper
|
||||||
|
{
|
||||||
|
public static function upsert_wp_categories_from_tree(
|
||||||
|
array $categories,
|
||||||
|
int $parent_id = 0,
|
||||||
|
string $taxonomy = 'category'
|
||||||
|
) {
|
||||||
|
foreach ($categories as $item) {
|
||||||
|
|
||||||
|
if (empty($item['id']) || empty($item['name'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$external_id = (string) $item['id'];
|
||||||
|
$name = $item['name'];
|
||||||
|
$slug = sanitize_title($name);
|
||||||
|
|
||||||
|
// Log category item
|
||||||
|
WPI_Logger::log_category_item($item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find existing term by description (external id)
|
||||||
|
*/
|
||||||
|
$existing_term = self::find_term_by_description(
|
||||||
|
$external_id,
|
||||||
|
$taxonomy
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($existing_term) {
|
||||||
|
|
||||||
|
$term_id = (int) $existing_term->term_id;
|
||||||
|
|
||||||
|
// Update name / slug / parent if needed
|
||||||
|
wp_update_term($term_id, $taxonomy, [
|
||||||
|
'name' => $name,
|
||||||
|
'slug' => $slug,
|
||||||
|
'parent' => $parent_id,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert new term
|
||||||
|
*/
|
||||||
|
$result = wp_insert_term($name, $taxonomy, [
|
||||||
|
'slug' => $slug,
|
||||||
|
'parent' => $parent_id,
|
||||||
|
'description' => $external_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (is_wp_error($result)) {
|
||||||
|
WPI_Logger::log('[CATEGORY_SYNC_ERROR] ' . $result->get_error_message());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$term_id = (int) $result['term_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursive children
|
||||||
|
*/
|
||||||
|
if (!empty($item['children']) && is_array($item['children'])) {
|
||||||
|
self::upsert_wp_categories_from_tree(
|
||||||
|
$item['children'],
|
||||||
|
$term_id,
|
||||||
|
$taxonomy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find term by description (external ID)
|
||||||
|
*/
|
||||||
|
private static function find_term_by_description(
|
||||||
|
string $description,
|
||||||
|
string $taxonomy
|
||||||
|
) {
|
||||||
|
$terms = get_terms([
|
||||||
|
'taxonomy' => $taxonomy,
|
||||||
|
'hide_empty' => false,
|
||||||
|
'number' => 1,
|
||||||
|
'meta_query' => [],
|
||||||
|
'description__like' => $description,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return !empty($terms) ? $terms[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function get_category_map(
|
||||||
|
string $taxonomy = 'product_cat'
|
||||||
|
): array {
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
|
||||||
|
$terms = get_terms([
|
||||||
|
'taxonomy' => $taxonomy,
|
||||||
|
'hide_empty' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (is_wp_error($terms) || empty($terms)) {
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($terms as $term) {
|
||||||
|
$external_id = trim($term->description);
|
||||||
|
|
||||||
|
if ($external_id === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$map[(string) $external_id] = (int) $term->term_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
<?php
|
||||||
|
class WPI_Product_Mapper
|
||||||
|
{
|
||||||
|
|
||||||
|
public static function upsert($data)
|
||||||
|
{
|
||||||
|
if (empty($data['sku'])) return;
|
||||||
|
|
||||||
|
$product_id = wc_get_product_id_by_sku($data['sku']);
|
||||||
|
|
||||||
|
$product = $product_id
|
||||||
|
? wc_get_product($product_id)
|
||||||
|
: new WC_Product_Simple();
|
||||||
|
|
||||||
|
$product->set_name($data['name'] ?? '');
|
||||||
|
$product->set_sku($data['sku']);
|
||||||
|
$product->set_regular_price($data['price'] ?? 0);
|
||||||
|
$product->set_stock_quantity($data['stock'] ?? 0);
|
||||||
|
$product->set_manage_stock(true);
|
||||||
|
|
||||||
|
|
||||||
|
if (!empty($data['description'])) {
|
||||||
|
$product->set_description(wp_kses_post($data['description']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['short_description'])) {
|
||||||
|
$product->set_short_description(wp_kses_post($data['short_description']));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $product->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function attach_images(int $product_id, array $urls)
|
||||||
|
{
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||||
|
|
||||||
|
$gallery_ids = [];
|
||||||
|
|
||||||
|
foreach ($urls as $index => $url) {
|
||||||
|
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment_id = media_sideload_image($url, $product_id, null, 'id');
|
||||||
|
|
||||||
|
if (is_wp_error($attachment_id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($index === 0) {
|
||||||
|
set_post_thumbnail($product_id, $attachment_id);
|
||||||
|
} else {
|
||||||
|
$gallery_ids[] = $attachment_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gallery_ids) {
|
||||||
|
update_post_meta(
|
||||||
|
$product_id,
|
||||||
|
'_product_image_gallery',
|
||||||
|
implode(',', $gallery_ids)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function upsert_from_api(array $data)
|
||||||
|
{
|
||||||
|
if (empty($data['sku'])) return;
|
||||||
|
|
||||||
|
$product_id = self::upsert($data);
|
||||||
|
|
||||||
|
if (!$product_id) return;
|
||||||
|
|
||||||
|
if (!empty($data['brand'])) {
|
||||||
|
self::sync_brand($product_id, $data['brand']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['categories'])) {
|
||||||
|
self::sync_categories($product_id, $data['categories']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['images'])) {
|
||||||
|
self::sync_images($product_id, $data['images']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $product_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function sync_categories(int $product_id, array $categories)
|
||||||
|
{
|
||||||
|
if (empty($categories)) return;
|
||||||
|
|
||||||
|
$term_ids = [];
|
||||||
|
|
||||||
|
foreach ($categories as $cat_id) {
|
||||||
|
// chỉ nhận số
|
||||||
|
if (is_numeric($cat_id)) {
|
||||||
|
$term_ids[] = (int) $cat_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$term_ids) return;
|
||||||
|
|
||||||
|
// gán trực tiếp, KHÔNG tạo mới
|
||||||
|
wp_set_object_terms(
|
||||||
|
$product_id,
|
||||||
|
array_unique($term_ids),
|
||||||
|
'product_cat'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected static function sync_images(int $product_id, array $images)
|
||||||
|
{
|
||||||
|
if (empty($images)) return;
|
||||||
|
|
||||||
|
$urls = array_filter(array_map('trim', $images));
|
||||||
|
if (!$urls) return;
|
||||||
|
|
||||||
|
self::attach_images($product_id, $urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected static function sync_brand(int $product_id, string $brand)
|
||||||
|
{
|
||||||
|
$brand = trim($brand);
|
||||||
|
if (!$brand) return;
|
||||||
|
|
||||||
|
$taxonomy = 'product_brand';
|
||||||
|
|
||||||
|
// Kiểm tra taxonomy tồn tại
|
||||||
|
if (!taxonomy_exists($taxonomy)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check brand đã tồn tại chưa
|
||||||
|
$term = term_exists($brand, $taxonomy);
|
||||||
|
|
||||||
|
// Chưa có → tạo mới
|
||||||
|
if (!$term) {
|
||||||
|
$term = wp_insert_term($brand, $taxonomy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_wp_error($term)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$term_id = (int) $term['term_id'];
|
||||||
|
|
||||||
|
// Gán brand cho product
|
||||||
|
wp_set_object_terms($product_id, [$term_id], $taxonomy);
|
||||||
|
|
||||||
|
// Đảm bảo attribute xuất hiện trong product
|
||||||
|
$product = wc_get_product($product_id);
|
||||||
|
if (!$product) return;
|
||||||
|
|
||||||
|
$attributes = $product->get_attributes();
|
||||||
|
|
||||||
|
if (!isset($attributes[$taxonomy])) {
|
||||||
|
$attr = new WC_Product_Attribute();
|
||||||
|
$attr->set_name($taxonomy);
|
||||||
|
$attr->set_options([$brand]);
|
||||||
|
$attr->set_visible(true);
|
||||||
|
$attr->set_variation(false);
|
||||||
|
|
||||||
|
$attributes[$taxonomy] = $attr;
|
||||||
|
$product->set_attributes($attributes);
|
||||||
|
$product->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function create_wp_categories_from_tree(
|
||||||
|
array $categories,
|
||||||
|
int $parent_id = 0,
|
||||||
|
string $taxonomy = 'category'
|
||||||
|
) {
|
||||||
|
foreach ($categories as $item) {
|
||||||
|
if (empty($item['name']) || empty($item['id'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $item['name'];
|
||||||
|
$slug = sanitize_title($name);
|
||||||
|
$description = (string) $item['id']; // 👈 external ID
|
||||||
|
|
||||||
|
// Check if term exists under same parent
|
||||||
|
$existing = term_exists($slug, $taxonomy, $parent_id);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$term_id = is_array($existing)
|
||||||
|
? $existing['term_id']
|
||||||
|
: $existing;
|
||||||
|
|
||||||
|
// Update description if changed
|
||||||
|
wp_update_term($term_id, $taxonomy, [
|
||||||
|
'description' => $description,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$result = wp_insert_term($name, $taxonomy, [
|
||||||
|
'slug' => $slug,
|
||||||
|
'parent' => $parent_id,
|
||||||
|
'description' => $description,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (is_wp_error($result)) {
|
||||||
|
error_log('[CATEGORY_SYNC] ' . $result->get_error_message());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$term_id = $result['term_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse children
|
||||||
|
if (!empty($item['children']) && is_array($item['children'])) {
|
||||||
|
self::create_wp_categories_from_tree(
|
||||||
|
$item['children'],
|
||||||
|
$term_id,
|
||||||
|
$taxonomy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
function wpi_create_config_table()
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table = $wpdb->prefix . 'wpi_configs';
|
||||||
|
$charset = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE {$table} (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
config_key VARCHAR(191) NOT NULL UNIQUE,
|
||||||
|
config_value LONGTEXT NULL,
|
||||||
|
description VARCHAR(255) NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL
|
||||||
|
) {$charset};";
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
dbDelta($sql);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class WPI_Config_Model
|
||||||
|
{
|
||||||
|
private static function table()
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . 'wpi_configs';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check config exists
|
||||||
|
*/
|
||||||
|
private static function exists(string $key): bool
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (bool) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT 1 FROM " . self::table() . " WHERE config_key = %s LIMIT 1",
|
||||||
|
$key
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert or update config
|
||||||
|
*/
|
||||||
|
public static function set_config(string $key, $value, string $description = null)
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table = self::table();
|
||||||
|
$now = current_time('mysql');
|
||||||
|
$data = [
|
||||||
|
'config_value' => wp_json_encode($value),
|
||||||
|
'updated_at' => $now,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($description !== null) {
|
||||||
|
$data['description'] = $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::exists($key)) {
|
||||||
|
|
||||||
|
// UPDATE only
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
$data,
|
||||||
|
['config_key' => $key],
|
||||||
|
['%s', '%s', '%s'],
|
||||||
|
['%s']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// INSERT new
|
||||||
|
$wpdb->insert(
|
||||||
|
$table,
|
||||||
|
array_merge($data, [
|
||||||
|
'config_key' => $key,
|
||||||
|
'created_at' => $now,
|
||||||
|
]),
|
||||||
|
['%s', '%s', '%s', '%s']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get config (auto json decode)
|
||||||
|
*/
|
||||||
|
public static function get_config(string $key, $default = null)
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$value = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT config_value FROM " . self::table() . " WHERE config_key = %s",
|
||||||
|
$key
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $value !== null ? json_decode($value, true) : $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
- Run cron
|
||||||
|
<!-- Run simple -->
|
||||||
|
<!-- Cron run import product -->
|
||||||
|
|
||||||
|
wp cron event run wpi_import_products_cron --allow-root <!-- Call by cron -->
|
||||||
|
wp eval "do_action('wpi_import_products_cron');" --allow-root <!-- Call by CLI -->
|
||||||
|
|
||||||
|
<!-- Cron sync categories -->
|
||||||
|
|
||||||
|
wp cron event run wpi_sync_categories_cron --allow-root <!-- Call by cron -->
|
||||||
|
wp eval "do_action('wpi_sync_categories_cron');" --allow-root <!-- Call by CLI -->
|
||||||
|
|
||||||
|
<!-- Cron triger auto sync -->
|
||||||
|
|
||||||
|
wp cron event run wpi_trigger_import_cron --allow-root <!-- Call by cron -->
|
||||||
|
wp eval "do_action('wpi_trigger_import_cron');" --allow-root <!-- Call by CLI -->
|
||||||
|
|
||||||
|
- Run Cron tabs
|
||||||
|
0 2 \* \* \* cd /var/www/html && wp cron event run wpi_trigger_import_cron --allow-root >> /var/log/wp-trigger-import.log 2>&1
|
||||||
|
|
||||||
|
- - - - - cd /var/www/html && wp cron event run wpi_import_products_cron --allow-root >> /var/log/wp-import-products.log 2>&1
|
||||||
|
|
||||||
|
- CLI WP
|
||||||
|
|
||||||
|
# Xoá toàn bộ products
|
||||||
|
|
||||||
|
wp post delete \
|
||||||
|
$(wp post list --post_type=product --field=ID --allow-root) \
|
||||||
|
--force --allow-root
|
||||||
|
|
||||||
|
# Xoá toàn bộ product categories
|
||||||
|
|
||||||
|
wp term delete product_cat \
|
||||||
|
$(wp term list product_cat --field=term_id --allow-root) \
|
||||||
|
--allow-root
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Name: WP Product Importer
|
||||||
|
* Description: Import WooCommerce products from API or CSV (Cron supported)
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Author: You
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
define('WPI_PATH', plugin_dir_path(__FILE__));
|
||||||
|
define('WPI_URL', plugin_dir_url(__FILE__));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =========================
|
||||||
|
* Require core files
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Migrations
|
||||||
|
require_once WPI_PATH . 'includes/migrations/create-config-table.php';
|
||||||
|
|
||||||
|
// Models
|
||||||
|
require_once WPI_PATH . 'includes/models/class-wpi-config-model.php';
|
||||||
|
|
||||||
|
// Helper
|
||||||
|
require_once WPI_PATH . 'includes/helpers/class-product-helper.php';
|
||||||
|
require_once WPI_PATH . 'includes/helpers/class-category-helper.php';
|
||||||
|
|
||||||
|
|
||||||
|
// Core classes
|
||||||
|
require_once WPI_PATH . 'includes/class-api-importer.php';
|
||||||
|
require_once WPI_PATH . 'includes/class-cron.php';
|
||||||
|
require_once WPI_PATH . 'includes/class-logger.php';
|
||||||
|
require_once WPI_PATH . 'includes/class-admin-page.php';
|
||||||
|
require_once WPI_PATH . 'includes/class-product-map.php';
|
||||||
|
require_once WPI_PATH . 'includes/class-product-validator.php';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =========================
|
||||||
|
* Init plugin
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
add_action('plugins_loaded', function () {
|
||||||
|
|
||||||
|
// Admin UI
|
||||||
|
// if (is_admin()) {
|
||||||
|
// new WPI_Admin_Page();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Init cron only if WooCommerce active
|
||||||
|
if (class_exists('WooCommerce')) {
|
||||||
|
WPI_Cron::init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =========================
|
||||||
|
* Plugin activation
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
register_activation_hook(__FILE__, function () {
|
||||||
|
|
||||||
|
if (!class_exists('WooCommerce')) {
|
||||||
|
deactivate_plugins(plugin_basename(__FILE__));
|
||||||
|
wp_die('WooCommerce is required for WP Product Importer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create DB tables
|
||||||
|
wpi_create_config_table();
|
||||||
|
|
||||||
|
// Default configs
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'base_api',
|
||||||
|
'https://distidata.nswteam.net/api/ecom',
|
||||||
|
'Api path to get data import'
|
||||||
|
);
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'import_api',
|
||||||
|
'/scraping-product',
|
||||||
|
'Api path to get data import'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Default configs
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'categories_api',
|
||||||
|
'/category',
|
||||||
|
'Api path to get categories'
|
||||||
|
);
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'authen_key',
|
||||||
|
'abc',
|
||||||
|
'Authentication key'
|
||||||
|
);
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'limit_per_time',
|
||||||
|
30,
|
||||||
|
'Item sync per time'
|
||||||
|
);
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'current_page',
|
||||||
|
1,
|
||||||
|
'Current page sync'
|
||||||
|
);
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'sync_status',
|
||||||
|
0,
|
||||||
|
'Status sync process'
|
||||||
|
);
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'status_cron',
|
||||||
|
1,
|
||||||
|
'Status cron sync'
|
||||||
|
);
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'categories_sync_status',
|
||||||
|
0,
|
||||||
|
'Status sync categories process'
|
||||||
|
);
|
||||||
|
|
||||||
|
WPI_Config_Model::set_config(
|
||||||
|
'categories_status_cron',
|
||||||
|
1,
|
||||||
|
'Categoroies status cron'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Activate cron
|
||||||
|
WPI_Cron::activate();
|
||||||
|
|
||||||
|
error_log('[WPI] Plugin activated');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =========================
|
||||||
|
* Plugin deactivation
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
register_deactivation_hook(__FILE__, function () {
|
||||||
|
|
||||||
|
WPI_Cron::deactivate();
|
||||||
|
|
||||||
|
error_log('[WPI] Plugin deactivated');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue