AIventory Connector requires WooCommerce to be installed and active.
';
});
return;
}
class AIventory_Connector {
private $api_key_option = 'aiventory_api_key';
private $version = '1.0.0';
public function __construct() {
register_activation_hook(__FILE__, array($this, 'activate'));
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
add_action('rest_api_init', array($this, 'register_routes'));
}
public function enqueue_admin_assets($hook) {
// Only load on our plugin page
if ($hook !== 'woocommerce_page_aiventory-connector') {
return;
}
// Register and enqueue inline script
wp_register_script('aiventory-admin', false, array(), $this->version, true);
wp_enqueue_script('aiventory-admin');
wp_add_inline_script('aiventory-admin', '
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
alert("Copied to clipboard!");
});
}
');
// Register and enqueue inline styles
wp_register_style('aiventory-admin', false, array(), $this->version);
wp_enqueue_style('aiventory-admin');
wp_add_inline_style('aiventory-admin', '
.card { background: white; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card h2 { margin-top: 0; }
');
}
public function activate() {
if (!get_option($this->api_key_option)) {
$api_key = 'aiv_' . bin2hex(random_bytes(32));
update_option($this->api_key_option, $api_key);
}
}
public function add_admin_menu() {
add_submenu_page(
'woocommerce',
'AIventory Connector',
'AIventory',
'manage_woocommerce',
'aiventory-connector',
array($this, 'admin_page')
);
}
public function admin_page() {
$api_key = get_option($this->api_key_option);
$store_url = get_site_url();
if (isset($_POST['regenerate_key']) && check_admin_referer('aiventory_regenerate')) {
$api_key = 'aiv_' . bin2hex(random_bytes(32));
update_option($this->api_key_option, $api_key);
echo 'API Key regenerated successfully!
';
}
include(plugin_dir_path(__FILE__) . 'admin-page.php');
}
public function register_routes() {
$endpoints = array(
array('path' => '/test', 'callback' => 'test_connection'),
array('path' => '/store', 'callback' => 'get_store_info'),
array('path' => '/products', 'callback' => 'get_products'),
array('path' => '/variants', 'callback' => 'get_variants'),
array('path' => '/collections', 'callback' => 'get_collections'),
array('path' => '/customers', 'callback' => 'get_customers'),
array('path' => '/customer-addresses', 'callback' => 'get_customer_addresses'),
array('path' => '/orders', 'callback' => 'get_orders'),
array('path' => '/order-line-items', 'callback' => 'get_order_line_items'),
array('path' => '/fulfillments', 'callback' => 'get_fulfillments'),
array('path' => '/refunds', 'callback' => 'get_refunds'),
array('path' => '/refund-line-items', 'callback' => 'get_refund_line_items'),
array('path' => '/discounts', 'callback' => 'get_discounts'),
array('path' => '/inventory-ledger', 'callback' => 'get_inventory_ledger')
);
foreach ($endpoints as $endpoint) {
register_rest_route('aiventory/v1', $endpoint['path'], array(
'methods' => 'GET',
'callback' => array($this, $endpoint['callback']),
'permission_callback' => array($this, 'verify_api_key')
));
}
}
public function verify_api_key($request) {
$api_key = $request->get_header('X-API-Key') ?: $request->get_param('api_key');
$stored_key = get_option($this->api_key_option);
return !empty($api_key) && hash_equals($stored_key, $api_key);
}
public function test_connection($request) {
return rest_ensure_response(array(
'success' => true,
'message' => 'AIventory Connector is working!',
'store_name' => get_bloginfo('name'),
'woocommerce_version' => WC()->version,
'plugin_version' => $this->version
));
}
public function get_store_info($request) {
return rest_ensure_response(array(
'success' => true,
'data' => array(
'name' => get_bloginfo('name'),
'url' => get_site_url(),
'email' => get_option('admin_email'),
'currency' => get_woocommerce_currency(),
'timezone' => wp_timezone_string(),
'woocommerce_version' => WC()->version
)
));
}
public function get_products($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$products = wc_get_products(array(
'status' => 'publish',
'limit' => $per_page,
'page' => $page,
'orderby' => 'date',
'order' => 'DESC'
));
$data = array();
foreach ($products as $product) {
$data[] = array(
'id' => $product->get_id(),
'external_product_id' => (string) $product->get_id(),
'title' => $product->get_name(),
'description' => $product->get_description(),
'handle' => $product->get_slug(),
'product_type' => $product->get_type(),
'vendor' => get_post_meta($product->get_id(), '_vendor', true) ?: '',
'tags' => array_map(function($tag) { return $tag->name; }, wp_get_post_terms($product->get_id(), 'product_tag')),
'status' => $product->get_status(),
'published' => $product->get_status() === 'publish',
'image_url' => wp_get_attachment_url($product->get_image_id()) ?: '',
'created_at' => $product->get_date_created() ? $product->get_date_created()->date('c') : null,
'updated_at' => $product->get_date_modified() ? $product->get_date_modified()->date('c') : null,
'platform' => 'woocommerce'
);
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_variants($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$product_id = $request->get_param('product_id');
$args = array(
'type' => array('simple', 'variation'),
'limit' => $per_page,
'page' => $page
);
if ($product_id) {
$args['parent'] = $product_id;
}
$products = wc_get_products($args);
$data = array();
foreach ($products as $product) {
$parent_id = $product->get_parent_id() ?: $product->get_id();
$parent = $product->get_parent_id() ? wc_get_product($product->get_parent_id()) : $product;
$data[] = array(
'id' => $product->get_id(),
'product_id' => $parent_id,
'external_variant_id' => (string) $product->get_id(),
'external_product_id' => (string) $parent_id,
'sku' => $product->get_sku() ?: 'product-' . $product->get_id(),
'title' => $product->get_name(),
'price' => (float) $product->get_price(),
'compare_at_price' => (float) $product->get_regular_price(),
'cost' => (float) $product->get_meta('_cost'),
'inventory_quantity' => $product->get_stock_quantity() ?: 0,
'barcode' => $product->get_meta('_barcode') ?: '',
'product_title' => $parent ? $parent->get_name() : '',
'product_type' => $product->get_type(),
'product_status' => $product->get_status(),
'image_url' => wp_get_attachment_url($product->get_image_id()) ?: '',
'created_at' => $product->get_date_created() ? $product->get_date_created()->date('c') : null,
'platform' => 'woocommerce'
);
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_collections($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$categories = get_terms(array(
'taxonomy' => 'product_cat',
'number' => $per_page,
'offset' => ($page - 1) * $per_page,
'hide_empty' => false
));
$data = array();
foreach ($categories as $category) {
$data[] = array(
'id' => $category->term_id,
'external_collection_id' => (string) $category->term_id,
'title' => $category->name,
'handle' => $category->slug,
'description' => $category->description,
'published' => true,
'platform' => 'woocommerce'
);
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_customers($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$customers = get_users(array(
'role__in' => array('customer', 'subscriber'),
'number' => $per_page,
'paged' => $page
));
$data = array();
foreach ($customers as $customer) {
$wc_customer = new WC_Customer($customer->ID);
$first_order = wc_get_orders(array(
'customer_id' => $customer->ID,
'limit' => 1,
'orderby' => 'date',
'order' => 'ASC'
));
$data[] = array(
'id' => $customer->ID,
'external_customer_id' => (string) $customer->ID,
'customer_name' => trim($wc_customer->get_first_name() . ' ' . $wc_customer->get_last_name()) ?: $customer->user_login,
'email' => $customer->user_email,
'phone' => $wc_customer->get_billing_phone(),
'first_name' => $wc_customer->get_first_name(),
'last_name' => $wc_customer->get_last_name(),
'orders_count' => wc_get_customer_order_count($customer->ID),
'total_spent' => (float) wc_get_customer_total_spent($customer->ID),
'first_order_date' => !empty($first_order) ? $first_order[0]->get_date_created()->date('Y-m-d') : null,
'accepts_marketing' => (bool) $wc_customer->get_meta('accepts_marketing'),
'tax_exempt' => $wc_customer->is_vat_exempt(),
'created_at' => $customer->user_registered,
'platform' => 'woocommerce'
);
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_customer_addresses($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$customers = get_users(array(
'role__in' => array('customer', 'subscriber'),
'number' => $per_page,
'paged' => $page
));
$data = array();
foreach ($customers as $customer) {
$wc_customer = new WC_Customer($customer->ID);
if ($wc_customer->get_billing_address_1()) {
$data[] = array(
'customer_id' => $customer->ID,
'address_type' => 'billing',
'first_name' => $wc_customer->get_billing_first_name(),
'last_name' => $wc_customer->get_billing_last_name(),
'company' => $wc_customer->get_billing_company(),
'address1' => $wc_customer->get_billing_address_1(),
'address2' => $wc_customer->get_billing_address_2(),
'city' => $wc_customer->get_billing_city(),
'province_code' => $wc_customer->get_billing_state(),
'country_code' => $wc_customer->get_billing_country(),
'zip' => $wc_customer->get_billing_postcode(),
'phone' => $wc_customer->get_billing_phone(),
'is_default' => true,
'platform' => 'woocommerce'
);
}
if ($wc_customer->get_shipping_address_1()) {
$data[] = array(
'customer_id' => $customer->ID,
'address_type' => 'shipping',
'first_name' => $wc_customer->get_shipping_first_name(),
'last_name' => $wc_customer->get_shipping_last_name(),
'company' => $wc_customer->get_shipping_company(),
'address1' => $wc_customer->get_shipping_address_1(),
'address2' => $wc_customer->get_shipping_address_2(),
'city' => $wc_customer->get_shipping_city(),
'province_code' => $wc_customer->get_shipping_state(),
'country_code' => $wc_customer->get_shipping_country(),
'zip' => $wc_customer->get_shipping_postcode(),
'phone' => $wc_customer->get_billing_phone(),
'is_default' => false,
'platform' => 'woocommerce'
);
}
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_orders($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$status = $request->get_param('status') ?: 'any';
$args = array(
'limit' => $per_page,
'page' => $page,
'orderby' => 'date',
'order' => 'DESC'
);
if ($status !== 'any') {
$args['status'] = $status;
}
$orders = wc_get_orders($args);
$data = array();
foreach ($orders as $order) {
$data[] = array(
'id' => $order->get_id(),
'external_order_id' => (string) $order->get_id(),
'order_number' => (string) $order->get_order_number(),
'external_customer_id' => $order->get_customer_id() ? (string) $order->get_customer_id() : null,
'financial_status' => $this->map_financial_status($order->get_status()),
'fulfillment_status' => $this->map_fulfillment_status($order->get_status()),
'currency' => $order->get_currency(),
'subtotal' => (float) $order->get_subtotal(),
'total_tax' => (float) $order->get_total_tax(),
'total_shipping' => (float) $order->get_shipping_total(),
'total_discounts' => (float) $order->get_total_discount(),
'total_amount' => (float) $order->get_total(),
'payment_method' => $order->get_payment_method_title(),
'notes' => $order->get_customer_note(),
'source_name' => $order->get_created_via(),
'processed_at' => $order->get_date_paid() ? $order->get_date_paid()->date('c') : null,
'created_at' => $order->get_date_created()->date('c'),
'updated_at' => $order->get_date_modified()->date('c'),
'platform' => 'woocommerce'
);
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_order_line_items($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$order_id = $request->get_param('order_id');
if ($order_id) {
$orders = array(wc_get_order($order_id));
} else {
$orders = wc_get_orders(array('limit' => $per_page, 'page' => $page));
}
$data = array();
foreach ($orders as $order) {
if (!$order) continue;
foreach ($order->get_items() as $item_id => $item) {
$product = $item->get_product();
$data[] = array(
'id' => $item_id,
'order_id' => $order->get_id(),
'external_line_item_id' => (string) $item_id,
'external_order_id' => (string) $order->get_id(),
'product_id' => $item->get_product_id(),
'variant_id' => $item->get_variation_id() ?: $item->get_product_id(),
'external_product_id' => (string) $item->get_product_id(),
'external_variant_id' => $item->get_variation_id() ? (string) $item->get_variation_id() : (string) $item->get_product_id(),
'product_name' => $item->get_name(),
'sku' => $product ? $product->get_sku() : '',
'quantity' => $item->get_quantity(),
'price' => (float) $order->get_item_total($item, false),
'total_discount' => (float) ($item->get_subtotal() - $item->get_total()),
'tax_amount' => (float) $item->get_total_tax(),
'fulfillment_status' => $order->get_status() === 'completed' ? 'fulfilled' : 'unfulfilled',
'fulfillable_quantity' => $order->get_status() === 'completed' ? 0 : $item->get_quantity(),
'requires_shipping' => $product ? !$product->is_virtual() : true,
'cost_at_time' => $product ? (float) $product->get_meta('_cost') : null,
'currency' => $order->get_currency(),
'platform' => 'woocommerce'
);
}
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_fulfillments($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$orders = wc_get_orders(array(
'limit' => $per_page,
'page' => $page,
'status' => array('completed', 'processing')
));
$data = array();
foreach ($orders as $order) {
if ($order->get_status() === 'completed') {
$data[] = array(
'id' => 'fulfillment-' . $order->get_id(),
'order_id' => $order->get_id(),
'external_fulfillment_id' => 'fulfillment-' . $order->get_id(),
'external_order_id' => (string) $order->get_id(),
'status' => 'success',
'shipped_at' => $order->get_date_completed() ? $order->get_date_completed()->date('c') : null,
'delivered_at' => $order->get_date_completed() ? $order->get_date_completed()->date('c') : null,
'platform' => 'woocommerce'
);
}
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_refunds($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$refunds = wc_get_orders(array(
'limit' => $per_page,
'page' => $page,
'type' => 'shop_order_refund'
));
$data = array();
foreach ($refunds as $refund) {
$data[] = array(
'id' => $refund->get_id(),
'order_id' => $refund->get_parent_id(),
'external_refund_id' => (string) $refund->get_id(),
'external_order_id' => (string) $refund->get_parent_id(),
'refund_number' => 'refund-' . $refund->get_id(),
'status' => 'completed',
'reason' => $refund->get_reason(),
'note' => $refund->get_reason(),
'total_amount' => abs((float) $refund->get_amount()),
'created_at' => $refund->get_date_created()->date('c'),
'platform' => 'woocommerce'
);
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_refund_line_items($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$refunds = wc_get_orders(array(
'limit' => $per_page,
'page' => $page,
'type' => 'shop_order_refund'
));
$data = array();
foreach ($refunds as $refund) {
foreach ($refund->get_items() as $item_id => $item) {
$data[] = array(
'id' => $item_id,
'refund_id' => $refund->get_id(),
'external_refund_id' => (string) $refund->get_id(),
'quantity' => abs($item->get_quantity()),
'amount' => abs((float) $item->get_total()),
'restock' => true,
'platform' => 'woocommerce'
);
}
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_discounts($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
$coupons = get_posts(array(
'posts_per_page' => $per_page,
'paged' => $page,
'post_type' => 'shop_coupon',
'post_status' => 'publish'
));
$data = array();
foreach ($coupons as $coupon_post) {
$coupon = new WC_Coupon($coupon_post->ID);
$data[] = array(
'id' => $coupon->get_id(),
'external_discount_id' => (string) $coupon->get_id(),
'code' => $coupon->get_code(),
'type' => $coupon->get_discount_type(),
'value' => (float) $coupon->get_amount(),
'minimum_purchase' => (float) $coupon->get_minimum_amount(),
'usage_limit' => $coupon->get_usage_limit() ?: null,
'usage_count' => $coupon->get_usage_count(),
'starts_at' => $coupon->get_date_created() ? $coupon->get_date_created()->date('c') : null,
'ends_at' => $coupon->get_date_expires() ? $coupon->get_date_expires()->date('c') : null,
'is_active' => !$coupon->get_date_expires() || $coupon->get_date_expires() > new DateTime(),
'platform' => 'woocommerce'
);
}
return $this->paginated_response($data, $page, $per_page);
}
public function get_inventory_ledger($request) {
$page = intval($request->get_param('page') ?: 1);
$per_page = intval($request->get_param('per_page') ?: 50);
global $wpdb;
$offset = ($page - 1) * $per_page;
$query = "
SELECT
oi.order_item_id as id,
oi.order_id as related_id,
p.post_date as created_at,
oim1.meta_value as product_id,
oim2.meta_value as quantity
FROM {$wpdb->prefix}woocommerce_order_items oi
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim1
ON oi.order_item_id = oim1.order_item_id AND oim1.meta_key = '_product_id'
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim2
ON oi.order_item_id = oim2.order_item_id AND oim2.meta_key = '_qty'
LEFT JOIN {$wpdb->posts} p ON oi.order_id = p.ID
WHERE oi.order_item_type = 'line_item'
ORDER BY p.post_date DESC
LIMIT %d OFFSET %d
";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is properly prepared with placeholders
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query required for real-time order sync; caching would return stale data
$results = $wpdb->get_results($wpdb->prepare($query, $per_page, $offset));
$data = array();
foreach ($results as $row) {
$product = wc_get_product($row->product_id);
if (!$product) continue;
$data[] = array(
'id' => $row->id,
'variant_id' => $row->product_id,
'change_type' => 'sale',
'quantity_change' => -abs((int) $row->quantity),
'new_quantity' => $product->get_stock_quantity(),
'related_id' => $row->related_id,
'notes' => 'Order #' . $row->related_id,
'created_at' => $row->created_at,
'platform' => 'woocommerce'
);
}
return $this->paginated_response($data, $page, $per_page);
}
private function map_financial_status($wc_status) {
$map = array(
'pending' => 'pending',
'processing' => 'paid',
'on-hold' => 'pending',
'completed' => 'paid',
'cancelled' => 'voided',
'refunded' => 'refunded',
'failed' => 'voided'
);
return isset($map[$wc_status]) ? $map[$wc_status] : 'pending';
}
private function map_fulfillment_status($wc_status) {
$map = array(
'pending' => 'unfulfilled',
'processing' => 'unfulfilled',
'on-hold' => 'unfulfilled',
'completed' => 'fulfilled',
'cancelled' => 'unfulfilled',
'refunded' => 'unfulfilled',
'failed' => 'unfulfilled'
);
return isset($map[$wc_status]) ? $map[$wc_status] : 'unfulfilled';
}
private function paginated_response($data, $page, $per_page) {
return rest_ensure_response(array(
'success' => true,
'data' => $data,
'pagination' => array(
'page' => $page,
'per_page' => $per_page,
'total' => count($data)
)
));
}
}
new AIventory_Connector();