Split 49 modules/suites into independent git repos; untrack from monorepo
Each top-level module/suite folder is now its own private repo on GitHub (gsinghpal/<name>) and gitea (admin/<name>), with a fresh single initial commit. The monorepo no longer tracks them (added to .gitignore + git rm --cached); working-tree files are retained on disk and managed in their own repos. The monorepo keeps shared root files (CLAUDE.md, docs/, scripts/, tools/, AGENTS.md, WIP/obsolete dirs) and full history. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,138 +0,0 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Fusion_WooDoo_Admin_Settings {
|
||||
|
||||
const OPTION_ODOO_URL = 'fusion_woodoo_odoo_url';
|
||||
const OPTION_API_KEY = 'fusion_woodoo_api_key';
|
||||
const OPTION_SHOW_ORDERS = 'fusion_woodoo_show_orders';
|
||||
const OPTION_SHOW_INVOICES = 'fusion_woodoo_show_invoices';
|
||||
const OPTION_SHOW_DELIVERIES = 'fusion_woodoo_show_deliveries';
|
||||
const OPTION_SHOW_RETURNS = 'fusion_woodoo_show_returns';
|
||||
|
||||
public function __construct() {
|
||||
add_action('admin_menu', [$this, 'add_settings_page']);
|
||||
add_action('admin_init', [$this, 'register_settings']);
|
||||
add_action('admin_post_fusion_woodoo_save', [$this, 'handle_save']);
|
||||
add_action('wp_ajax_fusion_woodoo_test_connection', [$this, 'ajax_test_connection']);
|
||||
add_action('update_option_' . self::OPTION_ODOO_URL, [$this, 'on_url_changed'], 10, 0);
|
||||
}
|
||||
|
||||
public function add_settings_page(): void {
|
||||
add_submenu_page(
|
||||
'woocommerce',
|
||||
__('Fusion WooDoo', 'fusion-woodoo'),
|
||||
__('Fusion WooDoo', 'fusion-woodoo'),
|
||||
'manage_woocommerce',
|
||||
'fusion-woodoo-settings',
|
||||
[$this, 'render_settings_page']
|
||||
);
|
||||
}
|
||||
|
||||
public function register_settings(): void {
|
||||
$options = [
|
||||
self::OPTION_ODOO_URL,
|
||||
self::OPTION_API_KEY,
|
||||
self::OPTION_SHOW_ORDERS,
|
||||
self::OPTION_SHOW_INVOICES,
|
||||
self::OPTION_SHOW_DELIVERIES,
|
||||
self::OPTION_SHOW_RETURNS,
|
||||
];
|
||||
foreach ($options as $option) {
|
||||
register_setting('fusion_woodoo_settings', $option, ['sanitize_callback' => 'sanitize_text_field']);
|
||||
}
|
||||
|
||||
add_settings_section(
|
||||
'fusion_woodoo_connection',
|
||||
__('Odoo Connection', 'fusion-woodoo'),
|
||||
null,
|
||||
'fusion-woodoo-settings'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
self::OPTION_ODOO_URL,
|
||||
__('Odoo URL', 'fusion-woodoo'),
|
||||
[$this, 'field_odoo_url'],
|
||||
'fusion-woodoo-settings',
|
||||
'fusion_woodoo_connection'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
self::OPTION_API_KEY,
|
||||
__('API Key', 'fusion-woodoo'),
|
||||
[$this, 'field_api_key'],
|
||||
'fusion-woodoo-settings',
|
||||
'fusion_woodoo_connection'
|
||||
);
|
||||
|
||||
add_settings_section(
|
||||
'fusion_woodoo_portal',
|
||||
__('Customer Portal Tabs', 'fusion-woodoo'),
|
||||
null,
|
||||
'fusion-woodoo-settings'
|
||||
);
|
||||
|
||||
$toggles = [
|
||||
self::OPTION_SHOW_ORDERS => __('Show Sales Orders tab', 'fusion-woodoo'),
|
||||
self::OPTION_SHOW_INVOICES => __('Show Invoices tab', 'fusion-woodoo'),
|
||||
self::OPTION_SHOW_DELIVERIES => __('Show Deliveries tab', 'fusion-woodoo'),
|
||||
self::OPTION_SHOW_RETURNS => __('Show Returns tab', 'fusion-woodoo'),
|
||||
];
|
||||
foreach ($toggles as $option => $label) {
|
||||
add_settings_field(
|
||||
$option,
|
||||
$label,
|
||||
[$this, 'field_checkbox'],
|
||||
'fusion-woodoo-settings',
|
||||
'fusion_woodoo_portal',
|
||||
['option' => $option]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function field_odoo_url(): void {
|
||||
$value = esc_attr(get_option(self::OPTION_ODOO_URL, ''));
|
||||
echo '<input type="url" name="' . self::OPTION_ODOO_URL . '" value="' . $value . '" class="regular-text" placeholder="https://erp.example.com" />';
|
||||
echo '<p class="description">' . __('Base URL of your Odoo instance — no trailing slash.', 'fusion-woodoo') . '</p>';
|
||||
}
|
||||
|
||||
public function field_api_key(): void {
|
||||
$value = esc_attr(get_option(self::OPTION_API_KEY, ''));
|
||||
echo '<input type="password" name="' . self::OPTION_API_KEY . '" value="' . $value . '" class="regular-text" autocomplete="new-password" />';
|
||||
echo '<p class="description">' . __('API key from the Fusion WooCommerce Odoo module.', 'fusion-woodoo') . '</p>';
|
||||
}
|
||||
|
||||
public function field_checkbox(array $args): void {
|
||||
$option = $args['option'];
|
||||
$checked = checked(1, get_option($option, 1), false);
|
||||
echo '<label><input type="checkbox" name="' . esc_attr($option) . '" value="1" ' . $checked . ' /> ' . __('Enabled', 'fusion-woodoo') . '</label>';
|
||||
}
|
||||
|
||||
public function render_settings_page(): void {
|
||||
$template = FUSION_WOODOO_PLUGIN_DIR . 'templates/admin/settings.php';
|
||||
if (file_exists($template)) {
|
||||
include $template;
|
||||
}
|
||||
}
|
||||
|
||||
public function on_url_changed(): void {
|
||||
if (class_exists('Fusion_WooDoo_Webhooks')) {
|
||||
Fusion_WooDoo_Webhooks::unregister_all();
|
||||
(new Fusion_WooDoo_Webhooks())->register_webhooks();
|
||||
}
|
||||
}
|
||||
|
||||
public function ajax_test_connection(): void {
|
||||
check_ajax_referer('fusion_woodoo_admin_nonce', 'nonce');
|
||||
if (!current_user_can('manage_woocommerce')) {
|
||||
wp_send_json_error(['message' => __('Permission denied.', 'fusion-woodoo')]);
|
||||
}
|
||||
$client = new Fusion_WooDoo_API_Client();
|
||||
$result = $client->test_connection();
|
||||
if ($result['success']) {
|
||||
wp_send_json_success(['message' => __('Connection successful! Odoo is reachable.', 'fusion-woodoo')]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => $result['error'] ?: __('Connection failed.', 'fusion-woodoo')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Fusion_WooDoo_API_Client {
|
||||
|
||||
private string $odoo_url;
|
||||
private string $api_key;
|
||||
|
||||
public function __construct() {
|
||||
$this->odoo_url = rtrim(get_option('fusion_woodoo_odoo_url', ''), '/');
|
||||
$this->api_key = get_option('fusion_woodoo_api_key', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a POST request to an Odoo endpoint.
|
||||
*
|
||||
* @param string $endpoint e.g. '/woo/api/order/status'
|
||||
* @param array $data
|
||||
* @return array{success: bool, data: mixed, error: string}
|
||||
*/
|
||||
public function request(string $endpoint, array $data = []): array {
|
||||
if (empty($this->odoo_url) || empty($this->api_key)) {
|
||||
return ['success' => false, 'data' => null, 'error' => 'Odoo URL or API key not configured.'];
|
||||
}
|
||||
|
||||
$url = $this->odoo_url . $endpoint;
|
||||
$response = wp_remote_post($url, [
|
||||
'timeout' => 15,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $this->api_key,
|
||||
],
|
||||
'body' => wp_json_encode($data),
|
||||
'data_format' => 'body',
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return ['success' => false, 'data' => null, 'error' => $response->get_error_message()];
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if ($code >= 200 && $code < 300) {
|
||||
return ['success' => true, 'data' => $body, 'error' => ''];
|
||||
}
|
||||
|
||||
$error_msg = $body['message'] ?? $body['error'] ?? 'Unexpected response from Odoo (HTTP ' . $code . ').';
|
||||
return ['success' => false, 'data' => $body, 'error' => $error_msg];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connectivity to Odoo.
|
||||
*/
|
||||
public function test_connection(): array {
|
||||
return $this->request('/woo/api/order/status', ['ping' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order status from Odoo.
|
||||
*
|
||||
* @param int|string $order_id WooCommerce order ID
|
||||
*/
|
||||
public function get_order_status(int|string $order_id): array {
|
||||
return $this->request('/woo/api/order/status', ['order_id' => $order_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a return request to Odoo.
|
||||
*
|
||||
* @param int|string $order_id
|
||||
* @param array $items [['product_id' => ..., 'qty' => ...], ...]
|
||||
* @param string $reason
|
||||
*/
|
||||
public function submit_return(int|string $order_id, array $items, string $reason): array {
|
||||
return $this->request('/woo/api/return/create', [
|
||||
'order_id' => $order_id,
|
||||
'items' => $items,
|
||||
'reason' => $reason,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch existing returns for a customer.
|
||||
*
|
||||
* @param int|string $customer_id WooCommerce user ID
|
||||
*/
|
||||
public function get_returns(int|string $customer_id): array {
|
||||
return $this->request('/woo/api/return/list', ['customer_id' => $customer_id]);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Fusion_WooDoo {
|
||||
|
||||
private static ?Fusion_WooDoo $instance = null;
|
||||
|
||||
public static function instance(): Fusion_WooDoo {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
$this->load_includes();
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
private function load_includes(): void {
|
||||
$includes = [
|
||||
'class-api-client.php',
|
||||
'class-admin-settings.php',
|
||||
'class-rest-endpoints.php',
|
||||
'class-webhooks.php',
|
||||
'class-my-account.php',
|
||||
'class-order-timeline.php',
|
||||
'class-returns.php',
|
||||
];
|
||||
foreach ($includes as $file) {
|
||||
$path = FUSION_WOODOO_PLUGIN_DIR . 'includes/' . $file;
|
||||
if (file_exists($path)) {
|
||||
require_once $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function init_hooks(): void {
|
||||
add_action('init', [$this, 'load_textdomain']);
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
|
||||
|
||||
// Instantiate components
|
||||
new Fusion_WooDoo_Admin_Settings();
|
||||
new Fusion_WooDoo_REST_Endpoints();
|
||||
new Fusion_WooDoo_Webhooks();
|
||||
new Fusion_WooDoo_My_Account();
|
||||
new Fusion_WooDoo_Order_Timeline();
|
||||
new Fusion_WooDoo_Returns();
|
||||
}
|
||||
|
||||
public function load_textdomain(): void {
|
||||
load_plugin_textdomain(
|
||||
'fusion-woodoo',
|
||||
false,
|
||||
dirname(plugin_basename(FUSION_WOODOO_PLUGIN_DIR . 'fusion-woodoo.php')) . '/languages'
|
||||
);
|
||||
}
|
||||
|
||||
public function enqueue_frontend_assets(): void {
|
||||
if (is_account_page() || is_woocommerce()) {
|
||||
wp_enqueue_style(
|
||||
'fusion-woodoo-my-account',
|
||||
FUSION_WOODOO_PLUGIN_URL . 'assets/css/my-account.css',
|
||||
[],
|
||||
FUSION_WOODOO_VERSION
|
||||
);
|
||||
wp_enqueue_script(
|
||||
'fusion-woodoo-my-account',
|
||||
FUSION_WOODOO_PLUGIN_URL . 'assets/js/my-account.js',
|
||||
['jquery'],
|
||||
FUSION_WOODOO_VERSION,
|
||||
true
|
||||
);
|
||||
wp_localize_script('fusion-woodoo-my-account', 'fusionWooDoo', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('fusion_woodoo_nonce'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function enqueue_admin_assets(string $hook): void {
|
||||
if (strpos($hook, 'fusion-woodoo') === false) {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_style(
|
||||
'fusion-woodoo-admin',
|
||||
FUSION_WOODOO_PLUGIN_URL . 'assets/css/admin.css',
|
||||
[],
|
||||
FUSION_WOODOO_VERSION
|
||||
);
|
||||
wp_enqueue_script(
|
||||
'fusion-woodoo-admin',
|
||||
FUSION_WOODOO_PLUGIN_URL . 'assets/js/admin.js',
|
||||
['jquery'],
|
||||
FUSION_WOODOO_VERSION,
|
||||
true
|
||||
);
|
||||
wp_localize_script('fusion-woodoo-admin', 'fusionWooDooAdmin', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('fusion_woodoo_admin_nonce'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Fusion_WooDoo_My_Account {
|
||||
|
||||
const ENDPOINTS = [
|
||||
'odoo-sales-orders' => 'Sales Orders',
|
||||
'odoo-invoices' => 'Invoices',
|
||||
'odoo-deliveries' => 'Deliveries',
|
||||
'odoo-returns' => 'Returns',
|
||||
];
|
||||
|
||||
const OPTION_MAP = [
|
||||
'odoo-sales-orders' => 'fusion_woodoo_show_orders',
|
||||
'odoo-invoices' => 'fusion_woodoo_show_invoices',
|
||||
'odoo-deliveries' => 'fusion_woodoo_show_deliveries',
|
||||
'odoo-returns' => 'fusion_woodoo_show_returns',
|
||||
];
|
||||
|
||||
const TEMPLATE_MAP = [
|
||||
'odoo-sales-orders' => 'sales-orders.php',
|
||||
'odoo-invoices' => 'invoices.php',
|
||||
'odoo-deliveries' => 'deliveries.php',
|
||||
'odoo-returns' => 'returns.php',
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
add_action('init', [$this, 'add_rewrite_endpoints']);
|
||||
add_filter('woocommerce_account_menu_items', [$this, 'add_menu_items']);
|
||||
foreach (array_keys(self::ENDPOINTS) as $endpoint) {
|
||||
add_action('woocommerce_account_' . $endpoint . '_endpoint', [$this, 'render_endpoint_content']);
|
||||
}
|
||||
// AJAX handler for PDF downloads
|
||||
add_action('wp_ajax_fusion_woodoo_download_pdf', [$this, 'ajax_download_pdf']);
|
||||
}
|
||||
|
||||
public function add_rewrite_endpoints(): void {
|
||||
foreach (array_keys(self::ENDPOINTS) as $endpoint) {
|
||||
add_rewrite_endpoint($endpoint, EP_ROOT | EP_PAGES);
|
||||
}
|
||||
}
|
||||
|
||||
public function add_menu_items(array $items): array {
|
||||
$new_items = [];
|
||||
foreach ($items as $key => $label) {
|
||||
$new_items[$key] = $label;
|
||||
// Insert Odoo tabs after 'orders'
|
||||
if ($key === 'orders') {
|
||||
foreach (self::ENDPOINTS as $endpoint => $title) {
|
||||
if (get_option(self::OPTION_MAP[$endpoint], 1)) {
|
||||
$new_items[$endpoint] = __($title, 'fusion-woodoo');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $new_items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic renderer — dispatches to the correct template.
|
||||
*/
|
||||
public function render_endpoint_content(): void {
|
||||
// Determine which endpoint triggered this action
|
||||
$current = null;
|
||||
foreach (array_keys(self::ENDPOINTS) as $endpoint) {
|
||||
if (did_action('woocommerce_account_' . $endpoint . '_endpoint')) {
|
||||
$current = $endpoint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$current || !isset(self::TEMPLATE_MAP[$current])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$template = FUSION_WOODOO_PLUGIN_DIR . 'templates/my-account/' . self::TEMPLATE_MAP[$current];
|
||||
if (file_exists($template)) {
|
||||
include $template;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: serve a stored PDF file to the logged-in customer who owns the order.
|
||||
*/
|
||||
public function ajax_download_pdf(): void {
|
||||
check_ajax_referer('fusion_woodoo_nonce', 'nonce');
|
||||
|
||||
$order_id = (int) ($_GET['order_id'] ?? 0);
|
||||
$type = sanitize_key($_GET['type'] ?? 'invoice');
|
||||
|
||||
if (!$order_id || !is_user_logged_in()) {
|
||||
wp_die(__('Access denied.', 'fusion-woodoo'), 403);
|
||||
}
|
||||
|
||||
$order = wc_get_order($order_id);
|
||||
if (!$order || $order->get_customer_id() !== get_current_user_id()) {
|
||||
wp_die(__('Access denied.', 'fusion-woodoo'), 403);
|
||||
}
|
||||
|
||||
$meta_key = $type === 'delivery' ? '_odoo_delivery_pdf' : '_odoo_invoice_pdf';
|
||||
$rel_path = $order->get_meta($meta_key);
|
||||
|
||||
if (empty($rel_path)) {
|
||||
wp_die(__('PDF not found.', 'fusion-woodoo'), 404);
|
||||
}
|
||||
|
||||
$upload = wp_upload_dir();
|
||||
$file_path = $upload['basedir'] . $rel_path;
|
||||
|
||||
if (!file_exists($file_path) || !is_file($file_path)) {
|
||||
wp_die(__('PDF file not found on server.', 'fusion-woodoo'), 404);
|
||||
}
|
||||
|
||||
// Security: ensure path stays within uploads dir
|
||||
$real_base = realpath($upload['basedir']);
|
||||
$real_file = realpath($file_path);
|
||||
if (!$real_file || strpos($real_file, $real_base) !== 0) {
|
||||
wp_die(__('Access denied.', 'fusion-woodoo'), 403);
|
||||
}
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: attachment; filename="' . basename($real_file) . '"');
|
||||
header('Content-Length: ' . filesize($real_file));
|
||||
readfile($real_file);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Fusion_WooDoo_Order_Timeline {
|
||||
|
||||
/**
|
||||
* Timeline stages in order. Key = _odoo_order_status value(s) that map to this stage.
|
||||
*/
|
||||
const STAGES = [
|
||||
'confirmed' => 'Confirmed',
|
||||
'processing' => 'Processing',
|
||||
'shipped' => 'Shipped',
|
||||
'delivered' => 'Delivered',
|
||||
'done' => 'Completed',
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
add_action('woocommerce_order_details_after_order_table', [$this, 'render_timeline']);
|
||||
}
|
||||
|
||||
public function render_timeline(WC_Order $order): void {
|
||||
$odoo_status = strtolower((string) $order->get_meta('_odoo_order_status'));
|
||||
|
||||
if (empty($odoo_status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tracking_number = $order->get_meta('_odoo_tracking_number');
|
||||
$shipping_carrier = $order->get_meta('_odoo_shipping_carrier');
|
||||
|
||||
$stage_keys = array_keys(self::STAGES);
|
||||
$current_index = array_search($odoo_status, $stage_keys);
|
||||
if ($current_index === false) {
|
||||
$current_index = 0;
|
||||
}
|
||||
|
||||
$template = FUSION_WOODOO_PLUGIN_DIR . 'templates/my-account/order-timeline.php';
|
||||
if (file_exists($template)) {
|
||||
include $template;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Fusion_WooDoo_REST_Endpoints {
|
||||
|
||||
public function __construct() {
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
$namespace = 'fusion-woodoo/v1';
|
||||
|
||||
register_rest_route($namespace, '/order/update', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'handle_order_update'],
|
||||
'permission_callback' => [$this, 'verify_api_key'],
|
||||
'args' => [
|
||||
'order_id' => ['required' => true, 'validate_callback' => 'is_numeric'],
|
||||
'status' => ['required' => false, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'tracking_number' => ['required' => false, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'shipping_carrier' => ['required' => false, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route($namespace, '/order/invoice', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'handle_order_invoice'],
|
||||
'permission_callback' => [$this, 'verify_api_key'],
|
||||
'args' => [
|
||||
'order_id' => ['required' => true, 'validate_callback' => 'is_numeric'],
|
||||
'pdf_data' => ['required' => true],
|
||||
'filename' => ['required' => false, 'sanitize_callback' => 'sanitize_file_name'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route($namespace, '/order/delivery', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'handle_order_delivery'],
|
||||
'permission_callback' => [$this, 'verify_api_key'],
|
||||
'args' => [
|
||||
'order_id' => ['required' => true, 'validate_callback' => 'is_numeric'],
|
||||
'pdf_data' => ['required' => true],
|
||||
'filename' => ['required' => false, 'sanitize_callback' => 'sanitize_file_name'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route($namespace, '/order/messages', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'handle_order_messages'],
|
||||
'permission_callback' => [$this, 'verify_api_key'],
|
||||
'args' => [
|
||||
'order_id' => ['required' => true, 'validate_callback' => 'is_numeric'],
|
||||
'messages' => ['required' => true],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Bearer token from Authorization header.
|
||||
*/
|
||||
public function verify_api_key(WP_REST_Request $request): bool|WP_Error {
|
||||
$auth_header = $request->get_header('authorization');
|
||||
if (empty($auth_header) || !str_starts_with($auth_header, 'Bearer ')) {
|
||||
return new WP_Error('missing_auth', __('Authorization header missing or malformed.', 'fusion-woodoo'), ['status' => 401]);
|
||||
}
|
||||
|
||||
$provided_key = trim(substr($auth_header, 7));
|
||||
$stored_key = get_option('fusion_woodoo_api_key', '');
|
||||
|
||||
if (empty($stored_key) || !hash_equals($stored_key, $provided_key)) {
|
||||
return new WP_Error('invalid_api_key', __('Invalid API key.', 'fusion-woodoo'), ['status' => 403]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /order/update — update order status and tracking info.
|
||||
*/
|
||||
public function handle_order_update(WP_REST_Request $request): WP_REST_Response|WP_Error {
|
||||
$order_id = (int) $request->get_param('order_id');
|
||||
$order = wc_get_order($order_id);
|
||||
|
||||
if (!$order) {
|
||||
return new WP_Error('order_not_found', __('Order not found.', 'fusion-woodoo'), ['status' => 404]);
|
||||
}
|
||||
|
||||
if ($status = $request->get_param('status')) {
|
||||
$order->update_meta_data('_odoo_order_status', sanitize_text_field($status));
|
||||
}
|
||||
if ($tracking = $request->get_param('tracking_number')) {
|
||||
$order->update_meta_data('_odoo_tracking_number', sanitize_text_field($tracking));
|
||||
}
|
||||
if ($carrier = $request->get_param('shipping_carrier')) {
|
||||
$order->update_meta_data('_odoo_shipping_carrier', sanitize_text_field($carrier));
|
||||
}
|
||||
|
||||
$order->save();
|
||||
|
||||
return rest_ensure_response(['success' => true, 'order_id' => $order_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /order/invoice — store base64-encoded invoice PDF.
|
||||
*/
|
||||
public function handle_order_invoice(WP_REST_Request $request): WP_REST_Response|WP_Error {
|
||||
return $this->save_pdf($request, 'invoices', '_odoo_invoice_pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /order/delivery — store base64-encoded delivery PDF.
|
||||
*/
|
||||
public function handle_order_delivery(WP_REST_Request $request): WP_REST_Response|WP_Error {
|
||||
return $this->save_pdf($request, 'deliveries', '_odoo_delivery_pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /order/messages — store Odoo messages array against the order.
|
||||
*/
|
||||
public function handle_order_messages(WP_REST_Request $request): WP_REST_Response|WP_Error {
|
||||
$order_id = (int) $request->get_param('order_id');
|
||||
$order = wc_get_order($order_id);
|
||||
|
||||
if (!$order) {
|
||||
return new WP_Error('order_not_found', __('Order not found.', 'fusion-woodoo'), ['status' => 404]);
|
||||
}
|
||||
|
||||
$messages = $request->get_param('messages');
|
||||
if (!is_array($messages)) {
|
||||
return new WP_Error('invalid_messages', __('Messages must be a JSON array.', 'fusion-woodoo'), ['status' => 400]);
|
||||
}
|
||||
|
||||
// Sanitize each message entry
|
||||
$sanitized = array_map(function($msg) {
|
||||
return [
|
||||
'author' => sanitize_text_field($msg['author'] ?? ''),
|
||||
'date' => sanitize_text_field($msg['date'] ?? ''),
|
||||
'body' => wp_kses_post($msg['body'] ?? ''),
|
||||
'type' => sanitize_text_field($msg['type'] ?? 'note'),
|
||||
];
|
||||
}, $messages);
|
||||
|
||||
$order->update_meta_data('_odoo_messages', $sanitized);
|
||||
$order->save();
|
||||
|
||||
return rest_ensure_response(['success' => true, 'count' => count($sanitized)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared logic to decode and store a PDF file.
|
||||
*/
|
||||
private function save_pdf(WP_REST_Request $request, string $folder, string $meta_key): WP_REST_Response|WP_Error {
|
||||
$order_id = (int) $request->get_param('order_id');
|
||||
$order = wc_get_order($order_id);
|
||||
|
||||
if (!$order) {
|
||||
return new WP_Error('order_not_found', __('Order not found.', 'fusion-woodoo'), ['status' => 404]);
|
||||
}
|
||||
|
||||
$pdf_data = $request->get_param('pdf_data');
|
||||
$decoded = base64_decode($pdf_data, true);
|
||||
if ($decoded === false) {
|
||||
return new WP_Error('invalid_pdf', __('Invalid base64-encoded PDF data.', 'fusion-woodoo'), ['status' => 400]);
|
||||
}
|
||||
|
||||
$filename = $request->get_param('filename') ?: ($folder . '-' . $order_id . '-' . time() . '.pdf');
|
||||
$filename = sanitize_file_name($filename);
|
||||
$upload = wp_upload_dir();
|
||||
$save_dir = $upload['basedir'] . '/fusion-woodoo/' . $folder . '/';
|
||||
|
||||
if (!file_exists($save_dir)) {
|
||||
wp_mkdir_p($save_dir);
|
||||
file_put_contents($save_dir . '.htaccess', 'deny from all');
|
||||
}
|
||||
|
||||
$file_path = $save_dir . $filename;
|
||||
$bytes = file_put_contents($file_path, $decoded);
|
||||
|
||||
if ($bytes === false) {
|
||||
return new WP_Error('file_write_error', __('Could not save PDF to disk.', 'fusion-woodoo'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$relative = str_replace($upload['basedir'], '', $file_path);
|
||||
$order->update_meta_data($meta_key, $relative);
|
||||
$order->save();
|
||||
|
||||
return rest_ensure_response(['success' => true, 'path' => $relative]);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Fusion_WooDoo_Returns {
|
||||
|
||||
public function __construct() {
|
||||
add_action('wp_ajax_fusion_woodoo_submit_return', [$this, 'ajax_submit_return']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last N orders for the current customer.
|
||||
*
|
||||
* @param int $limit
|
||||
* @return WC_Order[]
|
||||
*/
|
||||
public function get_customer_orders(int $limit = 20): array {
|
||||
if (!is_user_logged_in()) {
|
||||
return [];
|
||||
}
|
||||
return wc_get_orders([
|
||||
'customer' => get_current_user_id(),
|
||||
'limit' => $limit,
|
||||
'status' => ['completed', 'processing', 'on-hold'],
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch existing returns for the current customer from Odoo.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_existing_returns(): array {
|
||||
if (!is_user_logged_in()) {
|
||||
return [];
|
||||
}
|
||||
$client = new Fusion_WooDoo_API_Client();
|
||||
$result = $client->get_returns(get_current_user_id());
|
||||
return $result['success'] ? ($result['data']['returns'] ?? []) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for return form submission.
|
||||
*/
|
||||
public function ajax_submit_return(): void {
|
||||
check_ajax_referer('fusion_woodoo_nonce', 'nonce');
|
||||
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('You must be logged in to submit a return.', 'fusion-woodoo')]);
|
||||
}
|
||||
|
||||
$order_id = (int) ($_POST['order_id'] ?? 0);
|
||||
$reason = sanitize_textarea_field($_POST['reason'] ?? '');
|
||||
$items = $_POST['items'] ?? [];
|
||||
|
||||
if (!$order_id) {
|
||||
wp_send_json_error(['message' => __('Please select an order.', 'fusion-woodoo')]);
|
||||
}
|
||||
|
||||
if (empty($items) || !is_array($items)) {
|
||||
wp_send_json_error(['message' => __('Please select at least one item to return.', 'fusion-woodoo')]);
|
||||
}
|
||||
|
||||
if (empty($reason)) {
|
||||
wp_send_json_error(['message' => __('Please provide a reason for the return.', 'fusion-woodoo')]);
|
||||
}
|
||||
|
||||
// Verify the order belongs to the current user
|
||||
$order = wc_get_order($order_id);
|
||||
if (!$order || $order->get_customer_id() !== get_current_user_id()) {
|
||||
wp_send_json_error(['message' => __('Order not found or access denied.', 'fusion-woodoo')]);
|
||||
}
|
||||
|
||||
// Sanitize and validate items
|
||||
$sanitized_items = [];
|
||||
foreach ($items as $item) {
|
||||
$product_id = (int) ($item['product_id'] ?? 0);
|
||||
$qty = max(1, (int) ($item['qty'] ?? 1));
|
||||
if ($product_id > 0) {
|
||||
$sanitized_items[] = ['product_id' => $product_id, 'qty' => $qty];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($sanitized_items)) {
|
||||
wp_send_json_error(['message' => __('No valid items selected for return.', 'fusion-woodoo')]);
|
||||
}
|
||||
|
||||
$client = new Fusion_WooDoo_API_Client();
|
||||
$result = $client->submit_return($order_id, $sanitized_items, $reason);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success([
|
||||
'message' => __('Return request submitted successfully. Our team will review it shortly.', 'fusion-woodoo'),
|
||||
'return_id' => $result['data']['return_id'] ?? null,
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error([
|
||||
'message' => $result['error'] ?: __('Failed to submit return. Please try again or contact support.', 'fusion-woodoo'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Fusion_WooDoo_Webhooks {
|
||||
|
||||
const WEBHOOK_PREFIX = 'fusion-woodoo-';
|
||||
|
||||
public function __construct() {
|
||||
add_action('woocommerce_settings_saved', [$this, 'maybe_register']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all required WooCommerce webhooks pointing at the configured Odoo URL.
|
||||
*/
|
||||
public function register_webhooks(): void {
|
||||
$odoo_url = rtrim(get_option('fusion_woodoo_odoo_url', ''), '/');
|
||||
if (empty($odoo_url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$webhooks = [
|
||||
[
|
||||
'name' => self::WEBHOOK_PREFIX . 'order-created',
|
||||
'topic' => 'order.created',
|
||||
'api_url' => $odoo_url . '/woo/webhook/order',
|
||||
],
|
||||
[
|
||||
'name' => self::WEBHOOK_PREFIX . 'order-updated',
|
||||
'topic' => 'order.updated',
|
||||
'api_url' => $odoo_url . '/woo/webhook/order',
|
||||
],
|
||||
[
|
||||
'name' => self::WEBHOOK_PREFIX . 'product-updated',
|
||||
'topic' => 'product.updated',
|
||||
'api_url' => $odoo_url . '/woo/webhook/product',
|
||||
],
|
||||
[
|
||||
'name' => self::WEBHOOK_PREFIX . 'customer-created',
|
||||
'topic' => 'customer.created',
|
||||
'api_url' => $odoo_url . '/woo/webhook/customer',
|
||||
],
|
||||
[
|
||||
'name' => self::WEBHOOK_PREFIX . 'customer-updated',
|
||||
'topic' => 'customer.updated',
|
||||
'api_url' => $odoo_url . '/woo/webhook/customer',
|
||||
],
|
||||
];
|
||||
|
||||
// Remove stale webhooks first to avoid duplicates
|
||||
self::unregister_all();
|
||||
|
||||
foreach ($webhooks as $wh_data) {
|
||||
$webhook = new WC_Webhook();
|
||||
$webhook->set_name($wh_data['name']);
|
||||
$webhook->set_topic($wh_data['topic']);
|
||||
$webhook->set_delivery_url($wh_data['api_url']);
|
||||
$webhook->set_secret(get_option('fusion_woodoo_api_key', wp_generate_password(32)));
|
||||
$webhook->set_status('active');
|
||||
$webhook->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all fusion-woodoo webhooks from WooCommerce.
|
||||
*/
|
||||
public static function unregister_all(): void {
|
||||
$data_store = WC_Data_Store::load('webhook');
|
||||
$all_ids = $data_store->get_webhooks_ids('active');
|
||||
|
||||
foreach ($all_ids as $id) {
|
||||
$webhook = wc_get_webhook($id);
|
||||
if ($webhook && str_starts_with($webhook->get_name(), self::WEBHOOK_PREFIX)) {
|
||||
$webhook->delete(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping the Odoo URL and return whether it is reachable.
|
||||
*/
|
||||
public function check_stale(): bool {
|
||||
$odoo_url = get_option('fusion_woodoo_odoo_url', '');
|
||||
if (empty($odoo_url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = wp_remote_get(rtrim($odoo_url, '/') . '/web/health', [
|
||||
'timeout' => 10,
|
||||
'sslverify' => apply_filters('fusion_woodoo_sslverify', true),
|
||||
]);
|
||||
|
||||
return !is_wp_error($response) && wp_remote_retrieve_response_code($response) < 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-register webhooks when Odoo URL changes (triggered from settings).
|
||||
*/
|
||||
public function maybe_register(): void {
|
||||
if (
|
||||
isset($_POST[Fusion_WooDoo_Admin_Settings::OPTION_ODOO_URL]) &&
|
||||
!empty($_POST[Fusion_WooDoo_Admin_Settings::OPTION_ODOO_URL])
|
||||
) {
|
||||
$this->register_webhooks();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user