Kemp.rest / Examples / Code / Storefront and order sync
KEMP.REST / EXAMPLES / CODE
Storefront and order sync
Written by Kevin Kemp in PHP. JSON REST, OAuth, and Auth0, for a proprietary back office .NET points system.
View the codeCampaign links, order status, and shipment tracking were split between WooCommerce and the .NET back office. A coupon URL had to land in a ready cart. Status changes in the back office had to show up on the WooCommerce order, including a tracking number when it shipped. The API token also had to stay fresh. This child-theme file is that glue, plus a few account and content tools around it.
Type of code
A WordPress child-theme functions file: request hooks, cron jobs, a My Account endpoint, admin columns, and shortcodes.
Technology
PHP, WordPress, WooCommerce cart, coupons, order status, account endpoints, and email filters, Advanced Custom Fields, JSON over the back office REST API with a bearer token, an OAuth token request, the Auth0 WordPress plugin’s WooCommerce login filter (removed here so the store’s own form stays), AffiliateWP, and Flatsome markup. OAuth client credentials stay in ACF options and are not included here.
What it does
coupon-code,couponcode, orpromocodeon the URL adds that coupon’s products to the cart, applies the discount, and redirects to the cart or the shop.- A My Account endpoint,
view-order-office, renders the back office order template. - A cron job reads that day’s order changes from the back office and maps them onto WooCommerce statuses, including Pending, Processing, Completed, Cancelled, Refunded, Shipped, Declined, and Fulfillment Center. A Shipped change saves package number, carrier, tracking URL, and tracking number. The customer processing email is suppressed during that sync.
- A second cron job requests a new access token and stores it for the other API calls.
- Shortcodes print recent posts from a category. The users list shows AffiliateWP id and status. My Account links to the affiliate area, or to “Become an Affiliate.”
Problem it solved
The storefront could take a campaign straight into checkout, and WooCommerce orders picked up status and tracking from the back office without a person retyping them. The token refresh kept those calls working.
Code
Back to summary · custom-function.php
<?php
define('CHILD_THEME_URI_IMAGE', get_stylesheet_directory_uri().'/assets/img');
// Add to cart and rediect when use url smart coupon
function my_woocommerce_apply_cart_coupon_in_url_smart_coupon() {
// Return early if WooCommerce or sessions aren't available.
if ( ! function_exists( 'WC' ) || ! WC()->session ) {
return;
}
// Return if there is no coupon in the URL, otherwise set the variable.
if ( empty( $_REQUEST['coupon-code'] ) ) {
return;
} else {
$coupon_code = esc_attr( $_REQUEST['coupon-code'] );
}
// Set a session cookie to remember the coupon if they continue shopping.
WC()->session->set_customer_session_cookie(true);
// Apply the coupon to the cart if necessary.
if ( ! WC()->cart->has_discount( $coupon_code ) ) {
global $woocommerce;
$list_products = new WC_Coupon($coupon_code);
$product_ids = $list_products->product_ids;
foreach ($product_ids as $product_id ) {
WC()->cart->add_to_cart( $product_id );
}
// WC_Cart::add_discount() sanitizes the coupon code.
WC()->cart->add_discount( $coupon_code );
$shop_page = $_REQUEST['sc-page'];
if($shop_page == 'shop') {
$referer = wc_get_page_permalink( 'shop' );
}else {
$referer = WC()->cart->get_cart_url();
}
wp_safe_redirect( $referer );
exit;
}
}
add_action('wp_loaded', 'my_woocommerce_apply_cart_coupon_in_url_smart_coupon', 15);
// Add to cart and rediect when use url couponcode
function my_woocommerce_apply_cart_coupon_in_url_couponcode() {
// Return early if WooCommerce or sessions aren't available.
if ( ! function_exists( 'WC' ) || ! WC()->session ) {
return;
}
// Return if there is no coupon in the URL, otherwise set the variable.
if ( empty( $_REQUEST['couponcode'] ) ) {
return;
} else {
$coupon_code = esc_attr( $_REQUEST['couponcode'] );
}
// Set a session cookie to remember the coupon if they continue shopping.
WC()->session->set_customer_session_cookie(true);
// Apply the coupon to the cart if necessary.
if ( ! WC()->cart->has_discount( $coupon_code ) ) {
global $woocommerce;
$list_products = new WC_Coupon($coupon_code);
$product_ids = $list_products->product_ids;
foreach ($product_ids as $product_id ) {
WC()->cart->add_to_cart( $product_id );
}
// WC_Cart::add_discount() sanitizes the coupon code.
WC()->cart->add_discount( $coupon_code );
$referer = WC()->cart->get_cart_url();
wp_safe_redirect( $referer );
exit;
}
}
add_action('wp_loaded', 'my_woocommerce_apply_cart_coupon_in_url_couponcode', 30);
// Add to cart and rediect when use url promocode
function my_woocommerce_apply_cart_coupon_in_url_promocode() {
// Return early if WooCommerce or sessions aren't available.
if ( ! function_exists( 'WC' ) || ! WC()->session ) {
return;
}
// Return if there is no coupon in the URL, otherwise set the variable.
if ( empty( $_REQUEST['promocode'] ) ) {
return;
} else {
$coupon_code = esc_attr( $_REQUEST['promocode'] );
}
// Set a session cookie to remember the coupon if they continue shopping.
WC()->session->set_customer_session_cookie(true);
// Apply the coupon to the cart if necessary.
if ( ! WC()->cart->has_discount( $coupon_code ) ) {
global $woocommerce;
$list_products = new WC_Coupon($coupon_code);
$product_ids = $list_products->product_ids;
foreach ($product_ids as $product_id ) {
WC()->cart->add_to_cart( $product_id );
}
// WC_Cart::add_discount() sanitizes the coupon code.
WC()->cart->add_discount( $coupon_code );
$referer = WC()->cart->get_cart_url();
wp_safe_redirect( $referer );
}
}
add_action('wp_loaded', 'my_woocommerce_apply_cart_coupon_in_url_promocode', 30);
// Add page new view order my account
define('BACKOFFICE_BASE_URL_VIEWORDER','view-order-office');
define('BACKOFFICE_ORDER_ZERO',0);
if (class_exists('ACF')) {
// The ACF class doesn't exist, so you can probably redefine your functions here
define('BACKOFFICE_API_BASE_URL_OAUTH_TOKEN',get_field('url_oauth_token','option'));
}
function backoffice_woocommerce_account_rewrite_view_orders() {
add_rewrite_endpoint(BACKOFFICE_BASE_URL_VIEWORDER, EP_ROOT | EP_PAGES );
}
add_action( 'init', 'backoffice_woocommerce_account_rewrite_view_orders' );
add_action( 'woocommerce_account_'.BACKOFFICE_BASE_URL_VIEWORDER.'_endpoint', 'backoffice_woocommerce_account_view_orders' );
function backoffice_woocommerce_account_view_orders() {
//content goes here
$order_id = absint( get_query_var(BACKOFFICE_BASE_URL_VIEWORDER) );
$licenses = []; // Replace with function to return licenses for current logged in user
wc_get_template(
'order/order-details-office.php',
array(
'order_id' => $order_id,
)
);
}
//Add Prefix and/or Suffix to WooCommerce Order Numbers
//add_filter( 'woocommerce_order_number', 'change_woocommerce_order_number' );
function change_woocommerce_order_number( $order_id ) {
$prefix = '1';
$new_order_id = $prefix . $order_id;
return $new_order_id;
}
// Function Cronjob with get API sync order status
add_action( 'update_security_presscare', 'backoffice_woocommerce_sync_status_orders' );
function backoffice_woocommerce_sync_status_orders(){
$access_token = get_field('access_token','option');
$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
$limit = 100;
// Create a DateTime object for the current date
$current_date = new DateTime();
// Define the format
$format_from = 'Y-m-d\T00:00:00.000\Z';
$format = 'Y-m-d\T23:59:59.000\Z';
// Get the formatted date
$thruDate = $current_date->format($format);
$FromDate = $current_date->format($format_from);
// Create an old date field by subtracting 5 minutes from newdate
//$FromDate = '2024-08-11T00:00:03.000Z';
// $thruDate = '2024-08-12T23:59:03.000Z';
$api_url = BACKOFFICE_API_BASE_URL.'/Changes?IncludeCreatedBy=WooCommerce&Offset=' . $offset . '&Limit=' . $limit.'&FromDate='.$FromDate.'&thruDate='.$thruDate.'';
//$api_url = BACKOFFICE_API_BASE_URL.'/Changes?IncludeCreatedBy=WooCommerce&Offset=' . $offset . '&FromDate='.$FromDate.'&thruDate='.$thruDate.'';
$response = wp_remote_get($api_url, array(
'headers' => array('Authorization' => 'Bearer ' . $access_token)
));
if ( is_wp_error( $response ) ) {
echo '<p>' . __( 'There was an error fetching orders. Please try again later.', 'textdomain' ) . '</p>';
return;
}
$response_body = wp_remote_retrieve_body($response);
$backoffice_orders = json_decode( $response_body, true );
$status = array(
'pending' => 'Pending',
'processing' => 'Processing',
'on-hold' => '',
'completed' => 'Completed',
'cancelled' => 'Cancelled',
'refunded' => 'Refunded',
'failed' => '',
'checkout-draft' => '',
'shipped' => 'Shipped',
'declined' => 'Declined',
'fulfillment' => 'Fulfillment Center',
);
add_filter( 'woocommerce_email_recipient_customer_processing_order', 'disable_customer_emails_if_disabled', 9999, 2 );
foreach ($backoffice_orders as $backoffice_order){
$backoffice_status = $backoffice_order['orderStatus'];
$order_id = $backoffice_order['id'];
$office_order_id = $backoffice_order['orderId'];
$order = wc_get_order( $order_id );
if($order) {
$woo_status = $order->status;
if (isset($status[$woo_status]) && (($status[$woo_status]) != $backoffice_status) && ($office_order_id != 0)) {
$backoffice_status_lower = strtolower($backoffice_status);
$update_status = array_search($backoffice_status_lower, array_map('strtolower', $status));
if ($update_status !== false){
if ($update_status == 'shipped') {
// update param info shipping
foreach ($backoffice_order['changes'] as $change) {
if ($change['orderStatus'] === 'Shipped') {
if (isset($change['packages'])) {
foreach ($change['packages'] as $package) {
update_post_meta($order_id, 'shipped_packagenumber', $package['packageNumber']);
update_post_meta($order_id, 'shipped_provider', $package['provider']);
update_post_meta($order_id, 'shipped_trackingurl', $package['trackingURL']);
update_post_meta($order_id, 'shipped_trackingnumber', $package['trackingNumber']);
}
}
}
}
}
$order->update_status($update_status);
}
}
}
}
}
/* function update cron job get token access 30 days */
add_action( 'backoffice_get_access_token_update_order_cron', 'backoffice_get_access_token_update_order' );
function backoffice_get_access_token_update_order(){
$grant_type = get_field('grant_type','option');
$client_id = get_field('client_id','option');
$client_secret = get_field('client_secret','option');
$audience = get_field('audience','option');
$api_url = BACKOFFICE_API_BASE_URL_OAUTH_TOKEN;
$args = array(
'method' => 'POST',
'httpversion' => '1.1',
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8',
),
'body' => 'grant_type='.$grant_type.'&client_id='.$client_id.'&client_secret='.$client_secret.'&audience='.$audience.'',
);
$response = wp_remote_post( $api_url, $args );
if ( is_wp_error( $response ) ) {
echo '<p>' . __( 'There was an error fetching orders. Please try again later.', 'textdomain' ) . '</p>';
return;
}
$response_body = wp_remote_retrieve_body($response);
$token_response = json_decode( $response_body, true );
$access_token = $token_response['access_token'];
if($access_token){
update_field( 'access_token', $access_token, 'option' );
}
}
/**
* Add custom data to WC API shop order response
* Overriding "$object" here with $order so it's easier to access its properties
*/
function my_wc_rest_prepare_order( $response, $order, $request ) {
if( empty( $response->data ) )
return $response;
$order_id = $order->get_id();
$prefix = '1';
$new_order_id = $prefix . $order_id;
//$new_order_id = $order_id;
//$response->data['id'] = $new_order_id;
$response->data['id_default'] = $order_id;
return $response;
}
//add_filter( 'woocommerce_rest_prepare_shop_order_object', 'my_wc_rest_prepare_order', 10, 3 );
//fix request_timeout api
function __extend_http_request_timeout( $timeout ) {
return 200; // seconds
}
add_filter( 'http_request_timeout', '__extend_http_request_timeout' );
/* remove_filter button login woocommerce account from plugin Auth0 */
remove_filter( 'woocommerce_before_customer_login_form', 'wp_auth0_filter_woocommerce_before_customer_login_form' );
/* create shortcode get post by category */
function get_post_func($atts){
ob_start();
extract(shortcode_atts(array('cat' => "","limit"=>1), $atts));
$args = array(
"post_type" => "post",
"posts_per_page" => $limit,
"cat" => $cat
);
$wp_query = new WP_Query( $args );
if( $wp_query->have_posts() ){
?>
<?php
while ($wp_query->have_posts()) {
$wp_query->the_post();
$excerpt_length = 35;
$post_id = get_the_ID();
$featured = get_the_post_thumbnail_url( $post_id, "full" );
$backoffice_image = get_post_meta($post_id,'backoffice_custom_field_secondary_image',true);
if($backoffice_image) {
$featured_url = wp_get_attachment_image_url($backoffice_image,'full');
}else {
$featured_url = $featured;
}
?>
<div class="backoffice-get-post">
<div class="img has-hover x md-x lg-x y md-y lg-y backoffice-get-post-thumbnail">
<div class="img-inner dark">
<img src="<?php echo $featured_url;?>">
</div>
</div>
<div class="row row-collapse align-bottom text_row_sec">
<div class="col medium-10 small-8 large-10">
<div class="col-inner backoffice-get-post-inner">
<div class="text">
<p style="margin:5px 0px;"><strong><span style="color: #ddd; font-size:12px;"><?php echo get_the_date('M d,Y');?></span></strong></p>
<h2 style="font-weight: 500;margin:5px 0px;"><a href="<?php the_permalink(); ?>" style="color: #ffffff;"><span style="color: #ffffff;"><?php the_title(); ?></span></a></h2>
<p style="font-size:15px; margin:5px 0px;">
<span style="color: #ffffff;">
<?php echo flatsome_get_the_excerpt( $excerpt_length ); ?>
</span>
</p>
</div>
</div>
</div>
<div class="col medium-2 small-4 large-2">
<div class="col-inner backoffice-get-post-inner ">
<div class="img has-hover x md-x lg-x y md-y lg-y backoffice-get-post-icon">
<a class="" href="<?php the_permalink(); ?>">
<div class="img-inner image-glow dark">
<img src="<?php echo CHILD_THEME_URI_IMAGE.'/icon-next.png'; ?>">
</div>
</a>
</div>
</div>
</div>
</div>
</div>
<?php
}
}
wp_reset_query();
return ob_get_clean();
}
add_shortcode( "backoffice_get_post", "get_post_func" );
/* create shortcode get post by category */
function get_post_multi_func($atts){
ob_start();
extract(shortcode_atts(array('cat' => "","limit"=>4), $atts));
$args = array(
"post_type" => "post",
"posts_per_page" => $limit,
"cat" => $cat,
"offset" => 1
);
$wp_query = new WP_Query( $args );
if( $wp_query->have_posts() ){
?>
<?php
while ($wp_query->have_posts()) {
$wp_query->the_post();
?>
<div class="row row-collapse align-center text_row_sec backoffice-get-post-flex" >
<div class="col medium-10 small-8 large-10" >
<div class="col-inner backoffice-get-post-inner" >
<div class="is-border" style="border-color:rgba(255, 255, 255, 0.797);border-width:0px 0px 0px 0px;" ></div>
<div class="text backoffice-get-post-title" >
<p style="margin: 5px 0px;">
<strong>
<span style="color: #ddd; font-size: 12px;">
<?php echo get_the_date('M d,Y');?>
</span>
</strong>
</p>
<h2 style="font-weight: 500; margin: 5px 0px;"><a href="<?php the_permalink(); ?>" style="color: #ffffff;"><span style="color: #ffffff;"><?php the_title(); ?></span></a></h2>
<p style="font-size: 14px; margin: 5px 0px;"> </p>
</div>
</div>
</div>
<div class="col medium-2 small-4 large-2" >
<div class="col-inner backoffice-get-post-inner" >
<div class="img has-hover x md-x lg-x y md-y lg-y backoffice-get-post-icon" >
<a class="" href="<?php the_permalink(); ?>">
<div class="img-inner image-glow dark" >
<img src="<?php echo CHILD_THEME_URI_IMAGE.'/icon-next.png'; ?>">
</div>
</a>
</div>
</div>
</div>
</div>
<div class="is-divider divider clearfix" style="margin-top:10px;margin-bottom:10px;max-width:100%;height:1px;background-color:rgba(255, 255, 255, 0.715);" >
</div>
<?php
}
}
wp_reset_query();
return ob_get_clean();
}
add_shortcode( "backoffice_get_post_multi", "get_post_multi_func" );
/* disable send mail status processing */
function disable_customer_emails_if_disabled( $recipient, $order ) {
$recipient = '';
return $recipient;
}
/* add column get affiliate_id */
function new_modify_user_table( $column ) {
$column['affiliate_id'] = 'Affiliate ID';
$column['affiliate_status'] = 'Affiliate Status';
return $column;
}
add_filter( 'manage_users_columns', 'new_modify_user_table' );
function new_modify_user_table_row( $val, $column_name, $user_id ) {
$affiliate = affwp_get_affiliate_id( $user_id );
$affiliate_status = affwp_get_affiliate_status( $affiliate );
if($affiliate){
$url = admin_url( 'admin.php?page=affiliate-wp-affiliates' );
$affiliate_id = '<a href="'.$url.'&affiliate_id='.$affiliate.'&action=edit_affiliate ">'.$affiliate.'</a>';
}else {
$affiliate_id = "";
}
switch ($column_name) {
case 'affiliate_id' :
return $affiliate_id;
case 'affiliate_status' :
return $affiliate_status;
default:
}
return $val;
}
add_filter( 'manage_users_custom_column', 'new_modify_user_table_row', 10, 3 );
// Add the new affiliate area link to the "My Account" menu
// if Affiliate WP is enabled and the current user is an affiliate.
//
// Ensure that the logout link stays at the bottom of the menu ;)
function moh_add_aff_wp_link_my_account( $items ) {
if ( affwp_is_affiliate() ) {
$affiliate_area_page = affwp_get_affiliate_area_page_id();
if ( $affiliate_area_page ) {
/**
* Filters the title used for the Affiliate Area page in the WooCommerce My Account navigation.
*
* The page title is used by default.
*
* @since 2.1
*
* @param string $title Affiliate Area page title.
* @param int $affiliate_area_page Affiliate Area page ID.
*/
$title = apply_filters( 'affwp_woocommerce_affiliate_area_title', get_the_title( $affiliate_area_page ), $affiliate_area_page );
/*
* Normally this would be $slug => $title, but we're going to intercept the 'affiliate-area'
* value directly when overriding the endpoint URL in the 'woocommerce_get_endpoint_url' hook.
*/
$affiliate_area = array( 'affiliate-area' => $title );
$last_link = array();
if ( array_key_exists( 'customer-logout', $items ) ) {
// Grab the last link (probably the logout link).
$last_link = array_slice( $items, count( $items ) - 1, 1, true );
// Pop the last link off the end.
array_pop( $items );
}
// Inject the Affiliate Area link 2nd to last, reinserting the last link.
$items = array_merge( $items, $affiliate_area, $last_link );
}
} else {
$affiliate_area_page = affwp_get_affiliate_area_page_id();
if ( $affiliate_area_page ) {
/**
* Filters the title used for the Affiliate Area page in the WooCommerce My Account navigation.
*
* The page title is used by default.
*
* @since 2.1
*
* @param string $title Affiliate Area page title.
* @param int $affiliate_area_page Affiliate Area page ID.
*/
$title = 'Become an Affiliate';
/*
* Normally this would be $slug => $title, but we're going to intercept the 'affiliate-area'
* value directly when overriding the endpoint URL in the 'woocommerce_get_endpoint_url' hook.
*/
$affiliate_area = array( 'affiliate-area' => $title );
$last_link = array();
if ( array_key_exists( 'customer-logout', $items ) ) {
// Grab the last link (probably the logout link).
$last_link = array_slice( $items, count( $items ) - 1, 1, true );
// Pop the last link off the end.
array_pop( $items );
}
// Inject the Affiliate Area link 2nd to last, reinserting the last link.
$items = array_merge( $items, $affiliate_area, $last_link );
}
}
return $items;
}
add_filter( 'woocommerce_account_menu_items', 'moh_add_aff_wp_link_my_account' );
// change text meta box support ticket
function custom_modify_supportcandy_meta_box() {
// Remove the original meta box
remove_meta_box('wpsc_other_fields','shop_order', 'side');
// Add the new meta box with the desired title
add_meta_box(
'wpsc_other_fields',
'Support Tickets', // Change this to your desired title
'wpsc_create_ticket_from_order',
'shop_order',
'side'
);
}
//add_action('add_meta_boxes', 'custom_modify_supportcandy_meta_box', 30);