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 <noreply@anthropic.com>
This commit is contained in:
332
woo-business-central/admin/class-wbc-admin.php
Normal file
332
woo-business-central/admin/class-wbc-admin.php
Normal file
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Settings for WooCommerce Business Central Integration
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_Admin
|
||||
*
|
||||
* Handles admin settings page and AJAX actions.
|
||||
*/
|
||||
class WBC_Admin {
|
||||
|
||||
/**
|
||||
* Settings page slug
|
||||
*/
|
||||
const PAGE_SLUG = 'wbc-settings';
|
||||
|
||||
/**
|
||||
* Add admin menu
|
||||
*/
|
||||
public function add_admin_menu() {
|
||||
add_submenu_page(
|
||||
'woocommerce',
|
||||
__( 'Business Central', 'woo-business-central' ),
|
||||
__( 'Business Central', 'woo-business-central' ),
|
||||
'manage_woocommerce',
|
||||
self::PAGE_SLUG,
|
||||
array( $this, 'render_settings_page' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register settings
|
||||
*/
|
||||
public function register_settings() {
|
||||
// Connection settings
|
||||
register_setting( 'wbc_settings', 'wbc_tenant_id', array(
|
||||
'sanitize_callback' => '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(
|
||||
'<a href="%s">%s</a>',
|
||||
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 ),
|
||||
);
|
||||
}
|
||||
}
|
||||
233
woo-business-central/admin/css/wbc-admin.css
Normal file
233
woo-business-central/admin/css/wbc-admin.css
Normal file
@@ -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;
|
||||
}
|
||||
239
woo-business-central/admin/js/wbc-admin.js
Normal file
239
woo-business-central/admin/js/wbc-admin.js
Normal file
@@ -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(
|
||||
$('<option>', {
|
||||
value: company.id,
|
||||
text: company.displayName + ' (' + company.id.substring(0, 8) + '...)'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Show the select
|
||||
$list.slideDown();
|
||||
|
||||
// Pre-select current company if set
|
||||
var currentCompany = $('#wbc_company_id').val();
|
||||
if (currentCompany) {
|
||||
$select.val(currentCompany);
|
||||
}
|
||||
} else {
|
||||
alert(response.data.message || 'Failed to load companies. Make sure credentials are saved and correct.');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$btn.prop('disabled', false).text(originalText);
|
||||
alert('Request failed: ' + error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Select company from dropdown
|
||||
*/
|
||||
selectCompany: function() {
|
||||
var companyId = $(this).val();
|
||||
if (companyId) {
|
||||
$('#wbc_company_id').val(companyId);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle log data visibility
|
||||
*/
|
||||
toggleLogData: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $btn = $(this);
|
||||
var $data = $btn.siblings('.wbc-log-data');
|
||||
|
||||
$data.slideToggle(200, function() {
|
||||
if ($data.is(':visible')) {
|
||||
$btn.text($btn.data('hide-text') || 'Hide data');
|
||||
} else {
|
||||
$btn.text($btn.data('show-text') || 'Show data');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready
|
||||
$(document).ready(function() {
|
||||
WBC_Admin.init();
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
399
woo-business-central/admin/partials/wbc-admin-display.php
Normal file
399
woo-business-central/admin/partials/wbc-admin-display.php
Normal file
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin settings page template
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$tabs = array(
|
||||
'connection' => __( 'Connection', 'woo-business-central' ),
|
||||
'sync' => __( 'Sync Settings', 'woo-business-central' ),
|
||||
'orders' => __( 'Order Settings', 'woo-business-central' ),
|
||||
'logs' => __( 'Logs', 'woo-business-central' ),
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="wrap wbc-admin-wrap">
|
||||
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
|
||||
|
||||
<nav class="nav-tab-wrapper wbc-nav-tabs">
|
||||
<?php foreach ( $tabs as $tab_id => $tab_name ) : ?>
|
||||
<a href="<?php echo esc_url( add_query_arg( 'tab', $tab_id ) ); ?>"
|
||||
class="nav-tab <?php echo $current_tab === $tab_id ? 'nav-tab-active' : ''; ?>">
|
||||
<?php echo esc_html( $tab_name ); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<div class="wbc-admin-content">
|
||||
<?php if ( $current_tab === 'connection' ) : ?>
|
||||
<!-- Connection Settings -->
|
||||
<form method="post" action="options.php" class="wbc-settings-form">
|
||||
<?php settings_fields( 'wbc_settings' ); ?>
|
||||
|
||||
<div class="wbc-card">
|
||||
<h2><?php esc_html_e( 'Microsoft Azure AD Credentials', 'woo-business-central' ); ?></h2>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Enter your Azure AD application credentials to connect to Business Central.', 'woo-business-central' ); ?>
|
||||
</p>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbc_tenant_id"><?php esc_html_e( 'Tenant ID', 'woo-business-central' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="wbc_tenant_id" name="wbc_tenant_id"
|
||||
value="<?php echo esc_attr( get_option( 'wbc_tenant_id', '' ) ); ?>"
|
||||
class="regular-text" />
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Your Azure AD tenant ID (GUID format).', 'woo-business-central' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbc_client_id"><?php esc_html_e( 'Client ID', 'woo-business-central' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="wbc_client_id" name="wbc_client_id"
|
||||
value="<?php echo esc_attr( get_option( 'wbc_client_id', '' ) ); ?>"
|
||||
class="regular-text" />
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Application (client) ID from Azure AD.', 'woo-business-central' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbc_client_secret"><?php esc_html_e( 'Client Secret', 'woo-business-central' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<?php
|
||||
$has_secret = ! empty( WBC_OAuth::get_client_secret() );
|
||||
$placeholder = $has_secret ? '********' : '';
|
||||
?>
|
||||
<input type="password" id="wbc_client_secret" name="wbc_client_secret"
|
||||
value="" placeholder="<?php echo esc_attr( $placeholder ); ?>"
|
||||
class="regular-text" autocomplete="new-password" />
|
||||
<p class="description">
|
||||
<?php if ( $has_secret ) : ?>
|
||||
<?php esc_html_e( 'Leave blank to keep existing secret. Enter a new value to update.', 'woo-business-central' ); ?>
|
||||
<?php else : ?>
|
||||
<?php esc_html_e( 'Client secret from Azure AD application.', 'woo-business-central' ); ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbc_environment"><?php esc_html_e( 'Environment', 'woo-business-central' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="wbc_environment" name="wbc_environment">
|
||||
<option value="production" <?php selected( get_option( 'wbc_environment', 'production' ), 'production' ); ?>>
|
||||
<?php esc_html_e( 'Production', 'woo-business-central' ); ?>
|
||||
</option>
|
||||
<option value="sandbox" <?php selected( get_option( 'wbc_environment' ), 'sandbox' ); ?>>
|
||||
<?php esc_html_e( 'Sandbox', 'woo-business-central' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbc_company_id"><?php esc_html_e( 'Company ID', 'woo-business-central' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="wbc_company_id" name="wbc_company_id"
|
||||
value="<?php echo esc_attr( get_option( 'wbc_company_id', '' ) ); ?>"
|
||||
class="regular-text" />
|
||||
<button type="button" id="wbc-load-companies" class="button">
|
||||
<?php esc_html_e( 'Load Companies', 'woo-business-central' ); ?>
|
||||
</button>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Business Central company ID (GUID). Click "Load Companies" after saving credentials.', 'woo-business-central' ); ?>
|
||||
</p>
|
||||
<div id="wbc-companies-list" style="display: none; margin-top: 10px;">
|
||||
<select id="wbc-company-select">
|
||||
<option value=""><?php esc_html_e( 'Select a company...', 'woo-business-central' ); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<?php submit_button( __( 'Save Settings', 'woo-business-central' ), 'primary', 'submit', false ); ?>
|
||||
<button type="button" id="wbc-test-connection" class="button button-secondary">
|
||||
<?php esc_html_e( 'Test Connection', 'woo-business-central' ); ?>
|
||||
</button>
|
||||
<span id="wbc-connection-status" class="wbc-status"></span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php elseif ( $current_tab === 'sync' ) : ?>
|
||||
<!-- Sync Settings -->
|
||||
<form method="post" action="options.php" class="wbc-settings-form">
|
||||
<?php settings_fields( 'wbc_settings' ); ?>
|
||||
|
||||
<div class="wbc-card">
|
||||
<h2><?php esc_html_e( 'Product Sync Settings', 'woo-business-central' ); ?></h2>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbc_sync_frequency"><?php esc_html_e( 'Sync Frequency', 'woo-business-central' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="wbc_sync_frequency" name="wbc_sync_frequency">
|
||||
<option value="hourly" <?php selected( get_option( 'wbc_sync_frequency', 'daily' ), 'hourly' ); ?>>
|
||||
<?php esc_html_e( 'Hourly', 'woo-business-central' ); ?>
|
||||
</option>
|
||||
<option value="twice_daily" <?php selected( get_option( 'wbc_sync_frequency' ), 'twice_daily' ); ?>>
|
||||
<?php esc_html_e( 'Twice Daily', 'woo-business-central' ); ?>
|
||||
</option>
|
||||
<option value="daily" <?php selected( get_option( 'wbc_sync_frequency' ), 'daily' ); ?>>
|
||||
<?php esc_html_e( 'Daily', 'woo-business-central' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Enable Stock Sync', 'woo-business-central' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="wbc_enable_stock_sync" value="yes"
|
||||
<?php checked( get_option( 'wbc_enable_stock_sync', 'yes' ), 'yes' ); ?> />
|
||||
<?php esc_html_e( 'Sync stock levels from Business Central to WooCommerce', 'woo-business-central' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Enable Price Sync', 'woo-business-central' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="wbc_enable_price_sync" value="yes"
|
||||
<?php checked( get_option( 'wbc_enable_price_sync', 'yes' ), 'yes' ); ?> />
|
||||
<?php esc_html_e( 'Sync prices from Business Central to WooCommerce', 'woo-business-central' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<?php submit_button( __( 'Save Settings', 'woo-business-central' ), 'primary', 'submit', false ); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="wbc-card">
|
||||
<h2><?php esc_html_e( 'Sync Status', 'woo-business-central' ); ?></h2>
|
||||
|
||||
<table class="wbc-status-table">
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Last Sync:', 'woo-business-central' ); ?></th>
|
||||
<td><?php echo esc_html( WBC_Cron::get_last_sync_formatted() ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Next Scheduled Sync:', 'woo-business-central' ); ?></th>
|
||||
<td><?php echo esc_html( WBC_Cron::get_next_sync_formatted() ); ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<button type="button" id="wbc-manual-sync" class="button button-primary">
|
||||
<?php esc_html_e( 'Sync Now', 'woo-business-central' ); ?>
|
||||
</button>
|
||||
<span id="wbc-sync-status" class="wbc-status"></span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php elseif ( $current_tab === 'orders' ) : ?>
|
||||
<!-- Order Settings -->
|
||||
<form method="post" action="options.php" class="wbc-settings-form">
|
||||
<?php settings_fields( 'wbc_settings' ); ?>
|
||||
|
||||
<div class="wbc-card">
|
||||
<h2><?php esc_html_e( 'Order Sync Settings', 'woo-business-central' ); ?></h2>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Enable Order Sync', 'woo-business-central' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="wbc_enable_order_sync" value="yes"
|
||||
<?php checked( get_option( 'wbc_enable_order_sync', 'yes' ), 'yes' ); ?> />
|
||||
<?php esc_html_e( 'Sync orders to Business Central when payment is received', 'woo-business-central' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbc_default_payment_terms_id"><?php esc_html_e( 'Default Payment Terms ID', 'woo-business-central' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="wbc_default_payment_terms_id" name="wbc_default_payment_terms_id"
|
||||
value="<?php echo esc_attr( get_option( 'wbc_default_payment_terms_id', '' ) ); ?>"
|
||||
class="regular-text" />
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Optional. Business Central payment terms ID to use for new orders.', 'woo-business-central' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbc_default_shipment_method_id"><?php esc_html_e( 'Default Shipment Method ID', 'woo-business-central' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="wbc_default_shipment_method_id" name="wbc_default_shipment_method_id"
|
||||
value="<?php echo esc_attr( get_option( 'wbc_default_shipment_method_id', '' ) ); ?>"
|
||||
class="regular-text" />
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Optional. Business Central shipment method ID to use for new orders.', 'woo-business-central' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wbc_shipping_item_number"><?php esc_html_e( 'Shipping Item Number', 'woo-business-central' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="wbc_shipping_item_number" name="wbc_shipping_item_number"
|
||||
value="<?php echo esc_attr( get_option( 'wbc_shipping_item_number', '' ) ); ?>"
|
||||
class="regular-text" />
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Optional. Business Central item number to use for shipping charges.', 'woo-business-central' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<?php submit_button( __( 'Save Settings', 'woo-business-central' ), 'primary', 'submit', false ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php elseif ( $current_tab === 'logs' ) : ?>
|
||||
<!-- Logs -->
|
||||
<div class="wbc-card">
|
||||
<h2><?php esc_html_e( 'Sync Logs', 'woo-business-central' ); ?></h2>
|
||||
|
||||
<p>
|
||||
<button type="button" id="wbc-clear-logs" class="button">
|
||||
<?php esc_html_e( 'Clear All Logs', 'woo-business-central' ); ?>
|
||||
</button>
|
||||
<a href="<?php echo esc_url( add_query_arg( array( 'wbc_export_logs' => '1', '_wpnonce' => wp_create_nonce( 'wbc_export_logs' ) ) ) ); ?>"
|
||||
class="button">
|
||||
<?php esc_html_e( 'Download CSV', 'woo-business-central' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<?php
|
||||
$log_level = isset( $_GET['log_level'] ) ? sanitize_text_field( wp_unslash( $_GET['log_level'] ) ) : '';
|
||||
$page_num = isset( $_GET['log_page'] ) ? max( 1, absint( $_GET['log_page'] ) ) : 1;
|
||||
$per_page = 50;
|
||||
|
||||
$logs_data = WBC_Admin::get_logs_for_display( array(
|
||||
'level' => $log_level,
|
||||
'limit' => $per_page,
|
||||
'offset' => ( $page_num - 1 ) * $per_page,
|
||||
) );
|
||||
|
||||
$logs = $logs_data['logs'];
|
||||
$total = $logs_data['total'];
|
||||
$total_pages = ceil( $total / $per_page );
|
||||
?>
|
||||
|
||||
<div class="wbc-log-filters">
|
||||
<label for="wbc-log-level-filter"><?php esc_html_e( 'Filter by level:', 'woo-business-central' ); ?></label>
|
||||
<select id="wbc-log-level-filter" onchange="location = this.value;">
|
||||
<option value="<?php echo esc_url( remove_query_arg( 'log_level' ) ); ?>"><?php esc_html_e( 'All', 'woo-business-central' ); ?></option>
|
||||
<?php foreach ( array( 'DEBUG', 'INFO', 'WARNING', 'ERROR' ) as $level ) : ?>
|
||||
<option value="<?php echo esc_url( add_query_arg( 'log_level', $level ) ); ?>" <?php selected( $log_level, $level ); ?>>
|
||||
<?php echo esc_html( $level ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<table class="wp-list-table widefat fixed striped wbc-logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="column-timestamp"><?php esc_html_e( 'Timestamp', 'woo-business-central' ); ?></th>
|
||||
<th class="column-level"><?php esc_html_e( 'Level', 'woo-business-central' ); ?></th>
|
||||
<th class="column-context"><?php esc_html_e( 'Context', 'woo-business-central' ); ?></th>
|
||||
<th class="column-message"><?php esc_html_e( 'Message', 'woo-business-central' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ( empty( $logs ) ) : ?>
|
||||
<tr>
|
||||
<td colspan="4"><?php esc_html_e( 'No logs found.', 'woo-business-central' ); ?></td>
|
||||
</tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $logs as $log ) : ?>
|
||||
<tr class="wbc-log-level-<?php echo esc_attr( strtolower( $log['level'] ) ); ?>">
|
||||
<td class="column-timestamp">
|
||||
<?php echo esc_html( wp_date( 'Y-m-d H:i:s', strtotime( $log['timestamp'] ) ) ); ?>
|
||||
</td>
|
||||
<td class="column-level">
|
||||
<span class="wbc-log-badge wbc-log-badge-<?php echo esc_attr( strtolower( $log['level'] ) ); ?>">
|
||||
<?php echo esc_html( $log['level'] ); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="column-context"><?php echo esc_html( $log['context'] ); ?></td>
|
||||
<td class="column-message">
|
||||
<?php echo esc_html( $log['message'] ); ?>
|
||||
<?php if ( ! empty( $log['data'] ) ) : ?>
|
||||
<button type="button" class="button-link wbc-toggle-data"
|
||||
data-show-text="<?php esc_attr_e( 'Show data', 'woo-business-central' ); ?>"
|
||||
data-hide-text="<?php esc_attr_e( 'Hide data', 'woo-business-central' ); ?>">
|
||||
<?php esc_html_e( 'Show data', 'woo-business-central' ); ?>
|
||||
</button>
|
||||
<pre class="wbc-log-data" style="display: none;"><?php echo esc_html( $log['data'] ); ?></pre>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if ( $total_pages > 1 ) : ?>
|
||||
<div class="tablenav bottom">
|
||||
<div class="tablenav-pages">
|
||||
<span class="displaying-num">
|
||||
<?php printf( esc_html__( '%d items', 'woo-business-central' ), $total ); ?>
|
||||
</span>
|
||||
<span class="pagination-links">
|
||||
<?php if ( $page_num > 1 ) : ?>
|
||||
<a class="prev-page button" href="<?php echo esc_url( add_query_arg( 'log_page', $page_num - 1 ) ); ?>">
|
||||
‹
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<span class="paging-input">
|
||||
<?php echo esc_html( $page_num ); ?> / <?php echo esc_html( $total_pages ); ?>
|
||||
</span>
|
||||
<?php if ( $page_num < $total_pages ) : ?>
|
||||
<a class="next-page button" href="<?php echo esc_url( add_query_arg( 'log_page', $page_num + 1 ) ); ?>">
|
||||
›
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
381
woo-business-central/includes/class-wbc-api-client.php
Normal file
381
woo-business-central/includes/class-wbc-api-client.php
Normal file
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
/**
|
||||
* Business Central API Client
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_API_Client
|
||||
*
|
||||
* HTTP client for Business Central API using native cURL.
|
||||
*/
|
||||
class WBC_API_Client {
|
||||
|
||||
/**
|
||||
* Base API URL template
|
||||
*/
|
||||
const BASE_URL = 'https://api.businesscentral.dynamics.com/v2.0/%s/api/v2.0';
|
||||
|
||||
/**
|
||||
* Maximum retry attempts
|
||||
*/
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
/**
|
||||
* Retry delay in seconds
|
||||
*/
|
||||
const RETRY_DELAY = 2;
|
||||
|
||||
/**
|
||||
* Request timeout in seconds
|
||||
*/
|
||||
const TIMEOUT = 30;
|
||||
|
||||
/**
|
||||
* Get the base API URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_base_url() {
|
||||
$environment = WBC_OAuth::get_environment();
|
||||
return sprintf( self::BASE_URL, $environment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company ID for URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_company_id() {
|
||||
return WBC_OAuth::get_company_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full endpoint URL
|
||||
*
|
||||
* @param string $endpoint The API endpoint.
|
||||
* @param bool $include_company Whether to include company ID in URL.
|
||||
* @return string Full URL.
|
||||
*/
|
||||
private static function build_url( $endpoint, $include_company = true ) {
|
||||
$base_url = self::get_base_url();
|
||||
|
||||
if ( $include_company ) {
|
||||
$company_id = self::get_company_id();
|
||||
return $base_url . '/companies(' . $company_id . ')' . $endpoint;
|
||||
}
|
||||
|
||||
return $base_url . $endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP request to the Business Central API
|
||||
*
|
||||
* @param string $method HTTP method (GET, POST, PATCH, DELETE).
|
||||
* @param string $endpoint API endpoint.
|
||||
* @param array $params Query parameters (for GET) or body data (for POST/PATCH).
|
||||
* @param bool $include_company Whether to include company ID in URL.
|
||||
* @param int $retry_count Current retry attempt.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
private static function request( $method, $endpoint, $params = array(), $include_company = true, $retry_count = 0 ) {
|
||||
// Get access token
|
||||
$token = WBC_OAuth::get_access_token();
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
// Build URL
|
||||
$url = self::build_url( $endpoint, $include_company );
|
||||
|
||||
// Add query parameters for GET requests
|
||||
if ( $method === 'GET' && ! empty( $params ) ) {
|
||||
$url .= '?' . http_build_query( $params );
|
||||
}
|
||||
|
||||
WBC_Logger::debug( 'API', "Making $method request", array(
|
||||
'url' => $url,
|
||||
'method' => $method,
|
||||
) );
|
||||
|
||||
// Initialize cURL
|
||||
$ch = curl_init();
|
||||
|
||||
// Set base options
|
||||
$curl_options = array(
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => self::TIMEOUT,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
),
|
||||
);
|
||||
|
||||
// Set method-specific options
|
||||
switch ( $method ) {
|
||||
case 'POST':
|
||||
$curl_options[ CURLOPT_POST ] = true;
|
||||
if ( ! empty( $params ) ) {
|
||||
$curl_options[ CURLOPT_POSTFIELDS ] = wp_json_encode( $params );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
$curl_options[ CURLOPT_CUSTOMREQUEST ] = 'PATCH';
|
||||
if ( ! empty( $params ) ) {
|
||||
$curl_options[ CURLOPT_POSTFIELDS ] = wp_json_encode( $params );
|
||||
}
|
||||
// Add If-Match header for PATCH requests
|
||||
$curl_options[ CURLOPT_HTTPHEADER ][] = 'If-Match: *';
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$curl_options[ CURLOPT_CUSTOMREQUEST ] = 'DELETE';
|
||||
break;
|
||||
|
||||
default: // GET
|
||||
$curl_options[ CURLOPT_HTTPGET ] = true;
|
||||
break;
|
||||
}
|
||||
|
||||
curl_setopt_array( $ch, $curl_options );
|
||||
|
||||
// Execute request
|
||||
$response = curl_exec( $ch );
|
||||
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
$curl_error = curl_error( $ch );
|
||||
|
||||
curl_close( $ch );
|
||||
|
||||
// Handle cURL errors
|
||||
if ( $response === false ) {
|
||||
WBC_Logger::error( 'API', 'cURL error', array(
|
||||
'error' => $curl_error,
|
||||
'url' => $url,
|
||||
) );
|
||||
return new WP_Error( 'wbc_curl_error', $curl_error );
|
||||
}
|
||||
|
||||
// Parse response
|
||||
$data = json_decode( $response, true );
|
||||
|
||||
// Handle 401 (unauthorized) - token might be expired
|
||||
if ( $http_code === 401 && $retry_count < self::MAX_RETRIES ) {
|
||||
WBC_Logger::warning( 'API', 'Received 401, refreshing token and retrying', array(
|
||||
'retry_count' => $retry_count + 1,
|
||||
) );
|
||||
|
||||
// Clear token cache and retry
|
||||
WBC_OAuth::clear_token_cache();
|
||||
sleep( self::RETRY_DELAY );
|
||||
|
||||
return self::request( $method, $endpoint, $params, $include_company, $retry_count + 1 );
|
||||
}
|
||||
|
||||
// Handle rate limiting (429)
|
||||
if ( $http_code === 429 && $retry_count < self::MAX_RETRIES ) {
|
||||
$retry_after = isset( $data['error']['retryAfterSeconds'] ) ? (int) $data['error']['retryAfterSeconds'] : self::RETRY_DELAY * ( $retry_count + 1 );
|
||||
|
||||
WBC_Logger::warning( 'API', 'Rate limited, retrying after delay', array(
|
||||
'retry_after' => $retry_after,
|
||||
'retry_count' => $retry_count + 1,
|
||||
) );
|
||||
|
||||
sleep( $retry_after );
|
||||
|
||||
return self::request( $method, $endpoint, $params, $include_company, $retry_count + 1 );
|
||||
}
|
||||
|
||||
// Handle server errors (5xx)
|
||||
if ( $http_code >= 500 && $retry_count < self::MAX_RETRIES ) {
|
||||
WBC_Logger::warning( 'API', 'Server error, retrying', array(
|
||||
'http_code' => $http_code,
|
||||
'retry_count' => $retry_count + 1,
|
||||
) );
|
||||
|
||||
sleep( self::RETRY_DELAY * ( $retry_count + 1 ) );
|
||||
|
||||
return self::request( $method, $endpoint, $params, $include_company, $retry_count + 1 );
|
||||
}
|
||||
|
||||
// Handle other errors
|
||||
if ( $http_code >= 400 ) {
|
||||
$error_message = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'API request failed', 'woo-business-central' );
|
||||
|
||||
WBC_Logger::error( 'API', 'Request failed', array(
|
||||
'http_code' => $http_code,
|
||||
'error' => $data['error'] ?? null,
|
||||
'url' => $url,
|
||||
) );
|
||||
|
||||
return new WP_Error( 'wbc_api_error', $error_message, array(
|
||||
'status' => $http_code,
|
||||
'error' => $data['error'] ?? null,
|
||||
) );
|
||||
}
|
||||
|
||||
WBC_Logger::debug( 'API', 'Request successful', array(
|
||||
'http_code' => $http_code,
|
||||
'url' => $url,
|
||||
) );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request
|
||||
*
|
||||
* @param string $endpoint API endpoint.
|
||||
* @param array $params Query parameters.
|
||||
* @param bool $include_company Whether to include company ID.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function get( $endpoint, $params = array(), $include_company = true ) {
|
||||
return self::request( 'GET', $endpoint, $params, $include_company );
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request
|
||||
*
|
||||
* @param string $endpoint API endpoint.
|
||||
* @param array $data Request body data.
|
||||
* @param bool $include_company Whether to include company ID.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function post( $endpoint, $data = array(), $include_company = true ) {
|
||||
return self::request( 'POST', $endpoint, $data, $include_company );
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH request
|
||||
*
|
||||
* @param string $endpoint API endpoint.
|
||||
* @param array $data Request body data.
|
||||
* @param bool $include_company Whether to include company ID.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function patch( $endpoint, $data = array(), $include_company = true ) {
|
||||
return self::request( 'PATCH', $endpoint, $data, $include_company );
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
*
|
||||
* @param string $endpoint API endpoint.
|
||||
* @param bool $include_company Whether to include company ID.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function delete( $endpoint, $include_company = true ) {
|
||||
return self::request( 'DELETE', $endpoint, array(), $include_company );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all companies
|
||||
*
|
||||
* @return array|WP_Error Array of companies or error.
|
||||
*/
|
||||
public static function get_companies() {
|
||||
return self::get( '/companies', array(), false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items with pagination support
|
||||
*
|
||||
* @param int $top Number of items to fetch.
|
||||
* @param int $skip Number of items to skip.
|
||||
* @param string $select Fields to select.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function get_items( $top = 100, $skip = 0, $select = '' ) {
|
||||
$params = array(
|
||||
'$top' => $top,
|
||||
'$skip' => $skip,
|
||||
);
|
||||
|
||||
if ( ! empty( $select ) ) {
|
||||
$params['$select'] = $select;
|
||||
}
|
||||
|
||||
return self::get( '/items', $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customers with optional filter
|
||||
*
|
||||
* @param string $filter OData filter expression.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function get_customers( $filter = '' ) {
|
||||
$params = array();
|
||||
|
||||
if ( ! empty( $filter ) ) {
|
||||
$params['$filter'] = $filter;
|
||||
}
|
||||
|
||||
return self::get( '/customers', $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a customer
|
||||
*
|
||||
* @param array $data Customer data.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function create_customer( $data ) {
|
||||
return self::post( '/customers', $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sales order
|
||||
*
|
||||
* @param array $data Sales order data.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function create_sales_order( $data ) {
|
||||
return self::post( '/salesOrders', $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sales order line
|
||||
*
|
||||
* @param string $order_id Sales order ID.
|
||||
* @param array $data Sales order line data.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function create_sales_order_line( $order_id, $data ) {
|
||||
return self::post( '/salesOrders(' . $order_id . ')/salesOrderLines', $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string value for use in OData filter expressions
|
||||
*
|
||||
* @param string $value The value to escape.
|
||||
* @return string Escaped value safe for OData filters.
|
||||
*/
|
||||
public static function escape_odata_string( $value ) {
|
||||
return str_replace( "'", "''", $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific item by number
|
||||
*
|
||||
* @param string $item_number Item number.
|
||||
* @return array|WP_Error Response data or error.
|
||||
*/
|
||||
public static function get_item_by_number( $item_number ) {
|
||||
$params = array(
|
||||
'$filter' => "number eq '" . self::escape_odata_string( $item_number ) . "'",
|
||||
);
|
||||
|
||||
return self::get( '/items', $params );
|
||||
}
|
||||
}
|
||||
173
woo-business-central/includes/class-wbc-cron.php
Normal file
173
woo-business-central/includes/class-wbc-cron.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
/**
|
||||
* Cron Scheduler for WooCommerce Business Central Integration
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_Cron
|
||||
*
|
||||
* Handles scheduled tasks for syncing with Business Central.
|
||||
*/
|
||||
class WBC_Cron {
|
||||
|
||||
/**
|
||||
* Cron event name
|
||||
*/
|
||||
const SYNC_EVENT = 'wbc_product_sync_event';
|
||||
|
||||
/**
|
||||
* Cleanup event name
|
||||
*/
|
||||
const CLEANUP_EVENT = 'wbc_log_cleanup_event';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
// Register cleanup cron if not scheduled
|
||||
if ( ! wp_next_scheduled( self::CLEANUP_EVENT ) ) {
|
||||
wp_schedule_event( time(), 'daily', self::CLEANUP_EVENT );
|
||||
}
|
||||
|
||||
// Hook the cleanup handler
|
||||
add_action( self::CLEANUP_EVENT, array( $this, 'run_log_cleanup' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run scheduled product sync
|
||||
*/
|
||||
public function run_scheduled_sync() {
|
||||
WBC_Logger::info( 'Cron', 'Starting scheduled product sync' );
|
||||
|
||||
$product_sync = new WBC_Product_Sync();
|
||||
$result = $product_sync->run_sync();
|
||||
|
||||
if ( isset( $result['success'] ) && $result['success'] ) {
|
||||
WBC_Logger::info( 'Cron', 'Scheduled product sync completed', $result );
|
||||
} else {
|
||||
WBC_Logger::error( 'Cron', 'Scheduled product sync failed', $result );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run log cleanup
|
||||
*/
|
||||
public function run_log_cleanup() {
|
||||
$deleted = WBC_Logger::cleanup_old_logs( 30 );
|
||||
WBC_Logger::info( 'Cron', 'Log cleanup completed', array( 'deleted_count' => $deleted ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reschedule the sync event with a new frequency
|
||||
*
|
||||
* @param string $frequency New frequency (hourly, twice_daily, daily).
|
||||
*/
|
||||
public static function reschedule_sync( $frequency ) {
|
||||
// Clear existing schedule
|
||||
$timestamp = wp_next_scheduled( self::SYNC_EVENT );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, self::SYNC_EVENT );
|
||||
}
|
||||
|
||||
// Schedule with new frequency
|
||||
wp_schedule_event( time(), $frequency, self::SYNC_EVENT );
|
||||
|
||||
WBC_Logger::info( 'Cron', 'Sync schedule updated', array( 'frequency' => $frequency ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next scheduled sync time
|
||||
*
|
||||
* @return int|false Timestamp of next scheduled sync or false if not scheduled.
|
||||
*/
|
||||
public static function get_next_sync_time() {
|
||||
return wp_next_scheduled( self::SYNC_EVENT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next scheduled sync time formatted
|
||||
*
|
||||
* @return string Formatted date/time string or 'Not scheduled'.
|
||||
*/
|
||||
public static function get_next_sync_formatted() {
|
||||
$next = self::get_next_sync_time();
|
||||
|
||||
if ( ! $next ) {
|
||||
return __( 'Not scheduled', 'woo-business-central' );
|
||||
}
|
||||
|
||||
return wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last sync time
|
||||
*
|
||||
* @return string|false Last sync time or false if never synced.
|
||||
*/
|
||||
public static function get_last_sync_time() {
|
||||
return get_option( 'wbc_last_sync_time', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last sync time formatted
|
||||
*
|
||||
* @return string Formatted date/time string or 'Never'.
|
||||
*/
|
||||
public static function get_last_sync_formatted() {
|
||||
$last = self::get_last_sync_time();
|
||||
|
||||
if ( ! $last ) {
|
||||
return __( 'Never', 'woo-business-central' );
|
||||
}
|
||||
|
||||
return wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $last ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last sync time
|
||||
*/
|
||||
public static function update_last_sync_time() {
|
||||
update_option( 'wbc_last_sync_time', current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run sync now (manual trigger)
|
||||
*
|
||||
* @return array Sync result.
|
||||
*/
|
||||
public static function run_sync_now() {
|
||||
WBC_Logger::info( 'Cron', 'Manual sync triggered' );
|
||||
|
||||
$product_sync = new WBC_Product_Sync();
|
||||
$result = $product_sync->run_sync();
|
||||
|
||||
// Update last sync time
|
||||
self::update_last_sync_time();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all scheduled events
|
||||
*/
|
||||
public static function clear_all_schedules() {
|
||||
// Clear sync event
|
||||
$sync_timestamp = wp_next_scheduled( self::SYNC_EVENT );
|
||||
if ( $sync_timestamp ) {
|
||||
wp_unschedule_event( $sync_timestamp, self::SYNC_EVENT );
|
||||
}
|
||||
|
||||
// Clear cleanup event
|
||||
$cleanup_timestamp = wp_next_scheduled( self::CLEANUP_EVENT );
|
||||
if ( $cleanup_timestamp ) {
|
||||
wp_unschedule_event( $cleanup_timestamp, self::CLEANUP_EVENT );
|
||||
}
|
||||
}
|
||||
}
|
||||
270
woo-business-central/includes/class-wbc-customer-sync.php
Normal file
270
woo-business-central/includes/class-wbc-customer-sync.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
/**
|
||||
* Customer Sync to Business Central
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_Customer_Sync
|
||||
*
|
||||
* Handles syncing customers from WooCommerce to Business Central.
|
||||
*/
|
||||
class WBC_Customer_Sync {
|
||||
|
||||
/**
|
||||
* Get or create a customer in Business Central
|
||||
*
|
||||
* @param WC_Order $order WooCommerce order.
|
||||
* @return string|WP_Error BC customer number or error.
|
||||
*/
|
||||
public function get_or_create_customer( $order ) {
|
||||
$email = $order->get_billing_email();
|
||||
|
||||
if ( empty( $email ) ) {
|
||||
return new WP_Error( 'wbc_no_email', __( 'Order has no billing email.', 'woo-business-central' ) );
|
||||
}
|
||||
|
||||
WBC_Logger::debug( 'CustomerSync', 'Getting or creating customer', array( 'email' => $email ) );
|
||||
|
||||
// Check if we have a cached BC customer ID for this user
|
||||
$user_id = $order->get_user_id();
|
||||
if ( $user_id ) {
|
||||
$cached_customer_number = get_user_meta( $user_id, '_wbc_bc_customer_number', true );
|
||||
if ( ! empty( $cached_customer_number ) ) {
|
||||
// Verify the customer still exists in BC
|
||||
$exists = $this->verify_customer_exists( $cached_customer_number );
|
||||
if ( $exists ) {
|
||||
WBC_Logger::debug( 'CustomerSync', 'Using cached BC customer', array(
|
||||
'customer_number' => $cached_customer_number,
|
||||
) );
|
||||
return $cached_customer_number;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find existing customer in BC by email
|
||||
$existing = $this->find_customer_by_email( $email );
|
||||
|
||||
if ( is_wp_error( $existing ) ) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
if ( $existing ) {
|
||||
$customer_number = $existing['number'];
|
||||
|
||||
// Cache the customer number for this user
|
||||
if ( $user_id ) {
|
||||
update_user_meta( $user_id, '_wbc_bc_customer_id', $existing['id'] );
|
||||
update_user_meta( $user_id, '_wbc_bc_customer_number', $customer_number );
|
||||
}
|
||||
|
||||
WBC_Logger::info( 'CustomerSync', 'Found existing BC customer', array(
|
||||
'email' => $email,
|
||||
'customer_number' => $customer_number,
|
||||
) );
|
||||
|
||||
return $customer_number;
|
||||
}
|
||||
|
||||
// Create new customer in BC
|
||||
$new_customer = $this->create_customer( $order );
|
||||
|
||||
if ( is_wp_error( $new_customer ) ) {
|
||||
return $new_customer;
|
||||
}
|
||||
|
||||
$customer_number = $new_customer['number'];
|
||||
|
||||
// Cache the customer number for this user
|
||||
if ( $user_id ) {
|
||||
update_user_meta( $user_id, '_wbc_bc_customer_id', $new_customer['id'] );
|
||||
update_user_meta( $user_id, '_wbc_bc_customer_number', $customer_number );
|
||||
}
|
||||
|
||||
WBC_Logger::info( 'CustomerSync', 'Created new BC customer', array(
|
||||
'email' => $email,
|
||||
'customer_number' => $customer_number,
|
||||
) );
|
||||
|
||||
return $customer_number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find customer in BC by email
|
||||
*
|
||||
* @param string $email Customer email.
|
||||
* @return array|false|WP_Error Customer data, false if not found, or error.
|
||||
*/
|
||||
private function find_customer_by_email( $email ) {
|
||||
$filter = "email eq '" . WBC_API_Client::escape_odata_string( $email ) . "'";
|
||||
$result = WBC_API_Client::get_customers( $filter );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$customers = isset( $result['value'] ) ? $result['value'] : array();
|
||||
|
||||
if ( empty( $customers ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $customers[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a customer exists in BC
|
||||
*
|
||||
* @param string $customer_number BC customer number.
|
||||
* @return bool Whether the customer exists.
|
||||
*/
|
||||
private function verify_customer_exists( $customer_number ) {
|
||||
$filter = "number eq '" . WBC_API_Client::escape_odata_string( $customer_number ) . "'";
|
||||
$result = WBC_API_Client::get_customers( $filter );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customers = isset( $result['value'] ) ? $result['value'] : array();
|
||||
|
||||
return ! empty( $customers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new customer in BC
|
||||
*
|
||||
* @param WC_Order $order WooCommerce order.
|
||||
* @return array|WP_Error Created customer data or error.
|
||||
*/
|
||||
private function create_customer( $order ) {
|
||||
// Build customer data from order
|
||||
$customer_data = $this->build_customer_data( $order );
|
||||
|
||||
WBC_Logger::debug( 'CustomerSync', 'Creating new customer in BC', array(
|
||||
'data' => $customer_data,
|
||||
) );
|
||||
|
||||
$result = WBC_API_Client::create_customer( $customer_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
WBC_Logger::error( 'CustomerSync', 'Failed to create customer in BC', array(
|
||||
'error' => $result->get_error_message(),
|
||||
'data' => $customer_data,
|
||||
) );
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build customer data from WC order
|
||||
*
|
||||
* @param WC_Order $order WooCommerce order.
|
||||
* @return array Customer data for BC API.
|
||||
*/
|
||||
private function build_customer_data( $order ) {
|
||||
// Get billing info
|
||||
$first_name = $order->get_billing_first_name();
|
||||
$last_name = $order->get_billing_last_name();
|
||||
$company = $order->get_billing_company();
|
||||
|
||||
// Determine display name and type
|
||||
if ( ! empty( $company ) ) {
|
||||
$display_name = $company;
|
||||
$type = 'Company';
|
||||
} else {
|
||||
$display_name = trim( $first_name . ' ' . $last_name );
|
||||
$type = 'Person';
|
||||
}
|
||||
|
||||
// Build address
|
||||
$address_line1 = $order->get_billing_address_1();
|
||||
$address_line2 = $order->get_billing_address_2();
|
||||
|
||||
if ( ! empty( $address_line2 ) ) {
|
||||
$address_line1 .= ', ' . $address_line2;
|
||||
}
|
||||
|
||||
// Map country code
|
||||
$country = $order->get_billing_country();
|
||||
|
||||
// Build customer payload
|
||||
$customer_data = array(
|
||||
'displayName' => substr( $display_name, 0, 100 ), // BC has max length
|
||||
'type' => $type,
|
||||
'email' => $order->get_billing_email(),
|
||||
);
|
||||
|
||||
// Add optional fields if present
|
||||
$phone = $order->get_billing_phone();
|
||||
if ( ! empty( $phone ) ) {
|
||||
$customer_data['phoneNumber'] = substr( $phone, 0, 30 );
|
||||
}
|
||||
|
||||
if ( ! empty( $address_line1 ) ) {
|
||||
$customer_data['addressLine1'] = substr( $address_line1, 0, 100 );
|
||||
}
|
||||
|
||||
$city = $order->get_billing_city();
|
||||
if ( ! empty( $city ) ) {
|
||||
$customer_data['city'] = substr( $city, 0, 30 );
|
||||
}
|
||||
|
||||
$state = $order->get_billing_state();
|
||||
if ( ! empty( $state ) ) {
|
||||
$customer_data['state'] = substr( $state, 0, 30 );
|
||||
}
|
||||
|
||||
$postcode = $order->get_billing_postcode();
|
||||
if ( ! empty( $postcode ) ) {
|
||||
$customer_data['postalCode'] = substr( $postcode, 0, 20 );
|
||||
}
|
||||
|
||||
if ( ! empty( $country ) ) {
|
||||
$customer_data['country'] = $country;
|
||||
}
|
||||
|
||||
// Add currency if available
|
||||
$currency = $order->get_currency();
|
||||
if ( ! empty( $currency ) ) {
|
||||
$customer_data['currencyCode'] = $currency;
|
||||
}
|
||||
|
||||
return $customer_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update customer in BC with WC order data
|
||||
*
|
||||
* @param string $customer_id BC customer ID.
|
||||
* @param WC_Order $order WooCommerce order.
|
||||
* @return array|WP_Error Updated customer data or error.
|
||||
*/
|
||||
public function update_customer( $customer_id, $order ) {
|
||||
$customer_data = $this->build_customer_data( $order );
|
||||
|
||||
WBC_Logger::debug( 'CustomerSync', 'Updating customer in BC', array(
|
||||
'customer_id' => $customer_id,
|
||||
'data' => $customer_data,
|
||||
) );
|
||||
|
||||
$result = WBC_API_Client::patch( '/customers(' . $customer_id . ')', $customer_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
WBC_Logger::error( 'CustomerSync', 'Failed to update customer in BC', array(
|
||||
'error' => $result->get_error_message(),
|
||||
'customer_id' => $customer_id,
|
||||
) );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
140
woo-business-central/includes/class-wbc-loader.php
Normal file
140
woo-business-central/includes/class-wbc-loader.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
/**
|
||||
* Register all actions and filters for the plugin
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_Loader
|
||||
*
|
||||
* Maintains lists of all hooks registered throughout the plugin
|
||||
* and registers them with WordPress.
|
||||
*/
|
||||
class WBC_Loader {
|
||||
|
||||
/**
|
||||
* Array of actions registered with WordPress
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $actions = array();
|
||||
|
||||
/**
|
||||
* Array of filters registered with WordPress
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $filters = array();
|
||||
|
||||
/**
|
||||
* Initialize the loader
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->actions = array();
|
||||
$this->filters = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new action to the collection
|
||||
*
|
||||
* @param string $hook The name of the WordPress action.
|
||||
* @param object $component A reference to the instance of the object on which the action is defined.
|
||||
* @param string $callback The name of the function definition on the $component.
|
||||
* @param int $priority Optional. The priority at which the function should be fired. Default 10.
|
||||
* @param int $accepted_args Optional. The number of arguments that should be passed to the callback. Default 1.
|
||||
*/
|
||||
public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
|
||||
$this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new filter to the collection
|
||||
*
|
||||
* @param string $hook The name of the WordPress filter.
|
||||
* @param object $component A reference to the instance of the object on which the filter is defined.
|
||||
* @param string $callback The name of the function definition on the $component.
|
||||
* @param int $priority Optional. The priority at which the function should be fired. Default 10.
|
||||
* @param int $accepted_args Optional. The number of arguments that should be passed to the callback. Default 1.
|
||||
*/
|
||||
public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
|
||||
$this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility function for registering hooks into a single collection
|
||||
*
|
||||
* @param array $hooks The collection of hooks being registered.
|
||||
* @param string $hook The name of the WordPress filter being registered.
|
||||
* @param object $component A reference to the instance of the object on which the filter is defined.
|
||||
* @param string $callback The name of the function definition on the $component.
|
||||
* @param int $priority The priority at which the function should be fired.
|
||||
* @param int $accepted_args The number of arguments that should be passed to the callback.
|
||||
* @return array The collection of actions and filters registered with WordPress.
|
||||
*/
|
||||
private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) {
|
||||
$hooks[] = array(
|
||||
'hook' => $hook,
|
||||
'component' => $component,
|
||||
'callback' => $callback,
|
||||
'priority' => $priority,
|
||||
'accepted_args' => $accepted_args,
|
||||
);
|
||||
return $hooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the filters and actions with WordPress
|
||||
*/
|
||||
public function run() {
|
||||
// Register cron handler
|
||||
$cron = new WBC_Cron();
|
||||
$this->add_action( 'wbc_product_sync_event', $cron, 'run_scheduled_sync' );
|
||||
|
||||
// Register order sync handler
|
||||
$order_sync = new WBC_Order_Sync();
|
||||
$this->add_action( 'woocommerce_order_status_processing', $order_sync, 'sync_order', 10, 1 );
|
||||
$this->add_action( 'woocommerce_payment_complete', $order_sync, 'sync_order', 10, 1 );
|
||||
|
||||
// Register admin handler
|
||||
if ( is_admin() ) {
|
||||
$admin = new WBC_Admin();
|
||||
$this->add_action( 'admin_menu', $admin, 'add_admin_menu' );
|
||||
$this->add_action( 'admin_init', $admin, 'register_settings' );
|
||||
$this->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_scripts' );
|
||||
$this->add_action( 'wp_ajax_wbc_test_connection', $admin, 'ajax_test_connection' );
|
||||
$this->add_action( 'wp_ajax_wbc_manual_sync', $admin, 'ajax_manual_sync' );
|
||||
$this->add_action( 'wp_ajax_wbc_clear_logs', $admin, 'ajax_clear_logs' );
|
||||
$this->add_action( 'wp_ajax_wbc_get_companies', $admin, 'ajax_get_companies' );
|
||||
$this->add_action( 'admin_init', $admin, 'handle_csv_export' );
|
||||
|
||||
// Add settings link to plugins page
|
||||
$this->add_filter( 'plugin_action_links_' . WBC_PLUGIN_BASENAME, $admin, 'add_settings_link' );
|
||||
}
|
||||
|
||||
// Register all actions
|
||||
foreach ( $this->actions as $hook ) {
|
||||
add_action(
|
||||
$hook['hook'],
|
||||
array( $hook['component'], $hook['callback'] ),
|
||||
$hook['priority'],
|
||||
$hook['accepted_args']
|
||||
);
|
||||
}
|
||||
|
||||
// Register all filters
|
||||
foreach ( $this->filters as $hook ) {
|
||||
add_filter(
|
||||
$hook['hook'],
|
||||
array( $hook['component'], $hook['callback'] ),
|
||||
$hook['priority'],
|
||||
$hook['accepted_args']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
325
woo-business-central/includes/class-wbc-logger.php
Normal file
325
woo-business-central/includes/class-wbc-logger.php
Normal file
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* Logger class for WooCommerce Business Central Integration
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_Logger
|
||||
*
|
||||
* Handles logging to database with different severity levels.
|
||||
*/
|
||||
class WBC_Logger {
|
||||
|
||||
/**
|
||||
* Log level constants
|
||||
*/
|
||||
const DEBUG = 'DEBUG';
|
||||
const INFO = 'INFO';
|
||||
const WARNING = 'WARNING';
|
||||
const ERROR = 'ERROR';
|
||||
|
||||
/**
|
||||
* Database table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $table_name = null;
|
||||
|
||||
/**
|
||||
* Create the logs database table
|
||||
*/
|
||||
public static function create_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'wbc_logs';
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
timestamp datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
level varchar(20) NOT NULL,
|
||||
context varchar(100) NOT NULL,
|
||||
message text NOT NULL,
|
||||
data longtext,
|
||||
PRIMARY KEY (id),
|
||||
KEY level (level),
|
||||
KEY context (context),
|
||||
KEY timestamp (timestamp)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_table_name() {
|
||||
if ( self::$table_name === null ) {
|
||||
global $wpdb;
|
||||
self::$table_name = $wpdb->prefix . 'wbc_logs';
|
||||
}
|
||||
return self::$table_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a debug message
|
||||
*
|
||||
* @param string $context The context/source of the log.
|
||||
* @param string $message The log message.
|
||||
* @param mixed $data Optional. Additional data to log.
|
||||
*/
|
||||
public static function debug( $context, $message, $data = null ) {
|
||||
self::log( self::DEBUG, $context, $message, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an info message
|
||||
*
|
||||
* @param string $context The context/source of the log.
|
||||
* @param string $message The log message.
|
||||
* @param mixed $data Optional. Additional data to log.
|
||||
*/
|
||||
public static function info( $context, $message, $data = null ) {
|
||||
self::log( self::INFO, $context, $message, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning message
|
||||
*
|
||||
* @param string $context The context/source of the log.
|
||||
* @param string $message The log message.
|
||||
* @param mixed $data Optional. Additional data to log.
|
||||
*/
|
||||
public static function warning( $context, $message, $data = null ) {
|
||||
self::log( self::WARNING, $context, $message, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message
|
||||
*
|
||||
* @param string $context The context/source of the log.
|
||||
* @param string $message The log message.
|
||||
* @param mixed $data Optional. Additional data to log.
|
||||
*/
|
||||
public static function error( $context, $message, $data = null ) {
|
||||
self::log( self::ERROR, $context, $message, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to the database
|
||||
*
|
||||
* @param string $level The log level.
|
||||
* @param string $context The context/source of the log.
|
||||
* @param string $message The log message.
|
||||
* @param mixed $data Optional. Additional data to log.
|
||||
*/
|
||||
private static function log( $level, $context, $message, $data = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
// Prepare data for storage
|
||||
$data_json = null;
|
||||
if ( $data !== null ) {
|
||||
$data_json = wp_json_encode( $data, JSON_PRETTY_PRINT );
|
||||
}
|
||||
|
||||
// Insert log entry
|
||||
$wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'timestamp' => current_time( 'mysql' ),
|
||||
'level' => $level,
|
||||
'context' => sanitize_text_field( $context ),
|
||||
'message' => sanitize_textarea_field( $message ),
|
||||
'data' => $data_json,
|
||||
),
|
||||
array( '%s', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
|
||||
// Also log to WooCommerce logger if available
|
||||
if ( function_exists( 'wc_get_logger' ) ) {
|
||||
$wc_logger = wc_get_logger();
|
||||
$log_level = strtolower( $level );
|
||||
if ( method_exists( $wc_logger, $log_level ) ) {
|
||||
$wc_message = "[$context] $message";
|
||||
if ( $data !== null ) {
|
||||
$wc_message .= ' | Data: ' . wp_json_encode( $data );
|
||||
}
|
||||
$wc_logger->$log_level( $wc_message, array( 'source' => 'woo-business-central' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log entries
|
||||
*
|
||||
* @param array $args Query arguments.
|
||||
* @return array Array of log entries.
|
||||
*/
|
||||
public static function get_logs( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$defaults = array(
|
||||
'level' => '',
|
||||
'context' => '',
|
||||
'limit' => 100,
|
||||
'offset' => 0,
|
||||
'orderby' => 'timestamp',
|
||||
'order' => 'DESC',
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
// Build WHERE clause
|
||||
$where = array( '1=1' );
|
||||
$values = array();
|
||||
|
||||
if ( ! empty( $args['level'] ) ) {
|
||||
$where[] = 'level = %s';
|
||||
$values[] = $args['level'];
|
||||
}
|
||||
|
||||
if ( ! empty( $args['context'] ) ) {
|
||||
$where[] = 'context = %s';
|
||||
$values[] = $args['context'];
|
||||
}
|
||||
|
||||
$where_clause = implode( ' AND ', $where );
|
||||
|
||||
// Sanitize orderby and order
|
||||
$allowed_orderby = array( 'id', 'timestamp', 'level', 'context' );
|
||||
$orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'timestamp';
|
||||
$order = strtoupper( $args['order'] ) === 'ASC' ? 'ASC' : 'DESC';
|
||||
|
||||
// Build query
|
||||
$limit = absint( $args['limit'] );
|
||||
$offset = absint( $args['offset'] );
|
||||
|
||||
$sql = "SELECT * FROM $table_name WHERE $where_clause ORDER BY $orderby $order LIMIT $limit OFFSET $offset";
|
||||
|
||||
if ( ! empty( $values ) ) {
|
||||
$sql = $wpdb->prepare( $sql, $values );
|
||||
}
|
||||
|
||||
return $wpdb->get_results( $sql, ARRAY_A );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total log count
|
||||
*
|
||||
* @param array $args Query arguments.
|
||||
* @return int Total count.
|
||||
*/
|
||||
public static function get_log_count( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
// Build WHERE clause
|
||||
$where = array( '1=1' );
|
||||
$values = array();
|
||||
|
||||
if ( ! empty( $args['level'] ) ) {
|
||||
$where[] = 'level = %s';
|
||||
$values[] = $args['level'];
|
||||
}
|
||||
|
||||
if ( ! empty( $args['context'] ) ) {
|
||||
$where[] = 'context = %s';
|
||||
$values[] = $args['context'];
|
||||
}
|
||||
|
||||
$where_clause = implode( ' AND ', $where );
|
||||
|
||||
$sql = "SELECT COUNT(*) FROM $table_name WHERE $where_clause";
|
||||
|
||||
if ( ! empty( $values ) ) {
|
||||
$sql = $wpdb->prepare( $sql, $values );
|
||||
}
|
||||
|
||||
return (int) $wpdb->get_var( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all logs
|
||||
*
|
||||
* @return bool|int Number of rows deleted or false on error.
|
||||
*/
|
||||
public static function clear_logs() {
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
return $wpdb->query( "TRUNCATE TABLE $table_name" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete old logs
|
||||
*
|
||||
* @param int $days Number of days to keep logs.
|
||||
* @return int Number of rows deleted.
|
||||
*/
|
||||
public static function cleanup_old_logs( $days = 30 ) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
$cutoff_date = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
|
||||
|
||||
return $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $table_name WHERE timestamp < %s",
|
||||
$cutoff_date
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export logs to CSV format
|
||||
*
|
||||
* @param array $args Query arguments.
|
||||
* @return string CSV content.
|
||||
*/
|
||||
public static function export_to_csv( $args = array() ) {
|
||||
$logs = self::get_logs( array_merge( $args, array( 'limit' => 10000 ) ) );
|
||||
|
||||
$output = "ID,Timestamp,Level,Context,Message,Data\n";
|
||||
|
||||
foreach ( $logs as $log ) {
|
||||
$row = array(
|
||||
$log['id'],
|
||||
$log['timestamp'],
|
||||
$log['level'],
|
||||
self::sanitize_csv_field( $log['context'] ),
|
||||
self::sanitize_csv_field( $log['message'] ),
|
||||
self::sanitize_csv_field( $log['data'] ?? '' ),
|
||||
);
|
||||
$output .= implode( ',', $row ) . "\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a field value for CSV export to prevent formula injection
|
||||
*
|
||||
* @param string $value The value to sanitize.
|
||||
* @return string Sanitized and quoted CSV value.
|
||||
*/
|
||||
private static function sanitize_csv_field( $value ) {
|
||||
// Prevent formula injection by prefixing dangerous characters with a single quote
|
||||
if ( preg_match( '/^[=+\-@\t\r]/', $value ) ) {
|
||||
$value = "'" . $value;
|
||||
}
|
||||
return '"' . str_replace( '"', '""', $value ) . '"';
|
||||
}
|
||||
}
|
||||
338
woo-business-central/includes/class-wbc-oauth.php
Normal file
338
woo-business-central/includes/class-wbc-oauth.php
Normal file
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
/**
|
||||
* OAuth2 Authentication for Business Central API
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_OAuth
|
||||
*
|
||||
* Handles OAuth2 token management for Microsoft Business Central API.
|
||||
*/
|
||||
class WBC_OAuth {
|
||||
|
||||
/**
|
||||
* Token endpoint URL template
|
||||
*/
|
||||
const TOKEN_ENDPOINT = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token';
|
||||
|
||||
/**
|
||||
* API scope
|
||||
*/
|
||||
const API_SCOPE = 'https://api.businesscentral.dynamics.com/.default';
|
||||
|
||||
/**
|
||||
* Token transient name
|
||||
*/
|
||||
const TOKEN_TRANSIENT = 'wbc_access_token';
|
||||
|
||||
/**
|
||||
* Token expiry buffer (refresh 5 minutes before expiry)
|
||||
*/
|
||||
const EXPIRY_BUFFER = 300;
|
||||
|
||||
/**
|
||||
* Encryption key option name
|
||||
*/
|
||||
const ENCRYPTION_KEY_OPTION = 'wbc_encryption_key';
|
||||
|
||||
/**
|
||||
* Get the encryption key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_encryption_key() {
|
||||
$key = get_option( self::ENCRYPTION_KEY_OPTION );
|
||||
|
||||
if ( empty( $key ) ) {
|
||||
// Generate a new key if one doesn't exist
|
||||
$key = wp_generate_password( 32, true, true );
|
||||
update_option( self::ENCRYPTION_KEY_OPTION, $key );
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a value
|
||||
*
|
||||
* @param string $value The value to encrypt.
|
||||
* @return string Encrypted value.
|
||||
*/
|
||||
public static function encrypt( $value ) {
|
||||
if ( empty( $value ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$key = self::get_encryption_key();
|
||||
|
||||
// Use WordPress salt for additional entropy
|
||||
$salt = wp_salt( 'auth' );
|
||||
$key = hash( 'sha256', $key . $salt, true );
|
||||
|
||||
// Generate random IV
|
||||
$iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
|
||||
$iv = openssl_random_pseudo_bytes( $iv_length );
|
||||
|
||||
// Encrypt (OPENSSL_RAW_DATA ensures raw binary output)
|
||||
$encrypted = openssl_encrypt( $value, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
|
||||
|
||||
if ( $encrypted === false ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Combine IV and encrypted data, then base64 encode
|
||||
return base64_encode( $iv . $encrypted );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a value
|
||||
*
|
||||
* @param string $encrypted The encrypted value.
|
||||
* @return string Decrypted value.
|
||||
*/
|
||||
public static function decrypt( $encrypted ) {
|
||||
if ( empty( $encrypted ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$key = self::get_encryption_key();
|
||||
|
||||
// Use WordPress salt for additional entropy
|
||||
$salt = wp_salt( 'auth' );
|
||||
$key = hash( 'sha256', $key . $salt, true );
|
||||
|
||||
// Decode
|
||||
$data = base64_decode( $encrypted );
|
||||
if ( $data === false ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Extract IV and encrypted data
|
||||
$iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
|
||||
$iv = substr( $data, 0, $iv_length );
|
||||
$encrypted_data = substr( $data, $iv_length );
|
||||
|
||||
// Decrypt (OPENSSL_RAW_DATA matches the encrypt flag)
|
||||
$decrypted = openssl_decrypt( $encrypted_data, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
|
||||
|
||||
return $decrypted !== false ? $decrypted : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored tenant ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_tenant_id() {
|
||||
return get_option( 'wbc_tenant_id', '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored client ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_client_id() {
|
||||
return get_option( 'wbc_client_id', '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored client secret (decrypted)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_client_secret() {
|
||||
$encrypted = get_option( 'wbc_client_secret', '' );
|
||||
return self::decrypt( $encrypted );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save client secret (encrypted)
|
||||
*
|
||||
* @param string $secret The client secret to save.
|
||||
*/
|
||||
public static function save_client_secret( $secret ) {
|
||||
$encrypted = self::encrypt( $secret );
|
||||
update_option( 'wbc_client_secret', $encrypted );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment (production/sandbox)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_environment() {
|
||||
return get_option( 'wbc_environment', 'production' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_company_id() {
|
||||
return get_option( 'wbc_company_id', '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if credentials are configured
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_configured() {
|
||||
return ! empty( self::get_tenant_id() )
|
||||
&& ! empty( self::get_client_id() )
|
||||
&& ! empty( self::get_client_secret() )
|
||||
&& ! empty( self::get_company_id() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token
|
||||
*
|
||||
* @param bool $force_refresh Force refresh even if cached token exists.
|
||||
* @return string|WP_Error Access token or error.
|
||||
*/
|
||||
public static function get_access_token( $force_refresh = false ) {
|
||||
// Check for cached token
|
||||
if ( ! $force_refresh ) {
|
||||
$cached_token = get_transient( self::TOKEN_TRANSIENT );
|
||||
if ( $cached_token !== false ) {
|
||||
return $cached_token;
|
||||
}
|
||||
}
|
||||
|
||||
// Request new token
|
||||
return self::request_token();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a new access token from Microsoft
|
||||
*
|
||||
* @return string|WP_Error Access token or error.
|
||||
*/
|
||||
private static function request_token() {
|
||||
$tenant_id = self::get_tenant_id();
|
||||
$client_id = self::get_client_id();
|
||||
$client_secret = self::get_client_secret();
|
||||
|
||||
// Validate credentials
|
||||
if ( empty( $tenant_id ) || empty( $client_id ) || empty( $client_secret ) ) {
|
||||
$error = new WP_Error( 'wbc_missing_credentials', __( 'Business Central API credentials are not configured.', 'woo-business-central' ) );
|
||||
WBC_Logger::error( 'OAuth', 'Missing API credentials' );
|
||||
return $error;
|
||||
}
|
||||
|
||||
// Build token endpoint URL
|
||||
$token_url = sprintf( self::TOKEN_ENDPOINT, $tenant_id );
|
||||
|
||||
// Prepare request body
|
||||
$body = array(
|
||||
'grant_type' => 'client_credentials',
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'scope' => self::API_SCOPE,
|
||||
);
|
||||
|
||||
WBC_Logger::debug( 'OAuth', 'Requesting new access token', array( 'tenant_id' => $tenant_id ) );
|
||||
|
||||
// Make the request using cURL
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt_array( $ch, array(
|
||||
CURLOPT_URL => $token_url,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query( $body ),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
),
|
||||
) );
|
||||
|
||||
$response = curl_exec( $ch );
|
||||
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
$curl_error = curl_error( $ch );
|
||||
|
||||
curl_close( $ch );
|
||||
|
||||
// Check for cURL errors
|
||||
if ( $response === false ) {
|
||||
$error = new WP_Error( 'wbc_curl_error', sprintf( __( 'cURL error: %s', 'woo-business-central' ), $curl_error ) );
|
||||
WBC_Logger::error( 'OAuth', 'cURL error during token request', array( 'error' => $curl_error ) );
|
||||
return $error;
|
||||
}
|
||||
|
||||
// Parse response
|
||||
$data = json_decode( $response, true );
|
||||
|
||||
// Check for HTTP errors
|
||||
if ( $http_code !== 200 ) {
|
||||
$error_message = isset( $data['error_description'] ) ? $data['error_description'] : __( 'Unknown error', 'woo-business-central' );
|
||||
$error = new WP_Error( 'wbc_token_error', $error_message, array( 'status' => $http_code ) );
|
||||
WBC_Logger::error( 'OAuth', 'Token request failed', array(
|
||||
'http_code' => $http_code,
|
||||
'error' => $data['error'] ?? 'unknown',
|
||||
'message' => $error_message,
|
||||
) );
|
||||
return $error;
|
||||
}
|
||||
|
||||
// Extract token
|
||||
if ( ! isset( $data['access_token'] ) ) {
|
||||
$error = new WP_Error( 'wbc_invalid_response', __( 'Invalid token response from Microsoft.', 'woo-business-central' ) );
|
||||
WBC_Logger::error( 'OAuth', 'Invalid token response', array( 'response' => $data ) );
|
||||
return $error;
|
||||
}
|
||||
|
||||
$access_token = $data['access_token'];
|
||||
$expires_in = isset( $data['expires_in'] ) ? (int) $data['expires_in'] : 3600;
|
||||
|
||||
// Cache the token (with buffer for expiry)
|
||||
$cache_duration = max( 60, $expires_in - self::EXPIRY_BUFFER );
|
||||
set_transient( self::TOKEN_TRANSIENT, $access_token, $cache_duration );
|
||||
|
||||
WBC_Logger::info( 'OAuth', 'Successfully obtained access token', array(
|
||||
'expires_in' => $expires_in,
|
||||
'cached_for' => $cache_duration,
|
||||
) );
|
||||
|
||||
return $access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached token
|
||||
*/
|
||||
public static function clear_token_cache() {
|
||||
delete_transient( self::TOKEN_TRANSIENT );
|
||||
WBC_Logger::debug( 'OAuth', 'Token cache cleared' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the connection to Business Central
|
||||
*
|
||||
* @return array|WP_Error Array with success status or WP_Error.
|
||||
*/
|
||||
public static function test_connection() {
|
||||
// Clear any cached token
|
||||
self::clear_token_cache();
|
||||
|
||||
// Try to get a new token
|
||||
$token = self::get_access_token( true );
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'Successfully connected to Business Central.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
333
woo-business-central/includes/class-wbc-order-sync.php
Normal file
333
woo-business-central/includes/class-wbc-order-sync.php
Normal file
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
/**
|
||||
* Order Sync to Business Central
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_Order_Sync
|
||||
*
|
||||
* Handles syncing orders from WooCommerce to Business Central.
|
||||
*/
|
||||
class WBC_Order_Sync {
|
||||
|
||||
/**
|
||||
* Customer sync instance
|
||||
*
|
||||
* @var WBC_Customer_Sync
|
||||
*/
|
||||
private $customer_sync;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->customer_sync = new WBC_Customer_Sync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync an order to Business Central
|
||||
*
|
||||
* @param int $order_id WooCommerce order ID.
|
||||
* @return array|WP_Error Sync result or error.
|
||||
*/
|
||||
public function sync_order( $order_id ) {
|
||||
// Check if order sync is enabled
|
||||
if ( get_option( 'wbc_enable_order_sync', 'yes' ) !== 'yes' ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Order sync is disabled.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if credentials are configured
|
||||
if ( ! WBC_OAuth::is_configured() ) {
|
||||
WBC_Logger::error( 'OrderSync', 'Order sync failed - API credentials not configured', array(
|
||||
'order_id' => $order_id,
|
||||
) );
|
||||
return new WP_Error( 'wbc_not_configured', __( 'API credentials not configured.', 'woo-business-central' ) );
|
||||
}
|
||||
|
||||
// Get the order
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
return new WP_Error( 'wbc_order_not_found', __( 'Order not found.', 'woo-business-central' ) );
|
||||
}
|
||||
|
||||
// Check if order is already synced
|
||||
$bc_order_id = $order->get_meta( '_wbc_bc_order_id' );
|
||||
if ( ! empty( $bc_order_id ) ) {
|
||||
WBC_Logger::debug( 'OrderSync', 'Order already synced', array(
|
||||
'order_id' => $order_id,
|
||||
'bc_order_id' => $bc_order_id,
|
||||
) );
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'Order already synced to Business Central.', 'woo-business-central' ),
|
||||
'bc_order_id' => $bc_order_id,
|
||||
);
|
||||
}
|
||||
|
||||
// Acquire a lock to prevent duplicate syncs from concurrent hooks
|
||||
$lock_key = 'wbc_syncing_order_' . $order_id;
|
||||
if ( get_transient( $lock_key ) ) {
|
||||
WBC_Logger::debug( 'OrderSync', 'Order sync already in progress', array( 'order_id' => $order_id ) );
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'Order sync already in progress.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
set_transient( $lock_key, true, 120 );
|
||||
|
||||
WBC_Logger::info( 'OrderSync', 'Starting order sync', array( 'order_id' => $order_id ) );
|
||||
|
||||
// Step 1: Get or create customer in BC
|
||||
$customer_number = $this->customer_sync->get_or_create_customer( $order );
|
||||
|
||||
if ( is_wp_error( $customer_number ) ) {
|
||||
$this->add_order_note( $order, sprintf(
|
||||
__( 'Failed to sync customer to Business Central: %s', 'woo-business-central' ),
|
||||
$customer_number->get_error_message()
|
||||
) );
|
||||
return $customer_number;
|
||||
}
|
||||
|
||||
// Step 2: Create sales order in BC
|
||||
$sales_order = $this->create_sales_order( $order, $customer_number );
|
||||
|
||||
if ( is_wp_error( $sales_order ) ) {
|
||||
$this->add_order_note( $order, sprintf(
|
||||
__( 'Failed to create sales order in Business Central: %s', 'woo-business-central' ),
|
||||
$sales_order->get_error_message()
|
||||
) );
|
||||
return $sales_order;
|
||||
}
|
||||
|
||||
$bc_order_id = $sales_order['id'];
|
||||
$bc_order_number = $sales_order['number'] ?? '';
|
||||
|
||||
// Step 3: Add sales order lines
|
||||
$lines_result = $this->create_sales_order_lines( $order, $bc_order_id );
|
||||
|
||||
if ( is_wp_error( $lines_result ) ) {
|
||||
// Log the error but don't fail completely
|
||||
WBC_Logger::warning( 'OrderSync', 'Some order lines failed to sync', array(
|
||||
'order_id' => $order_id,
|
||||
'bc_order_id' => $bc_order_id,
|
||||
'error' => $lines_result->get_error_message(),
|
||||
) );
|
||||
}
|
||||
|
||||
// Step 4: Save BC order ID to WC order meta
|
||||
$order->update_meta_data( '_wbc_bc_order_id', $bc_order_id );
|
||||
$order->update_meta_data( '_wbc_bc_order_number', $bc_order_number );
|
||||
$order->update_meta_data( '_wbc_synced_at', current_time( 'mysql' ) );
|
||||
$order->save();
|
||||
|
||||
// Add order note
|
||||
$this->add_order_note( $order, sprintf(
|
||||
__( 'Order synced to Business Central. BC Order Number: %s', 'woo-business-central' ),
|
||||
$bc_order_number
|
||||
) );
|
||||
|
||||
// Release the sync lock
|
||||
delete_transient( 'wbc_syncing_order_' . $order_id );
|
||||
|
||||
WBC_Logger::info( 'OrderSync', 'Order sync completed', array(
|
||||
'order_id' => $order_id,
|
||||
'bc_order_id' => $bc_order_id,
|
||||
'bc_order_number' => $bc_order_number,
|
||||
) );
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'Order synced to Business Central.', 'woo-business-central' ),
|
||||
'bc_order_id' => $bc_order_id,
|
||||
'bc_order_number' => $bc_order_number,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sales order in BC
|
||||
*
|
||||
* @param WC_Order $order WooCommerce order.
|
||||
* @param string $customer_number BC customer number.
|
||||
* @return array|WP_Error Created sales order or error.
|
||||
*/
|
||||
private function create_sales_order( $order, $customer_number ) {
|
||||
$order_data = array(
|
||||
'customerNumber' => $customer_number,
|
||||
'orderDate' => $order->get_date_created()->format( 'Y-m-d' ),
|
||||
'externalDocumentNumber' => $order->get_order_number(),
|
||||
'currencyCode' => $order->get_currency(),
|
||||
);
|
||||
|
||||
// Add payment terms if configured
|
||||
$payment_terms_id = get_option( 'wbc_default_payment_terms_id', '' );
|
||||
if ( ! empty( $payment_terms_id ) ) {
|
||||
$order_data['paymentTermsId'] = $payment_terms_id;
|
||||
}
|
||||
|
||||
// Add shipment method if configured
|
||||
$shipment_method_id = get_option( 'wbc_default_shipment_method_id', '' );
|
||||
if ( ! empty( $shipment_method_id ) ) {
|
||||
$order_data['shipmentMethodId'] = $shipment_method_id;
|
||||
}
|
||||
|
||||
// Add shipping address if different from billing
|
||||
$shipping_address_1 = $order->get_shipping_address_1();
|
||||
if ( ! empty( $shipping_address_1 ) ) {
|
||||
$order_data['shipToName'] = trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() );
|
||||
$order_data['shipToAddressLine1'] = $shipping_address_1;
|
||||
$order_data['shipToCity'] = $order->get_shipping_city();
|
||||
$order_data['shipToState'] = $order->get_shipping_state();
|
||||
$order_data['shipToPostCode'] = $order->get_shipping_postcode();
|
||||
$order_data['shipToCountry'] = $order->get_shipping_country();
|
||||
}
|
||||
|
||||
WBC_Logger::debug( 'OrderSync', 'Creating sales order in BC', array(
|
||||
'order_id' => $order->get_id(),
|
||||
'data' => $order_data,
|
||||
) );
|
||||
|
||||
return WBC_API_Client::create_sales_order( $order_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sales order lines in BC
|
||||
*
|
||||
* @param WC_Order $order WooCommerce order.
|
||||
* @param string $bc_order_id BC sales order ID.
|
||||
* @return true|WP_Error True on success or error.
|
||||
*/
|
||||
private function create_sales_order_lines( $order, $bc_order_id ) {
|
||||
$errors = array();
|
||||
$items = $order->get_items();
|
||||
|
||||
foreach ( $items as $item_id => $item ) {
|
||||
$product = $item->get_product();
|
||||
|
||||
if ( ! $product ) {
|
||||
WBC_Logger::warning( 'OrderSync', 'Skipping order line - product not found', array(
|
||||
'order_id' => $order->get_id(),
|
||||
'item_id' => $item_id,
|
||||
) );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the BC item number (use SKU)
|
||||
$bc_item_number = $product->get_sku();
|
||||
|
||||
if ( empty( $bc_item_number ) ) {
|
||||
WBC_Logger::warning( 'OrderSync', 'Skipping order line - product has no SKU', array(
|
||||
'order_id' => $order->get_id(),
|
||||
'product_id' => $product->get_id(),
|
||||
) );
|
||||
$errors[] = sprintf( 'Product "%s" has no SKU', $product->get_name() );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build line data
|
||||
$line_data = array(
|
||||
'lineType' => 'Item',
|
||||
'lineObjectNumber' => $bc_item_number,
|
||||
'quantity' => $item->get_quantity(),
|
||||
'unitPrice' => (float) $order->get_item_total( $item, false, false ),
|
||||
);
|
||||
|
||||
// Add description if different from product name
|
||||
$line_description = $item->get_name();
|
||||
if ( ! empty( $line_description ) ) {
|
||||
$line_data['description'] = substr( $line_description, 0, 100 );
|
||||
}
|
||||
|
||||
WBC_Logger::debug( 'OrderSync', 'Creating sales order line', array(
|
||||
'bc_order_id' => $bc_order_id,
|
||||
'data' => $line_data,
|
||||
) );
|
||||
|
||||
$result = WBC_API_Client::create_sales_order_line( $bc_order_id, $line_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
WBC_Logger::error( 'OrderSync', 'Failed to create sales order line', array(
|
||||
'bc_order_id' => $bc_order_id,
|
||||
'bc_item_number' => $bc_item_number,
|
||||
'error' => $result->get_error_message(),
|
||||
) );
|
||||
$errors[] = sprintf( 'Item %s: %s', $bc_item_number, $result->get_error_message() );
|
||||
}
|
||||
}
|
||||
|
||||
// Handle shipping as a line item if there's shipping cost
|
||||
$shipping_total = (float) $order->get_shipping_total();
|
||||
if ( $shipping_total > 0 ) {
|
||||
$shipping_item_number = get_option( 'wbc_shipping_item_number', '' );
|
||||
|
||||
if ( ! empty( $shipping_item_number ) ) {
|
||||
$shipping_line = array(
|
||||
'lineType' => 'Item',
|
||||
'lineObjectNumber' => $shipping_item_number,
|
||||
'quantity' => 1,
|
||||
'unitPrice' => $shipping_total,
|
||||
'description' => __( 'Shipping', 'woo-business-central' ),
|
||||
);
|
||||
|
||||
$result = WBC_API_Client::create_sales_order_line( $bc_order_id, $shipping_line );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
WBC_Logger::warning( 'OrderSync', 'Failed to add shipping line', array(
|
||||
'bc_order_id' => $bc_order_id,
|
||||
'error' => $result->get_error_message(),
|
||||
) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new WP_Error( 'wbc_line_errors', implode( '; ', $errors ) );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add order note
|
||||
*
|
||||
* @param WC_Order $order WooCommerce order.
|
||||
* @param string $message Note message.
|
||||
*/
|
||||
private function add_order_note( $order, $message ) {
|
||||
$order->add_order_note( '[WBC] ' . $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual sync for an order (callable from admin)
|
||||
*
|
||||
* @param int $order_id WooCommerce order ID.
|
||||
* @return array Sync result.
|
||||
*/
|
||||
public function manual_sync( $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Order not found.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Clear existing sync data to force re-sync
|
||||
$order->delete_meta_data( '_wbc_bc_order_id' );
|
||||
$order->delete_meta_data( '_wbc_bc_order_number' );
|
||||
$order->save();
|
||||
|
||||
return $this->sync_order( $order_id );
|
||||
}
|
||||
}
|
||||
360
woo-business-central/includes/class-wbc-product-sync.php
Normal file
360
woo-business-central/includes/class-wbc-product-sync.php
Normal file
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
/**
|
||||
* Product Sync from Business Central to WooCommerce
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WBC_Product_Sync
|
||||
*
|
||||
* Handles syncing stock and pricing from Business Central to WooCommerce.
|
||||
*/
|
||||
class WBC_Product_Sync {
|
||||
|
||||
/**
|
||||
* Items per page for BC API pagination
|
||||
*/
|
||||
const ITEMS_PER_PAGE = 100;
|
||||
|
||||
/**
|
||||
* Fields to select from BC items
|
||||
*/
|
||||
const SELECT_FIELDS = 'id,number,gtin,displayName,unitPrice,inventory';
|
||||
|
||||
/**
|
||||
* Sync results
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $results = array(
|
||||
'success' => 0,
|
||||
'failed' => 0,
|
||||
'skipped' => 0,
|
||||
'total' => 0,
|
||||
'errors' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Run the product sync
|
||||
*
|
||||
* @return array Sync results.
|
||||
*/
|
||||
public function run_sync() {
|
||||
// Check if sync is enabled
|
||||
if ( get_option( 'wbc_enable_stock_sync', 'yes' ) !== 'yes' && get_option( 'wbc_enable_price_sync', 'yes' ) !== 'yes' ) {
|
||||
WBC_Logger::info( 'ProductSync', 'Sync skipped - both stock and price sync are disabled' );
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'Sync skipped - both stock and price sync are disabled.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if credentials are configured
|
||||
if ( ! WBC_OAuth::is_configured() ) {
|
||||
WBC_Logger::error( 'ProductSync', 'Sync failed - API credentials not configured' );
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Sync failed - API credentials not configured.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
|
||||
WBC_Logger::info( 'ProductSync', 'Starting product sync' );
|
||||
|
||||
$start_time = microtime( true );
|
||||
$skip = 0;
|
||||
$has_more = true;
|
||||
|
||||
// Fetch all items from BC with pagination
|
||||
while ( $has_more ) {
|
||||
$items = $this->fetch_items( $skip );
|
||||
|
||||
if ( is_wp_error( $items ) ) {
|
||||
$this->results['errors'][] = $items->get_error_message();
|
||||
WBC_Logger::error( 'ProductSync', 'Failed to fetch items from BC', array(
|
||||
'error' => $items->get_error_message(),
|
||||
'skip' => $skip,
|
||||
) );
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we have items
|
||||
$item_list = isset( $items['value'] ) ? $items['value'] : array();
|
||||
|
||||
if ( empty( $item_list ) ) {
|
||||
$has_more = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Process each item
|
||||
foreach ( $item_list as $item ) {
|
||||
$this->process_item( $item );
|
||||
}
|
||||
|
||||
// Check for more pages
|
||||
$skip += self::ITEMS_PER_PAGE;
|
||||
$has_more = count( $item_list ) >= self::ITEMS_PER_PAGE;
|
||||
|
||||
// Add a small delay to avoid rate limiting
|
||||
if ( $has_more ) {
|
||||
usleep( 100000 ); // 100ms
|
||||
}
|
||||
}
|
||||
|
||||
$duration = round( microtime( true ) - $start_time, 2 );
|
||||
|
||||
WBC_Logger::info( 'ProductSync', 'Product sync completed', array(
|
||||
'duration_seconds' => $duration,
|
||||
'total' => $this->results['total'],
|
||||
'success' => $this->results['success'],
|
||||
'failed' => $this->results['failed'],
|
||||
'skipped' => $this->results['skipped'],
|
||||
) );
|
||||
|
||||
return array(
|
||||
'success' => empty( $this->results['errors'] ),
|
||||
'message' => sprintf(
|
||||
__( 'Sync completed in %s seconds. Success: %d, Failed: %d, Skipped: %d', 'woo-business-central' ),
|
||||
$duration,
|
||||
$this->results['success'],
|
||||
$this->results['failed'],
|
||||
$this->results['skipped']
|
||||
),
|
||||
'results' => $this->results,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch items from Business Central
|
||||
*
|
||||
* @param int $skip Number of items to skip.
|
||||
* @return array|WP_Error Items data or error.
|
||||
*/
|
||||
private function fetch_items( $skip = 0 ) {
|
||||
return WBC_API_Client::get_items( self::ITEMS_PER_PAGE, $skip, self::SELECT_FIELDS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single BC item
|
||||
*
|
||||
* @param array $item BC item data.
|
||||
*/
|
||||
private function process_item( $item ) {
|
||||
$this->results['total']++;
|
||||
|
||||
// Get item number and GTIN
|
||||
$item_number = isset( $item['number'] ) ? $item['number'] : '';
|
||||
$gtin = isset( $item['gtin'] ) ? $item['gtin'] : '';
|
||||
|
||||
if ( empty( $item_number ) && empty( $gtin ) ) {
|
||||
$this->results['skipped']++;
|
||||
WBC_Logger::debug( 'ProductSync', 'Skipping item without number or GTIN', array( 'item' => $item ) );
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find WooCommerce product
|
||||
$product_id = $this->find_wc_product( $item_number, $gtin );
|
||||
|
||||
if ( ! $product_id ) {
|
||||
$this->results['skipped']++;
|
||||
WBC_Logger::debug( 'ProductSync', 'No matching WC product found', array(
|
||||
'item_number' => $item_number,
|
||||
'gtin' => $gtin,
|
||||
) );
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the product
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
$this->results['skipped']++;
|
||||
WBC_Logger::warning( 'ProductSync', 'Product ID found but product not loaded', array(
|
||||
'product_id' => $product_id,
|
||||
) );
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the product
|
||||
$updated = $this->update_product( $product, $item );
|
||||
|
||||
if ( $updated ) {
|
||||
$this->results['success']++;
|
||||
} else {
|
||||
$this->results['failed']++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find WooCommerce product by SKU (item number or GTIN)
|
||||
*
|
||||
* @param string $item_number BC item number.
|
||||
* @param string $gtin BC GTIN/EAN.
|
||||
* @return int|false Product ID or false if not found.
|
||||
*/
|
||||
private function find_wc_product( $item_number, $gtin ) {
|
||||
// First try to match by item number (SKU)
|
||||
if ( ! empty( $item_number ) ) {
|
||||
$product_id = wc_get_product_id_by_sku( $item_number );
|
||||
if ( $product_id ) {
|
||||
return $product_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to GTIN match
|
||||
if ( ! empty( $gtin ) ) {
|
||||
$product_id = wc_get_product_id_by_sku( $gtin );
|
||||
if ( $product_id ) {
|
||||
return $product_id;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update WooCommerce product with BC data
|
||||
*
|
||||
* @param WC_Product $product WooCommerce product.
|
||||
* @param array $item BC item data.
|
||||
* @return bool Whether the product was updated.
|
||||
*/
|
||||
private function update_product( $product, $item ) {
|
||||
$product_id = $product->get_id();
|
||||
$updated = false;
|
||||
|
||||
try {
|
||||
// Update stock if enabled
|
||||
if ( get_option( 'wbc_enable_stock_sync', 'yes' ) === 'yes' ) {
|
||||
$stock = isset( $item['inventory'] ) ? (float) $item['inventory'] : 0;
|
||||
$current_stock = (float) $product->get_stock_quantity();
|
||||
|
||||
if ( $stock !== $current_stock ) {
|
||||
// Enable stock management if not already enabled
|
||||
if ( ! $product->get_manage_stock() ) {
|
||||
$product->set_manage_stock( true );
|
||||
}
|
||||
|
||||
wc_update_product_stock( $product, $stock );
|
||||
$updated = true;
|
||||
|
||||
WBC_Logger::debug( 'ProductSync', 'Updated stock', array(
|
||||
'product_id' => $product_id,
|
||||
'old_stock' => $current_stock,
|
||||
'new_stock' => $stock,
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
// Update price if enabled
|
||||
if ( get_option( 'wbc_enable_price_sync', 'yes' ) === 'yes' ) {
|
||||
$price = isset( $item['unitPrice'] ) ? (float) $item['unitPrice'] : 0;
|
||||
$current_price = (float) $product->get_regular_price();
|
||||
|
||||
if ( $price > 0 && $price !== $current_price ) {
|
||||
$product->set_regular_price( $price );
|
||||
|
||||
// Also update sale price if it's higher than the new regular price
|
||||
$sale_price = (float) $product->get_sale_price();
|
||||
if ( $sale_price > 0 && $sale_price >= $price ) {
|
||||
$product->set_sale_price( '' );
|
||||
}
|
||||
|
||||
$product->save();
|
||||
$updated = true;
|
||||
|
||||
WBC_Logger::debug( 'ProductSync', 'Updated price', array(
|
||||
'product_id' => $product_id,
|
||||
'old_price' => $current_price,
|
||||
'new_price' => $price,
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
// Store BC item info in meta via WooCommerce CRUD
|
||||
$product->update_meta_data( '_wbc_bc_item_id', $item['id'] ?? '' );
|
||||
$product->update_meta_data( '_wbc_bc_item_number', $item['number'] ?? '' );
|
||||
$product->update_meta_data( '_wbc_last_sync', current_time( 'mysql' ) );
|
||||
$product->save();
|
||||
|
||||
return true;
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
WBC_Logger::error( 'ProductSync', 'Failed to update product', array(
|
||||
'product_id' => $product_id,
|
||||
'error' => $e->getMessage(),
|
||||
) );
|
||||
$this->results['errors'][] = sprintf( 'Product %d: %s', $product_id, $e->getMessage() );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync results
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_results() {
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a single product by WooCommerce product ID
|
||||
*
|
||||
* @param int $product_id WooCommerce product ID.
|
||||
* @return array Sync result.
|
||||
*/
|
||||
public function sync_single_product( $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Product not found.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
|
||||
$sku = $product->get_sku();
|
||||
|
||||
if ( empty( $sku ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Product has no SKU.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch item from BC
|
||||
$result = WBC_API_Client::get_item_by_number( $sku );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => $result->get_error_message(),
|
||||
);
|
||||
}
|
||||
|
||||
$items = isset( $result['value'] ) ? $result['value'] : array();
|
||||
|
||||
if ( empty( $items ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Item not found in Business Central.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
|
||||
$item = $items[0];
|
||||
$updated = $this->update_product( $product, $item );
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => $updated
|
||||
? __( 'Product updated successfully.', 'woo-business-central' )
|
||||
: __( 'Product is already up to date.', 'woo-business-central' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
0
woo-business-central/languages/.gitkeep
Normal file
0
woo-business-central/languages/.gitkeep
Normal file
75
woo-business-central/uninstall.php
Normal file
75
woo-business-central/uninstall.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Uninstall WooCommerce Business Central Integration
|
||||
*
|
||||
* @package WooBusinessCentral
|
||||
*/
|
||||
|
||||
// Exit if uninstall not called from WordPress
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if user wants to delete data (optional, could be a setting)
|
||||
$delete_data = get_option( 'wbc_delete_data_on_uninstall', 'no' );
|
||||
|
||||
// Always delete these options
|
||||
$options_to_delete = array(
|
||||
'wbc_tenant_id',
|
||||
'wbc_client_id',
|
||||
'wbc_client_secret',
|
||||
'wbc_environment',
|
||||
'wbc_company_id',
|
||||
'wbc_sync_frequency',
|
||||
'wbc_enable_stock_sync',
|
||||
'wbc_enable_price_sync',
|
||||
'wbc_enable_order_sync',
|
||||
'wbc_default_payment_terms_id',
|
||||
'wbc_default_shipment_method_id',
|
||||
'wbc_shipping_item_number',
|
||||
'wbc_last_sync_time',
|
||||
'wbc_encryption_key',
|
||||
'wbc_delete_data_on_uninstall',
|
||||
);
|
||||
|
||||
foreach ( $options_to_delete as $option ) {
|
||||
delete_option( $option );
|
||||
}
|
||||
|
||||
// Delete transients
|
||||
delete_transient( 'wbc_access_token' );
|
||||
|
||||
// Clear scheduled events
|
||||
$sync_timestamp = wp_next_scheduled( 'wbc_product_sync_event' );
|
||||
if ( $sync_timestamp ) {
|
||||
wp_unschedule_event( $sync_timestamp, 'wbc_product_sync_event' );
|
||||
}
|
||||
|
||||
$cleanup_timestamp = wp_next_scheduled( 'wbc_log_cleanup_event' );
|
||||
if ( $cleanup_timestamp ) {
|
||||
wp_unschedule_event( $cleanup_timestamp, 'wbc_log_cleanup_event' );
|
||||
}
|
||||
|
||||
// Drop logs table
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'wbc_logs';
|
||||
$wpdb->query( "DROP TABLE IF EXISTS $table_name" );
|
||||
|
||||
// Delete user meta (BC customer IDs)
|
||||
$wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key IN ('_wbc_bc_customer_id', '_wbc_bc_customer_number')" );
|
||||
|
||||
// Delete post meta (BC item info, order sync info)
|
||||
$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_wbc_%'" );
|
||||
|
||||
// Delete order meta for HPOS (if using custom order tables)
|
||||
if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) ) {
|
||||
if ( Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
|
||||
$orders_table = $wpdb->prefix . 'wc_orders_meta';
|
||||
if ( $wpdb->get_var( "SHOW TABLES LIKE '$orders_table'" ) === $orders_table ) {
|
||||
$wpdb->query( "DELETE FROM $orders_table WHERE meta_key LIKE '_wbc_%'" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any cached data
|
||||
wp_cache_flush();
|
||||
168
woo-business-central/woo-business-central.php
Normal file
168
woo-business-central/woo-business-central.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WooCommerce Business Central Integration
|
||||
* Plugin URI: https://example.com/woo-business-central
|
||||
* Description: Syncs stock, pricing, orders, and customers between WooCommerce and Microsoft Dynamics 365 Business Central.
|
||||
* Version: 1.0.0
|
||||
* Author: Your Name
|
||||
* Author URI: https://example.com
|
||||
* License: GPL-2.0+
|
||||
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
|
||||
* Text Domain: woo-business-central
|
||||
* Domain Path: /languages
|
||||
* Requires at least: 5.8
|
||||
* Requires PHP: 7.4
|
||||
* WC requires at least: 5.0
|
||||
* WC tested up to: 8.0
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Plugin constants
|
||||
define( 'WBC_VERSION', '1.0.0' );
|
||||
define( 'WBC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'WBC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'WBC_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active
|
||||
*/
|
||||
function wbc_check_woocommerce() {
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
add_action( 'admin_notices', 'wbc_woocommerce_missing_notice' );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin notice for missing WooCommerce
|
||||
*/
|
||||
function wbc_woocommerce_missing_notice() {
|
||||
?>
|
||||
<div class="notice notice-error">
|
||||
<p><?php esc_html_e( 'WooCommerce Business Central Integration requires WooCommerce to be installed and active.', 'woo-business-central' ); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin activation hook
|
||||
*/
|
||||
function wbc_activate() {
|
||||
// Check WooCommerce dependency
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
deactivate_plugins( plugin_basename( __FILE__ ) );
|
||||
wp_die(
|
||||
esc_html__( 'WooCommerce Business Central Integration requires WooCommerce to be installed and active.', 'woo-business-central' ),
|
||||
'Plugin dependency check',
|
||||
array( 'back_link' => true )
|
||||
);
|
||||
}
|
||||
|
||||
// Create logs table
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-logger.php';
|
||||
WBC_Logger::create_table();
|
||||
|
||||
// Set default options
|
||||
$defaults = array(
|
||||
'wbc_sync_frequency' => 'daily',
|
||||
'wbc_enable_stock_sync' => 'yes',
|
||||
'wbc_enable_price_sync' => 'yes',
|
||||
'wbc_enable_order_sync' => 'yes',
|
||||
'wbc_environment' => 'production',
|
||||
);
|
||||
|
||||
foreach ( $defaults as $option => $value ) {
|
||||
if ( get_option( $option ) === false ) {
|
||||
add_option( $option, $value );
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule cron event
|
||||
if ( ! wp_next_scheduled( 'wbc_product_sync_event' ) ) {
|
||||
$frequency = get_option( 'wbc_sync_frequency', 'daily' );
|
||||
wp_schedule_event( time(), $frequency, 'wbc_product_sync_event' );
|
||||
}
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
register_activation_hook( __FILE__, 'wbc_activate' );
|
||||
|
||||
/**
|
||||
* Plugin deactivation hook
|
||||
*/
|
||||
function wbc_deactivate() {
|
||||
// Clear scheduled cron events
|
||||
$timestamp = wp_next_scheduled( 'wbc_product_sync_event' );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, 'wbc_product_sync_event' );
|
||||
}
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
register_deactivation_hook( __FILE__, 'wbc_deactivate' );
|
||||
|
||||
/**
|
||||
* Load plugin text domain
|
||||
*/
|
||||
function wbc_load_textdomain() {
|
||||
load_plugin_textdomain( 'woo-business-central', false, dirname( WBC_PLUGIN_BASENAME ) . '/languages' );
|
||||
}
|
||||
add_action( 'plugins_loaded', 'wbc_load_textdomain' );
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
*/
|
||||
function wbc_init() {
|
||||
// Check WooCommerce dependency
|
||||
if ( ! wbc_check_woocommerce() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load required files
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-loader.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-logger.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-oauth.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-api-client.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-product-sync.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-customer-sync.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-order-sync.php';
|
||||
require_once WBC_PLUGIN_DIR . 'includes/class-wbc-cron.php';
|
||||
|
||||
// Load admin files
|
||||
if ( is_admin() ) {
|
||||
require_once WBC_PLUGIN_DIR . 'admin/class-wbc-admin.php';
|
||||
}
|
||||
|
||||
// Initialize the loader
|
||||
$loader = new WBC_Loader();
|
||||
$loader->run();
|
||||
}
|
||||
add_action( 'plugins_loaded', 'wbc_init', 20 );
|
||||
|
||||
/**
|
||||
* Add custom cron schedules
|
||||
*/
|
||||
function wbc_cron_schedules( $schedules ) {
|
||||
$schedules['twice_daily'] = array(
|
||||
'interval' => 12 * HOUR_IN_SECONDS,
|
||||
'display' => __( 'Twice Daily', 'woo-business-central' ),
|
||||
);
|
||||
return $schedules;
|
||||
}
|
||||
add_filter( 'cron_schedules', 'wbc_cron_schedules' );
|
||||
|
||||
/**
|
||||
* HPOS compatibility declaration
|
||||
*/
|
||||
add_action( 'before_woocommerce_init', function() {
|
||||
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
|
||||
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
|
||||
}
|
||||
} );
|
||||
Reference in New Issue
Block a user