commit b64397dcd36c130f01a0f202cb4585fd787be506 Author: Malin Date: Tue Feb 17 09:59:53 2026 +0100 feat: WooCommerce Business Central integration plugin Native PHP plugin (no Composer) that syncs: - Product stock and pricing from BC to WooCommerce (scheduled cron) - Orders from WooCommerce to BC (on payment received) - Auto-creates customers in BC from WooCommerce billing data Product matching: WooCommerce SKU → BC Item Number, fallback to GTIN (EAN). OAuth2 client credentials auth with encrypted secret storage. Admin settings page with connection test, manual sync, and log viewer. Co-Authored-By: Claude Opus 4.6 diff --git a/woo-business-central/admin/class-wbc-admin.php b/woo-business-central/admin/class-wbc-admin.php new file mode 100644 index 0000000..3df3c4c --- /dev/null +++ b/woo-business-central/admin/class-wbc-admin.php @@ -0,0 +1,332 @@ + 'sanitize_text_field', + ) ); + register_setting( 'wbc_settings', 'wbc_client_id', array( + 'sanitize_callback' => 'sanitize_text_field', + ) ); + register_setting( 'wbc_settings', 'wbc_client_secret', array( + 'sanitize_callback' => array( $this, 'sanitize_client_secret' ), + ) ); + register_setting( 'wbc_settings', 'wbc_environment', array( + 'sanitize_callback' => 'sanitize_text_field', + ) ); + register_setting( 'wbc_settings', 'wbc_company_id', array( + 'sanitize_callback' => 'sanitize_text_field', + ) ); + + // Sync settings + register_setting( 'wbc_settings', 'wbc_sync_frequency', array( + 'sanitize_callback' => array( $this, 'sanitize_frequency' ), + ) ); + register_setting( 'wbc_settings', 'wbc_enable_stock_sync', array( + 'sanitize_callback' => array( $this, 'sanitize_checkbox' ), + ) ); + register_setting( 'wbc_settings', 'wbc_enable_price_sync', array( + 'sanitize_callback' => array( $this, 'sanitize_checkbox' ), + ) ); + + // Order settings + register_setting( 'wbc_settings', 'wbc_enable_order_sync', array( + 'sanitize_callback' => array( $this, 'sanitize_checkbox' ), + ) ); + register_setting( 'wbc_settings', 'wbc_default_payment_terms_id', array( + 'sanitize_callback' => 'sanitize_text_field', + ) ); + register_setting( 'wbc_settings', 'wbc_default_shipment_method_id', array( + 'sanitize_callback' => 'sanitize_text_field', + ) ); + register_setting( 'wbc_settings', 'wbc_shipping_item_number', array( + 'sanitize_callback' => 'sanitize_text_field', + ) ); + } + + /** + * Sanitize client secret + * + * @param string $value Input value. + * @return string Sanitized value. + */ + public function sanitize_client_secret( $value ) { + if ( empty( $value ) ) { + // Keep existing value if empty (masked field) + return get_option( 'wbc_client_secret', '' ); + } + + // If it looks like a masked value, keep existing + if ( strpos( $value, '***' ) !== false ) { + return get_option( 'wbc_client_secret', '' ); + } + + // Encrypt new value + return WBC_OAuth::encrypt( sanitize_text_field( $value ) ); + } + + /** + * Sanitize frequency + * + * @param string $value Input value. + * @return string Sanitized value. + */ + public function sanitize_frequency( $value ) { + $allowed = array( 'hourly', 'twice_daily', 'daily' ); + $value = sanitize_text_field( $value ); + + if ( ! in_array( $value, $allowed, true ) ) { + return 'daily'; + } + + // Reschedule cron if frequency changed + $old_frequency = get_option( 'wbc_sync_frequency', 'daily' ); + if ( $value !== $old_frequency ) { + WBC_Cron::reschedule_sync( $value ); + } + + return $value; + } + + /** + * Sanitize checkbox + * + * @param string $value Input value. + * @return string Sanitized value. + */ + public function sanitize_checkbox( $value ) { + return $value === 'yes' ? 'yes' : 'no'; + } + + /** + * Enqueue admin scripts + * + * @param string $hook Current admin page hook. + */ + public function enqueue_scripts( $hook ) { + if ( strpos( $hook, self::PAGE_SLUG ) === false ) { + return; + } + + wp_enqueue_style( + 'wbc-admin-style', + WBC_PLUGIN_URL . 'admin/css/wbc-admin.css', + array(), + WBC_VERSION + ); + + wp_enqueue_script( + 'wbc-admin-script', + WBC_PLUGIN_URL . 'admin/js/wbc-admin.js', + array( 'jquery' ), + WBC_VERSION, + true + ); + + wp_localize_script( 'wbc-admin-script', 'wbc_admin', array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'wbc_admin_nonce' ), + 'strings' => array( + 'testing' => __( 'Testing connection...', 'woo-business-central' ), + 'syncing' => __( 'Syncing products...', 'woo-business-central' ), + 'clearing' => __( 'Clearing logs...', 'woo-business-central' ), + 'loading' => __( 'Loading...', 'woo-business-central' ), + 'confirm_clear' => __( 'Are you sure you want to clear all logs?', 'woo-business-central' ), + ), + ) ); + } + + /** + * Add settings link to plugin list + * + * @param array $links Existing links. + * @return array Modified links. + */ + public function add_settings_link( $links ) { + $settings_link = sprintf( + '%s', + admin_url( 'admin.php?page=' . self::PAGE_SLUG ), + __( 'Settings', 'woo-business-central' ) + ); + + array_unshift( $links, $settings_link ); + + return $links; + } + + /** + * Render settings page + */ + public function render_settings_page() { + // Check user capabilities + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_die( __( 'You do not have sufficient permissions to access this page.', 'woo-business-central' ) ); + } + + // Handle tab + $current_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'connection'; + + include WBC_PLUGIN_DIR . 'admin/partials/wbc-admin-display.php'; + } + + /** + * AJAX: Test connection + */ + public function ajax_test_connection() { + check_ajax_referer( 'wbc_admin_nonce', 'nonce' ); + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'woo-business-central' ) ) ); + } + + $result = WBC_OAuth::test_connection(); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( $result ); + } + + /** + * AJAX: Manual sync + */ + public function ajax_manual_sync() { + check_ajax_referer( 'wbc_admin_nonce', 'nonce' ); + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'woo-business-central' ) ) ); + } + + $result = WBC_Cron::run_sync_now(); + + if ( isset( $result['success'] ) && $result['success'] ) { + wp_send_json_success( $result ); + } else { + wp_send_json_error( $result ); + } + } + + /** + * AJAX: Clear logs + */ + public function ajax_clear_logs() { + check_ajax_referer( 'wbc_admin_nonce', 'nonce' ); + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'woo-business-central' ) ) ); + } + + WBC_Logger::clear_logs(); + + wp_send_json_success( array( 'message' => __( 'Logs cleared successfully.', 'woo-business-central' ) ) ); + } + + /** + * AJAX: Get companies + */ + public function ajax_get_companies() { + check_ajax_referer( 'wbc_admin_nonce', 'nonce' ); + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'woo-business-central' ) ) ); + } + + $result = WBC_API_Client::get_companies(); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + $companies = isset( $result['value'] ) ? $result['value'] : array(); + + wp_send_json_success( array( 'companies' => $companies ) ); + } + + /** + * Handle CSV export of logs + */ + public function handle_csv_export() { + if ( ! isset( $_GET['wbc_export_logs'] ) || $_GET['wbc_export_logs'] !== '1' ) { + return; + } + + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wbc_export_logs' ) ) { + wp_die( esc_html__( 'Security check failed.', 'woo-business-central' ) ); + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_die( esc_html__( 'Permission denied.', 'woo-business-central' ) ); + } + + $csv = WBC_Logger::export_to_csv(); + + header( 'Content-Type: text/csv; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename=wbc-logs-' . gmdate( 'Y-m-d' ) . '.csv' ); + header( 'Pragma: no-cache' ); + header( 'Expires: 0' ); + + echo $csv; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV output + exit; + } + + /** + * Get logs for display + * + * @param array $args Query arguments. + * @return array Logs data. + */ + public static function get_logs_for_display( $args = array() ) { + $defaults = array( + 'limit' => 50, + 'offset' => 0, + 'level' => '', + ); + + $args = wp_parse_args( $args, $defaults ); + + return array( + 'logs' => WBC_Logger::get_logs( $args ), + 'total' => WBC_Logger::get_log_count( $args ), + ); + } +} diff --git a/woo-business-central/admin/css/wbc-admin.css b/woo-business-central/admin/css/wbc-admin.css new file mode 100644 index 0000000..19e2fc8 --- /dev/null +++ b/woo-business-central/admin/css/wbc-admin.css @@ -0,0 +1,233 @@ +/** + * WooCommerce Business Central - Admin Styles + */ + +/* General Layout */ +.wbc-admin-wrap { + max-width: 1200px; +} + +.wbc-nav-tabs { + margin-bottom: 0; +} + +/* Cards */ +.wbc-card { + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + padding: 20px; + margin-top: 20px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} + +.wbc-card h2 { + margin-top: 0; + padding-bottom: 10px; + border-bottom: 1px solid #eee; + font-size: 1.3em; +} + +.wbc-card h2:first-child { + margin-top: 0; +} + +/* Status Messages */ +.wbc-status { + display: inline-block; + margin-left: 10px; + font-weight: 500; +} + +.wbc-status.success { + color: #00a32a; +} + +.wbc-status.error { + color: #d63638; +} + +.wbc-status.loading { + color: #2271b1; +} + +/* Status Table */ +.wbc-status-table { + margin-bottom: 15px; +} + +.wbc-status-table th { + text-align: left; + padding: 8px 20px 8px 0; + font-weight: 600; +} + +.wbc-status-table td { + padding: 8px 0; +} + +/* Logs Table */ +.wbc-logs-table { + margin-top: 15px; +} + +.wbc-logs-table .column-timestamp { + width: 160px; +} + +.wbc-logs-table .column-level { + width: 90px; +} + +.wbc-logs-table .column-context { + width: 130px; +} + +.wbc-logs-table .column-message { + min-width: 300px; +} + +/* Log Level Badges */ +.wbc-log-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +.wbc-log-badge-debug { + background: #f0f0f1; + color: #50575e; +} + +.wbc-log-badge-info { + background: #d1e4f6; + color: #0a4b78; +} + +.wbc-log-badge-warning { + background: #fcf9e8; + color: #bd8600; +} + +.wbc-log-badge-error { + background: #facfd2; + color: #8a1f1f; +} + +/* Log Row Colors */ +.wbc-log-level-error { + background-color: #fff5f5 !important; +} + +.wbc-log-level-warning { + background-color: #fffbe8 !important; +} + +/* Log Data Display */ +.wbc-log-data { + background: #f6f7f7; + padding: 10px; + margin-top: 10px; + font-size: 12px; + max-height: 200px; + overflow: auto; + border: 1px solid #ddd; + border-radius: 3px; + white-space: pre-wrap; + word-break: break-word; +} + +/* Log Filters */ +.wbc-log-filters { + margin-bottom: 15px; + padding: 10px; + background: #f6f7f7; + border-radius: 3px; +} + +.wbc-log-filters label { + font-weight: 500; + margin-right: 10px; +} + +/* Toggle Data Link */ +.wbc-toggle-data { + margin-left: 10px; + font-size: 12px; +} + +/* Form Improvements */ +.wbc-settings-form .form-table th { + padding-left: 0; +} + +.wbc-settings-form .description { + color: #646970; + font-style: normal; + margin-top: 5px; +} + +/* Button Spacing */ +.wbc-card .button + .button, +.wbc-card .button-primary + .button { + margin-left: 10px; +} + +/* Connection Status */ +#wbc-connection-status { + vertical-align: middle; +} + +/* Companies List */ +#wbc-companies-list { + margin-top: 10px; + padding: 10px; + background: #f6f7f7; + border-radius: 3px; +} + +#wbc-company-select { + min-width: 300px; +} + +/* Responsive Adjustments */ +@media screen and (max-width: 782px) { + .wbc-logs-table .column-timestamp, + .wbc-logs-table .column-context { + display: none; + } + + .wbc-logs-table .column-level { + width: 70px; + } + + #wbc-company-select { + min-width: 100%; + } +} + +/* Loading State */ +.wbc-loading { + opacity: 0.5; + pointer-events: none; +} + +/* Pagination */ +.wbc-logs-table + .tablenav { + margin-top: 15px; +} + +.tablenav-pages .pagination-links { + display: flex; + align-items: center; + gap: 5px; +} + +.tablenav-pages .button { + padding: 0 8px; + min-height: 28px; + line-height: 26px; +} diff --git a/woo-business-central/admin/js/wbc-admin.js b/woo-business-central/admin/js/wbc-admin.js new file mode 100644 index 0000000..c5d6afe --- /dev/null +++ b/woo-business-central/admin/js/wbc-admin.js @@ -0,0 +1,239 @@ +/** + * WooCommerce Business Central - Admin JavaScript + */ + +(function($) { + 'use strict'; + + var WBC_Admin = { + /** + * Initialize + */ + init: function() { + this.bindEvents(); + }, + + /** + * Bind event handlers + */ + bindEvents: function() { + // Test connection + $('#wbc-test-connection').on('click', this.testConnection); + + // Manual sync + $('#wbc-manual-sync').on('click', this.manualSync); + + // Clear logs + $('#wbc-clear-logs').on('click', this.clearLogs); + + // Load companies + $('#wbc-load-companies').on('click', this.loadCompanies); + + // Select company + $('#wbc-company-select').on('change', this.selectCompany); + + // Toggle log data + $(document).on('click', '.wbc-toggle-data', this.toggleLogData); + }, + + /** + * Test connection to Business Central + */ + testConnection: function(e) { + e.preventDefault(); + + var $btn = $(this); + var $status = $('#wbc-connection-status'); + + $btn.prop('disabled', true); + $status.removeClass('success error').addClass('loading').text(wbc_admin.strings.testing); + + $.ajax({ + url: wbc_admin.ajax_url, + type: 'POST', + data: { + action: 'wbc_test_connection', + nonce: wbc_admin.nonce + }, + success: function(response) { + $btn.prop('disabled', false); + $status.removeClass('loading'); + + if (response.success) { + $status.addClass('success').text(response.data.message); + } else { + $status.addClass('error').text(response.data.message || 'Connection failed'); + } + }, + error: function(xhr, status, error) { + $btn.prop('disabled', false); + $status.removeClass('loading').addClass('error').text('Request failed: ' + error); + } + }); + }, + + /** + * Run manual sync + */ + manualSync: function(e) { + e.preventDefault(); + + var $btn = $(this); + var $status = $('#wbc-sync-status'); + + $btn.prop('disabled', true); + $status.removeClass('success error').addClass('loading').text(wbc_admin.strings.syncing); + + $.ajax({ + url: wbc_admin.ajax_url, + type: 'POST', + data: { + action: 'wbc_manual_sync', + nonce: wbc_admin.nonce + }, + success: function(response) { + $btn.prop('disabled', false); + $status.removeClass('loading'); + + if (response.success) { + $status.addClass('success').text(response.data.message); + } else { + $status.addClass('error').text(response.data.message || 'Sync failed'); + } + }, + error: function(xhr, status, error) { + $btn.prop('disabled', false); + $status.removeClass('loading').addClass('error').text('Request failed: ' + error); + } + }); + }, + + /** + * Clear all logs + */ + clearLogs: function(e) { + e.preventDefault(); + + if (!confirm(wbc_admin.strings.confirm_clear)) { + return; + } + + var $btn = $(this); + var originalText = $btn.text(); + + $btn.prop('disabled', true).text(wbc_admin.strings.clearing); + + $.ajax({ + url: wbc_admin.ajax_url, + type: 'POST', + data: { + action: 'wbc_clear_logs', + nonce: wbc_admin.nonce + }, + success: function(response) { + if (response.success) { + // Reload the page to show empty logs + location.reload(); + } else { + alert(response.data.message || 'Failed to clear logs'); + $btn.prop('disabled', false).text(originalText); + } + }, + error: function(xhr, status, error) { + alert('Request failed: ' + error); + $btn.prop('disabled', false).text(originalText); + } + }); + }, + + /** + * Load companies from Business Central + */ + loadCompanies: function(e) { + e.preventDefault(); + + var $btn = $(this); + var $select = $('#wbc-company-select'); + var $list = $('#wbc-companies-list'); + var originalText = $btn.text(); + + $btn.prop('disabled', true).text(wbc_admin.strings.loading); + + $.ajax({ + url: wbc_admin.ajax_url, + type: 'POST', + data: { + action: 'wbc_get_companies', + nonce: wbc_admin.nonce + }, + success: function(response) { + $btn.prop('disabled', false).text(originalText); + + if (response.success && response.data.companies) { + // Clear existing options except the first one + $select.find('option:not(:first)').remove(); + + // Add companies + $.each(response.data.companies, function(i, company) { + $select.append( + $('