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:
2026-02-17 09:59:53 +01:00
commit b64397dcd3
15 changed files with 3766 additions and 0 deletions

View 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 ),
);
}
}

View 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;
}

View 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);

View 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 ) ); ?>">
&lsaquo;
</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 ) ); ?>">
&rsaquo;
</a>
<?php endif; ?>
</span>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>

View 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 );
}
}

View 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 );
}
}
}

View 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;
}
}

View 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']
);
}
}
}

View 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 ) . '"';
}
}

View 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' ),
);
}
}

View 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 );
}
}

View 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' ),
);
}
}

View File

View 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();

View 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 );
}
} );