<?php /** * Created by Vextras. * * Name: Ryan Hungate * Email: ryan@vextras.com * Date: 7/15/16 * Time: 11:42 AM */ class MailChimp_WooCommerce_Single_Order extends Mailchimp_Woocommerce_Job { public $id; public $cart_session_id; public $campaign_id; public $landing_site; public $user_language; public $is_update = false; public $is_admin_save = false; public $is_full_sync = false; public $partially_refunded = false; public $gdpr_fields = false; protected $woo_order_number = false; protected $is_amazon_order = false; protected $is_privacy_restricted = false; /** * MailChimp_WooCommerce_Single_Order constructor. * * @param null $id * @param null $cart_session_id * @param null $campaign_id * @param null $landing_site * @param null $user_language * @param null $gdpr_fields */ public function __construct($id = null, $cart_session_id = null, $campaign_id = null, $landing_site = null, $user_language = null, $gdpr_fields = null) { if (!empty($id)) $this->id = $id; if (!empty($cart_session_id)) $this->cart_session_id = $cart_session_id; if (!empty($campaign_id)) $this->campaign_id = $campaign_id; if (!empty($landing_site)) $this->landing_site = $landing_site; if (!empty($user_language)) $this->user_language = $user_language; if (!empty($gdpr_fields)) $this->gdpr_fields = $gdpr_fields; } /** * @param null $id * @return MailChimp_WooCommerce_Single_Order */ public function setId($id) { $this->id = $id; return $this; } /** * @param $is_full_sync * * @return $this */ public function set_full_sync($is_full_sync) { $this->is_full_sync = $is_full_sync; return $this; } /** * @return false * @throws MailChimp_WooCommerce_Error * @throws MailChimp_WooCommerce_RateLimitError * @throws MailChimp_WooCommerce_ServerError */ public function handle() { $this->process(); return false; } /** * @return false * @throws MailChimp_WooCommerce_Error * @throws MailChimp_WooCommerce_RateLimitError * @throws MailChimp_WooCommerce_ServerError */ public function process() { if (!mailchimp_is_configured() || !($api = mailchimp_get_api())) { mailchimp_debug(get_called_class(), 'Mailchimp is not configured properly'); return false; } $store_id = mailchimp_get_store_id(); if (!($woo_order_number = $this->getRealOrderNumber())) { mailchimp_log('order_submit.failure', "There is no real order number to use."); return false; } $job = new MailChimp_WooCommerce_Transform_Orders(); // set the campaign ID $job->campaign_id = $this->campaign_id; try { $call = ($api_response = $api->getStoreOrder($store_id, $woo_order_number, true)) ? 'updateStoreOrder' : 'addStoreOrder'; } catch (Exception $e) { if ($e instanceof MailChimp_WooCommerce_RateLimitError) { sleep(2); mailchimp_error('order_submit.error', mailchimp_error_trace($e, "RateLimited :: #{$this->id}")); $this->retry(); } $call = 'addStoreOrder'; } $new_order = $call === 'addStoreOrder'; if (!$this->is_admin_save && $new_order && $this->is_update === true) { return false; } // if we already pushed this order into the system, we need to unset it now just in case there // was another campaign that had been sent and this was only an order update. if (!$new_order) { $job->campaign_id = null; $this->campaign_id = null; $this->landing_site = null; } $email = null; // will either add or update the order try { if (!($order_post = get_post($this->id))) { return false; } // transform the order $order = $job->transform($order_post); // don't allow this to happen. if ($order->getOriginalWooStatus() === 'checkout-draft') { mailchimp_debug('filter', "Order {$woo_order_number} is in draft status and can not be submitted"); return false; } // if the order is new, and has been flagged as a status that should not be pushed over to // Mailchimp - just ignore it and log it. if ($new_order && $order->shouldIgnoreIfNotInMailchimp()) { mailchimp_debug('filter', "order {$woo_order_number} is in {$order->getOriginalWooStatus()} status, and is being skipped for now."); return false; } // see if we need to prevent this order from being submitted. $email = $order->getCustomer()->getEmailAddress(); // see if we have a bad email if ($this->shouldSkipOrder($email, $order->getId())) { return false; } $user_email = $order->getCustomer()->getEmailAddress(); $status = $order->getCustomer()->getOptInStatus(); $transient_key = mailchimp_hash_trim_lower($user_email).".mc.status"; $current_status = null; $pulled_member = false; if (!$status && mailchimp_submit_subscribed_only()) { try { $subscriber = $api->member(mailchimp_get_list_id(), $user_email); $current_status = $subscriber['status']; mailchimp_set_transient($transient_key, $current_status); if ($current_status != 'subscribed') { mailchimp_debug('filter', "#{$woo_order_number} was blocked due to subscriber only settings and current mailchimp status was {$current_status}"); return false; } } catch (Exception $e) { mailchimp_set_transient($transient_key, $current_status); mailchimp_debug('filter', "#{$woo_order_number} was blocked due to subscriber only settings"); return false; } $pulled_member = true; } if ($this->is_full_sync) { // see if this store has the auto subscribe setting enabled on initial sync $plugin_options = get_option('mailchimp-woocommerce'); $should_auto_subscribe = (bool) $plugin_options['mailchimp_auto_subscribe']; // since we're syncing the customer for the first time, this is where we need to add the override // for subscriber status. We don't get the checkbox until this plugin is actually installed and working! if (!$status) { try { if (!$pulled_member) { $subscriber = $api->member(mailchimp_get_list_id(), $order->getCustomer()->getEmailAddress()); $current_status = $subscriber['status']; $pulled_member = true; } if ($pulled_member && $current_status != 'archived' && isset($subscriber)) { $status = !in_array($subscriber['status'], array('unsubscribed', 'transactional')); $order->getCustomer()->setOptInStatus($status); } } catch (Exception $e) { if ($e instanceof MailChimp_WooCommerce_RateLimitError) { mailchimp_error('order_sync.error', mailchimp_error_trace($e, "GET subscriber :: {$order->getId()}")); throw $e; } // if they are using double opt in, we need to pass this in as false here so it doesn't auto subscribe. try { $doi = mailchimp_list_has_double_optin(true); } catch (Exception $e_doi) { throw $e_doi; } $status = $doi ? false : $should_auto_subscribe; $order->getCustomer()->setOptInStatus($status); } } } // will be the same as the customer id. an md5'd hash of a lowercased email. $this->cart_session_id = $order->getCustomer()->getId(); // see if we have a campaign ID already from the order transformer / cookie. $campaign_id = $order->getCampaignId(); // if the campaign ID is empty, and we have a cart session id if (empty($campaign_id) && !empty($this->cart_session_id)) { // pull the cart info from Mailchimp if (($abandoned_cart_record = $api->getCart($store_id, $this->cart_session_id))) { // set the campaign ID $order->setCampaignId($this->campaign_id = $abandoned_cart_record->getCampaignID()); } } if ($order->getOriginalWooStatus() !== 'pending') { // delete the AC cart record. $deleted_abandoned_cart = !empty($this->cart_session_id) && $api->deleteCartByID($store_id, $this->cart_session_id); } // skip amazon orders and skip privacy protected orders. if ($order->isFlaggedAsAmazonOrder()) { mailchimp_log('validation.amazon', "Order #{$woo_order_number} was placed through Amazon. Skipping!"); return false; } elseif ($order->isFlaggedAsPrivacyProtected()) { mailchimp_log('validation.gdpr', "Order #{$woo_order_number} is GDPR restricted. Skipping!"); return false; } if ($new_order) { // if single sync and // if the order is in failed or cancelled status - and it's brand new, we shouldn't submit it. if (!$this->is_full_sync && in_array($order->getFinancialStatus(), array('failed', 'cancelled')) || $order->getOriginalWooStatus() === 'pending') { mailchimp_log('order_submit', "#{$order->getId()} has a financial status of {$order->getFinancialStatus()} and was skipped."); return false; } // if full sync and // if the original woocommerce status is actually pending, we need to skip these on new orders because // it is probably happening due to 3rd party payment processing and it's still pending. These orders // don't always make it over because someone could be cancelling out of the payment there. if ($this->is_full_sync && !in_array(strtolower($order->getFinancialStatus()), array('processing', 'completed', 'paid'))) { mailchimp_log('order_submit', "#{$order->getId()} has a financial status of {$order->getFinancialStatus()} and was skipped."); return false; } } // if the order is brand new, and we already have a paid status, // we need to double up the post to force the confirmation + the invoice. if ($new_order && $order->getFinancialStatus() === 'paid') { $order->setFinancialStatus('pending'); $order->confirmAndPay(true); } // if we're overriding this we need to set it here. if ($this->partially_refunded) { $order->setFinancialStatus('partially_refunded'); } $log = "$call :: #{$order->getId()} :: email: {$email}"; // only do this stuff on new orders if ($new_order) { // apply a campaign id if we have one. if (!empty($this->campaign_id)) { try { $order->setCampaignId($this->campaign_id); $log .= ' :: campaign id ' . $this->campaign_id; } catch (Exception $e) { mailchimp_log('single_order_set_campaign_id.error', 'No campaign added to order, with provided ID: '. $this->campaign_id. ' :: '. $e->getMessage(). ' :: in '.$e->getFile().' :: on '.$e->getLine()); } } // apply the landing site if we have one. if (!empty($this->landing_site)) { $log .= ' :: landing site ' . $this->landing_site; $order->setLandingSite($this->landing_site); } } if ($this->is_full_sync) { $line_items = $order->items(); // if we don't have any line items, we need to create the mailchimp product // with a price of 1.00 and we'll use the inventory quantity to adjust correctly. if (empty($line_items) || !count($line_items)) { // this will create an empty product placeholder, or return the pre populated version if already // sent to Mailchimp. $product = $api->createEmptyLineItemProductPlaceholder(); $line_item = new MailChimp_WooCommerce_LineItem(); $line_item->setId($product->getId()); $line_item->setPrice(1); $line_item->setProductId($product->getId()); $line_item->setProductVariantId($product->getId()); $line_item->setQuantity((int) $order->getOrderTotal()); $order->addItem($line_item); mailchimp_log('order_submit.error', "Order {$order->getId()} does not have any line items, so we are using 'empty_line_item_placeholder' instead."); } } mailchimp_debug('order_submit', "#{$woo_order_number}", $order->toArray()); try { // update or create $api_response = $api->$call($store_id, $order, false); } catch (Exception $e) { // if for whatever reason we get a product not found error, we need to iterate // through the order items, and use a "create mode only" on each product // then re-submit the order once they're in the database again. if (mailchimp_string_contains($e->getMessage(), 'product with the provided ID')) { $api->handleProductsMissingFromAPI($order); // make another attempt again to add the order. $api_response = $api->$call($store_id, $order, false); } elseif (mailchimp_string_contains($e->getMessage(), 'campaign with the provided ID')) { // the campaign was invalid, we need to remove it and re-submit $order->setCampaignId(null); // make another attempt again to add the order. $api_response = $api->$call($store_id, $order, false); } else { throw $e; } } if (empty($api_response)) { mailchimp_error('order_submit.failure', "$call :: #{$order->getId()} :: email: {$email} produced a blank response from MailChimp"); return isset($api_response) ? $api_response : false; } if (isset($deleted_abandoned_cart) && $deleted_abandoned_cart) { $log .= " :: abandoned cart deleted [{$this->cart_session_id}]"; } // if we require double opt in on the list, and the customer requires double opt in, // we should mark them as pending so they get the opt in email now. if (mailchimp_list_has_double_optin()) { $status_if_new = $order->getCustomer()->getOriginalSubscriberStatus() ? 'pending' : 'transactional'; } else { // if true, subscribed - otherwise transactional $status_if_new = $order->getCustomer()->getOptInStatus() ? 'subscribed' : 'transactional'; } // if this is not currently in mailchimp - and we have the saved GDPR fields from // we can use the post meta for gdpr fields that were saved during checkout. if (!$this->is_full_sync && $new_order && empty($this->gdpr_fields)) { $this->gdpr_fields = get_post_meta($order->getId(), 'mailchimp_woocommerce_gdpr_fields', true); } // Maybe sync subscriber to set correct member.language mailchimp_member_data_update($email, $this->user_language, 'order', $status_if_new, $order, $this->gdpr_fields, !$this->is_full_sync); mailchimp_log('order_submit.success', $log); if ($this->is_full_sync && $new_order) { // if the customer has a flag to double opt in - we need to push this data over to MailChimp as pending //TODO: RYAN: this is the only place getOriginalSubscriberStatus() is called, but the iterate method uses another way. // mailchimp_update_member_with_double_opt_in($order, ($should_auto_subscribe || $status)); mailchimp_update_member_with_double_opt_in($order, ((isset($should_auto_subscribe) && $should_auto_subscribe) || $order->getCustomer()->getOriginalSubscriberStatus())); } return $api_response; } catch (MailChimp_WooCommerce_RateLimitError $e) { sleep(3); mailchimp_error('order_submit.error', mailchimp_error_trace($e, "RateLimited :: #{$this->id}")); $this->applyRateLimitedScenario(); throw $e; } catch (MailChimp_WooCommerce_ServerError $e) { mailchimp_error('order_submit.error', mailchimp_error_trace($e, "{$call} :: #{$this->id}")); throw $e; } catch (MailChimp_WooCommerce_Error $e) { mailchimp_error('order_submit.error', mailchimp_error_trace($e, "{$call} :: #{$this->id}")); throw $e; } catch (Exception $e) { $message = strtolower($e->getMessage()); mailchimp_error('order_submit.tracing_error', $e); if (!isset($order)) { // transform the order $order = $job->transform(get_post($this->id)); $this->cart_session_id = $order->getCustomer()->getId(); } // this can happen when a customer changes their email. if (isset($order) && strpos($message, 'not be changed')) { try { mailchimp_log('order_submit.deleting_customer', "#{$order->getId()} :: email: {$email}"); // delete the customer before adding it again. $api->deleteCustomer($store_id, $order->getCustomer()->getId()); // update or create $api_response = $api->$call($store_id, $order, false); $log = "Deleted Customer :: $call :: #{$order->getId()} :: email: {$email}"; if (!empty($job->campaign_id)) { $log .= ' :: campaign id '.$job->campaign_id; } mailchimp_log('order_submit.success', $log); // if we're adding a new order and the session id is here, we need to delete the AC cart record. if (!empty($this->cart_session_id)) { $api->deleteCartByID($store_id, $this->cart_session_id); } return $api_response; } catch (Exception $e) { mailchimp_error('order_submit.error', mailchimp_error_trace($e, 'deleting-customer-re-add :: #'.$this->id)); } } throw $e; } } /** * @return bool */ public function getRealOrderNumber() { try { if (empty($this->id) || !($order_post = get_post($this->id))) { return false; } $woo = wc_get_order($order_post); return $this->woo_order_number = $woo->get_order_number(); } catch (Exception $e) { $this->woo_order_number = false; mailchimp_error('order_sync.failure', mailchimp_error_trace($e, "{$this->id} could not be loaded")); return false; } } /** * @param $email * @param $order_id * @return bool */ protected function shouldSkipOrder($email, $order_id) { if (!is_email($email)) { mailchimp_log('validation.bad_email', "Order #{$order_id} has an invalid email address. Skipping!"); return true; } // make sure we can submit this order to MailChimp or skip it. if (mailchimp_email_is_amazon($email)) { mailchimp_log('validation.amazon', "Order #{$order_id} was placed through Amazon. Skipping!"); return true; } if (mailchimp_email_is_privacy_protected($email)) { mailchimp_log('validation.gdpr', "Order #{$order_id} is GDPR restricted. Skipping!"); return true; } return false; } }