From f8f77e937fb52070f201b7a8a838687795c101f8 Mon Sep 17 00:00:00 2001 From: Jason Irish Date: Fri, 2 Mar 2018 11:40:58 -0600 Subject: [PATCH] Order Parsing and History Rework (#254) Replaced relationships between OrderDetail and OptionItems with new OrderOption object. Overhaul of the various parsing functions, simplifying and clarifying. Moved to Order. Order History page displays Product info from Datafeed, links to Product page if it exists/matches. Order->Response is now saved encrypted. Added two new BuildTasks: FoxyStripe: ParseOrders - loops through all orders in the database that have a value for Response, and re-parses the Order data. FoxyStripe: EncryptReponses - loops through all Orders and encrypts any Response fields that are unencrypted XML. For legacy migrations. --- code/controllers/FoxyStripe_Controller.php | 184 ++------------------- code/objects/Order.php | 167 ++++++++++++++++++- code/objects/OrderDetail.php | 27 ++- code/objects/OrderOption.php | 19 +++ tasks/EncryptResponsesTask.php | 33 ++++ tasks/ParseOrdersTask.php | 21 +++ templates/Layout/OrderHistoryPage.ss | 62 ++++--- 7 files changed, 305 insertions(+), 208 deletions(-) create mode 100644 code/objects/OrderOption.php create mode 100644 tasks/EncryptResponsesTask.php create mode 100644 tasks/ParseOrdersTask.php diff --git a/code/controllers/FoxyStripe_Controller.php b/code/controllers/FoxyStripe_Controller.php index 9d325d8..b1cb157 100644 --- a/code/controllers/FoxyStripe_Controller.php +++ b/code/controllers/FoxyStripe_Controller.php @@ -14,12 +14,16 @@ public function getURLSegment() { ); public function index() { + // handle POST from FoxyCart API transaction if ((isset($_POST["FoxyData"]) OR isset($_POST['FoxySubscriptionData']))) { + $FoxyData_encrypted = (isset($_POST["FoxyData"])) ? urldecode($_POST["FoxyData"]) : urldecode($_POST["FoxySubscriptionData"]); $FoxyData_decrypted = rc4crypt::decrypt(FoxyCart::getStoreKey(),$FoxyData_encrypted); + + // parse the response and save the order self::handleDataFeed($FoxyData_encrypted, $FoxyData_decrypted); // extend to allow for additional integrations with Datafeed @@ -35,188 +39,30 @@ public function index() { } public function handleDataFeed($encrypted, $decrypted){ - //handle encrypted & decrypted data + $orders = new SimpleXMLElement($decrypted); // loop over each transaction to find FoxyCart Order ID - foreach ($orders->transactions->transaction as $order) { - - if (isset($order->id)) { - ($transaction = Order::get()->filter('Order_ID', $order->id)->First()) ? - $transaction : - $transaction = Order::create(); - } - - // save base order info - $transaction->Order_ID = (int) $order->id; - $transaction->Response = $decrypted; - - // record transaction as order - $transaction->write(); - - // parse order - $this->parseOrder($order->id); - - } - } - - public function parseOrder($Order_ID) { - - $transaction = Order::get()->filter(array('Order_ID' => $Order_ID))->First(); - - if ($transaction) { - // grab response, parse as XML - $orders = new SimpleXMLElement($transaction->Response); - - $this->parseOrderInfo($orders, $transaction); - $this->parseOrderCustomer($orders, $transaction); - // record transaction so user info can be accessed from parseOrderDetails() - $transaction->write(); - $this->parseOrderDetails($orders, $transaction); - - // record transaction as order - $transaction->write(); - } - } - - public function parseOrderInfo($orders, $transaction) { - - foreach ($orders->transactions->transaction as $order) { - - // Record transaction data from FoxyCart Datafeed: - $transaction->Store_ID = (int)$order->store_id; - $transaction->TransactionDate = (string)$order->transaction_date; - $transaction->ProductTotal = (float)$order->product_total; - $transaction->TaxTotal = (float)$order->tax_total; - $transaction->ShippingTotal = (float)$order->shipping_total; - $transaction->OrderTotal = (float)$order->order_total; - $transaction->ReceiptURL = (string)$order->receipt_url; - $transaction->OrderStatus = (string)$order->status; - } - } - - public function parseOrderCustomer($orders, $transaction) { - - foreach ($orders->transactions->transaction as $order) { - - // if not a guest transaction in FoxyCart - if (isset($order->customer_email) && $order->is_anonymous == 0) { - - // if Customer is existing member, associate with current order - if(Member::get()->filter('Email', $order->customer_email)->First()) { - - $customer = Member::get()->filter('Email', $order->customer_email)->First(); - - /* todo: make sure local password is updated if changed on FoxyCart - $customer->Password = (string)$order->customer_password; - $customer->write(); + foreach ($orders->transactions->transaction as $transaction) { - $customer->Password = (string)$order->customer_password; - $customer->Salt = (string)$order->customer_password_salt; - $customer->write(); - */ + // if FoxyCart order id, then parse order + if (isset($transaction->id)) { - } else { + ($order = Order::get()->filter('Order_ID', (int) $transaction->id)->First()) ? + $order = Order::get()->filter('Order_ID', (int) $transaction->id)->First() : + $order = Order::create(); - // set PasswordEncryption to 'none' so imported, encrypted password is not encrypted again - Config::inst()->update('Security', 'password_encryption_algorithm', 'none'); - - // create new Member, set password info from FoxyCart - $customer = Member::create(); - $customer->Customer_ID = (int)$order->customer_id; - $customer->FirstName = (string)$order->customer_first_name; - $customer->Surname = (string)$order->customer_last_name; - $customer->Email = (string)$order->customer_email; - $customer->Password = (string)$order->customer_password; - $customer->PasswordEncryption = 'none'; - - // record member record - $customer->write(); - - $customer->Password = (string)$order->customer_password; - $customer->Salt = (string)$order->customer_password_salt; - - $customer->write(); - - } - - // set Order MemberID - $transaction->MemberID = $customer->ID; + // save base order info + $order->Order_ID = (int) $transaction->id; + $order->Response = urlencode($encrypted); + $order->write(); } - } - } - public function parseOrderDetails($orders, $transaction) { - - // remove previous OrderDetails so we don't end up with duplicates - foreach ($transaction->Details() as $detail) { - $detail->delete(); - } - - foreach ($orders->transactions->transaction as $order) { - - // Associate ProductPages, Options, Quanity with Order - foreach ($order->transaction_details->transaction_detail as $product) { - - $OrderDetail = OrderDetail::create(); - - // set Quantity - $OrderDetail->Quantity = (int)$product->product_quantity; - - // set calculated price (after option modifiers) - $OrderDetail->Price = (float)$product->product_price; - - // Find product via product_id custom variable - foreach ($product->transaction_detail_options->transaction_detail_option as $productID) { - if ($productID->product_option_name == 'product_id') { - - $OrderProduct = ProductPage::get() - ->filter('ID', (int)$productID->product_option_value) - ->First(); - - // if product could be found, then set Option Items - if ($OrderProduct) { - - // set ProductID - $OrderDetail->ProductID = $OrderProduct->ID; - - // loop through all Product Options - foreach ($product->transaction_detail_options->transaction_detail_option as $option) { - - $OptionItem = OptionItem::get()->filter(array( - 'ProductID' => (string)$OrderProduct->ID, - 'Title' => (string)$option->product_option_value - ))->First(); - - if ($OptionItem) { - $OrderDetail->Options()->add($OptionItem); - - // modify product price - if ($priceMod = $option->price_mod) { - $OrderDetail->Price += $priceMod; - } - } - } - } - } - - // associate with this order - $OrderDetail->OrderID = $transaction->ID; - - // extend OrderDetail parsing, allowing for recording custom fields from FoxyCart - $this->extend('handleOrderItem', $decrypted, $product, $OrderDetail); - - // write - $OrderDetail->write(); - - } - } } } - // Single Sign on integration with FoxyCart public function sso() { diff --git a/code/objects/Order.php b/code/objects/Order.php index 46e44a0..7d69eb2 100644 --- a/code/objects/Order.php +++ b/code/objects/Order.php @@ -33,8 +33,8 @@ class Order extends DataObject implements PermissionProvider 'TransactionDate.NiceUS', 'Member.Name', 'ProductTotal.Nice', - 'TaxTotal.Nice', 'ShippingTotal.Nice', + 'TaxTotal.Nice', 'OrderTotal.Nice', 'ReceiptLink' ); @@ -90,17 +90,166 @@ public function getReceiptLink() return $obj; } - public function canView($member = false) - { - return Permission::check('Product_ORDERS'); + public function getDecryptedResponse() { + $decrypted = urldecode($this->Response); + return rc4crypt::decrypt(FoxyCart::getStoreKey(), $decrypted); } - public function canEdit($member = null) - { - //return Permission::check('Product_ORDERS'); - return false; + public function onBeforeWrite() { + + $this->parseOrder(); + parent::onBeforeWrite(); } + public function parseOrder() { + + if ($this->getDecryptedResponse()) { + + $response = new SimpleXMLElement($this->getDecryptedResponse()); + + $this->parseOrderInfo($response); + $this->parseOrderCustomer($response); + $this->parseOrderDetails($response); + + return true; + + } else { + + return false; + + } + } + + public function parseOrderInfo($response) { + + foreach ($response->transactions->transaction as $transaction) { + + // Record transaction data from FoxyCart Datafeed: + $this->Store_ID = (int) $transaction->store_id; + $this->TransactionDate = (string) $transaction->transaction_date; + $this->ProductTotal = (float) $transaction->product_total; + $this->TaxTotal = (float) $transaction->tax_total; + $this->ShippingTotal = (float) $transaction->shipping_total; + $this->OrderTotal = (float) $transaction->order_total; + $this->ReceiptURL = (string) $transaction->receipt_url; + $this->OrderStatus = (string) $transaction->status; + + $this->extend('handleOrderInfo', $order, $response); + } + } + + public function parseOrderCustomer($response) { + + foreach ($response->transactions->transaction as $transaction) { + + // if not a guest transaction in FoxyCart + if (isset($transaction->customer_email) && $transaction->is_anonymous == 0) { + + // if Customer is existing member, associate with current order + if(Member::get()->filter('Email', $transaction->customer_email)->First()) { + + $customer = Member::get()->filter('Email', $transaction->customer_email)->First(); + + // if new customer, create account with data from FoxyCart + } else { + + // set PasswordEncryption to 'none' so imported, encrypted password is not encrypted again + Config::inst()->update('Security', 'password_encryption_algorithm', 'none'); + + // create new Member, set password info from FoxyCart + $customer = Member::create(); + $customer->Customer_ID = (int) $transaction->customer_id; + $customer->FirstName = (string) $transaction->customer_first_name; + $customer->Surname = (string) $transaction->customer_last_name; + $customer->Email = (string) $transaction->customer_email; + $customer->Password = (string) $transaction->customer_password; + $customer->Salt = (string) $transaction->customer_password_salt; + $customer->PasswordEncryption = 'none'; + + // record member record + $customer->write(); + } + + // set Order MemberID + $this->MemberID = $customer->ID; + + $this->extend('handleOrderCustomer', $order, $response, $customer); + + } + } + } + + public function parseOrderDetails($response) { + + // remove previous OrderDetails and OrderOptions so we don't end up with duplicates + foreach ($this->Details() as $detail) { + foreach ($detail->OrderOptions() as $orderOption) { + $orderOption->delete(); + } + $detail->delete(); + } + + foreach ($response->transactions->transaction as $transaction) { + + // Associate ProductPages, Options, Quantity with Order + foreach ($transaction->transaction_details->transaction_detail as $detail) { + + $OrderDetail = OrderDetail::create(); + + $OrderDetail->Quantity = (int) $detail->product_quantity; + $OrderDetail->ProductName = (string) $detail->product_name; + $OrderDetail->ProductCode = (string) $detail->product_code; + $OrderDetail->ProductImage = (string) $detail->image; + $OrderDetail->ProductCategory = (string) $detail->category_code; + $priceModifier = 0; + + // parse OrderOptions + foreach ($detail->transaction_detail_options->transaction_detail_option as $option) { + + // Find product via product_id custom variable + if ($option->product_option_name == 'product_id') { + + // if product is found, set relation to OrderDetail + $OrderProduct = ProductPage::get()->byID((int) $option->product_option_value); + if ($OrderProduct) $OrderDetail->ProductID = $OrderProduct->ID; + + } else { + + $OrderOption = OrderOption::create(); + $OrderOption->Name = (string) $option->product_option_name; + $OrderOption->Value = (string) $option->product_option_value; + $OrderOption->write(); + $OrderDetail->OrderOptions()->add($OrderOption); + + $priceModifier += $option->price_mod; + } + + } + + $OrderDetail->Price = (float) $detail->product_price + (float) $priceModifier; + + // extend OrderDetail parsing, allowing for recording custom fields from FoxyCart + $this->extend('handleOrderItem', $order, $response, $OrderDetail); + + // write + $OrderDetail->write(); + + // associate with this order + $this->Details()->add($OrderDetail); + } + } + } + + + public function canView($member = false) { + return Permission::check('Product_ORDERS'); + } + + public function canEdit($member = null) { + return false; + //return Permission::check('Product_ORDERS'); + } + public function canDelete($member = null) { return false; @@ -118,4 +267,4 @@ public function providePermissions() 'Product_ORDERS' => 'Allow user to manage Orders and related objects' ); } -} +} \ No newline at end of file diff --git a/code/objects/OrderDetail.php b/code/objects/OrderDetail.php index e3a8312..11b8564 100644 --- a/code/objects/OrderDetail.php +++ b/code/objects/OrderDetail.php @@ -22,7 +22,11 @@ class OrderDetail extends DataObject */ private static $db = array( 'Quantity' => 'Int', - 'Price' => 'Currency' + 'Price' => 'Currency', + 'ProductName' => 'Varchar(255)', + 'ProductCode' => 'Varchar(100)', + 'ProductImage' => 'Text', + 'ProductCategory' => 'Varchar(100)' ); /** @@ -33,11 +37,8 @@ class OrderDetail extends DataObject 'Order' => 'Order' ); - /** - * @var array - */ - private static $many_many = array( - 'Options' => 'OptionItem' + private static $has_many = array( + 'OrderOptions' => 'OrderOption' ); /** @@ -99,5 +100,15 @@ public function canDelete($member = null) public function canCreate($member = null) { return false; - } -} + //return Permission::check('Product_ORDERS'); + } + + public function canDelete($member = null) { + return Permission::check('Product_ORDERS'); + } + + public function canCreate($member = null) { + return false; + } + +} \ No newline at end of file diff --git a/code/objects/OrderOption.php b/code/objects/OrderOption.php new file mode 100644 index 0000000..06532d7 --- /dev/null +++ b/code/objects/OrderOption.php @@ -0,0 +1,19 @@ + 'Varchar(200)', + 'Value' => 'Varchar(200)' + ); + + private static $has_one = array( + 'OrderDetail' => 'OrderDetail' + ); + + private static $summary_fields = array( + 'Name', + 'Value' + ); + +} \ No newline at end of file diff --git a/tasks/EncryptResponsesTask.php b/tasks/EncryptResponsesTask.php new file mode 100644 index 0000000..2b30d47 --- /dev/null +++ b/tasks/EncryptResponsesTask.php @@ -0,0 +1,33 @@ +Response, 0, $length) === $needle || substr($order->Response, 0, $length) === $needle2) { + + $encrypted = rc4crypt::encrypt(FoxyCart::getStoreKey(), $order->Response); + $encrypted = urlencode($encrypted); + + $order->Response = $encrypted; + $order->write(); + $ct++; + } + + } + echo $ct . ' order responses encrypted'; + + } + +} \ No newline at end of file diff --git a/tasks/ParseOrdersTask.php b/tasks/ParseOrdersTask.php new file mode 100644 index 0000000..64060d0 --- /dev/null +++ b/tasks/ParseOrdersTask.php @@ -0,0 +1,21 @@ +parseOrder()) { + $order->write(); + $ct++; + } + } + echo $ct . ' orders updated'; + + } + +} \ No newline at end of file diff --git a/templates/Layout/OrderHistoryPage.ss b/templates/Layout/OrderHistoryPage.ss index 2131d89..acb984d 100755 --- a/templates/Layout/OrderHistoryPage.ss +++ b/templates/Layout/OrderHistoryPage.ss @@ -15,36 +15,54 @@ <% if $Orders %> <% loop $Orders %>
-