/**
  * Get order line items (products) in a neatly-formatted array of objects
  * with properties:
  *
  * + id - item ID
  * + name - item name, usually product title, processed through htmlentities()
  * + description - formatted item meta (e.g. Size: Medium, Color: blue), processed through htmlentities()
  * + quantity - item quantity
  * + item_total - item total (line total divided by quantity, excluding tax & rounded)
  * + line_total - line item total (excluding tax & rounded)
  * + meta - formatted item meta array
  * + product - item product or null if getting product from item failed
  * + item - raw item array
  *
  * @since 3.0.0
  * @param \WC_Order $order
  * @return array
  */
 public static function get_order_line_items($order)
 {
     $line_items = array();
     foreach ($order->get_items() as $id => $item) {
         $line_item = new stdClass();
         $product = $order->get_product_from_item($item);
         $meta = SV_WC_Plugin_Compatibility::is_wc_version_gte_2_4() ? $item : $item['item_meta'];
         // get meta + format it
         $item_meta = new WC_Order_Item_Meta($meta);
         $item_meta = $item_meta->get_formatted();
         if (!empty($item_meta)) {
             $item_desc = array();
             foreach ($item_meta as $meta) {
                 $item_desc[] = sprintf('%s: %s', $meta['label'], $meta['value']);
             }
             $item_desc = implode(', ', $item_desc);
         } else {
             // default description to SKU
             $item_desc = is_callable(array($product, 'get_sku')) && $product->get_sku() ? sprintf('SKU: %s', $product->get_sku()) : null;
         }
         $line_item->id = $id;
         $line_item->name = htmlentities($item['name'], ENT_QUOTES, 'UTF-8', false);
         $line_item->description = htmlentities($item_desc, ENT_QUOTES, 'UTF-8', false);
         $line_item->quantity = $item['qty'];
         $line_item->item_total = isset($item['recurring_line_total']) ? $item['recurring_line_total'] : $order->get_item_total($item);
         $line_item->line_total = $order->get_line_total($item);
         $line_item->meta = $item_meta;
         $line_item->product = is_object($product) ? $product : null;
         $line_item->item = $item;
         $line_items[] = $line_item;
     }
     return $line_items;
 }
 /**
  * Get the invoice line items for a given order. This includes:
  *
  * + Products, mapped as FreshBooks items when the admin has linked them
  * + Fees, mapped as a `Fee` item
  * + Shipping, mapped as a `Shipping` item
  * + Taxes, mapped using the tax code as the item
  *
  * Note that taxes cannot be added per-product as WooCommerce doesn't provide
  * any way to get individual tax information per product and FreshBooks requires
  * a tax name/percentage to be set on a per-product basis. Further, FreshBooks
  * only allows 2 taxes per product where realistically most stores will have
  * more
  *
  * @param \WC_FreshBooks_Order $order order instance
  * @return array line items
  * @since 3.0
  */
 private function get_invoice_lines(WC_FreshBooks_Order $order)
 {
     $line_items = array();
     // add products
     foreach ($order->get_items() as $item_key => $item) {
         $product = $order->get_product_from_item($item);
         // must be a valid product
         if (is_object($product)) {
             $product_id = $product->is_type('variation') ? $product->variation_id : $product->id;
             $item_name = metadata_exists('post', $product_id, '_wc_freshbooks_item_name') ? get_post_meta($product_id, '_wc_freshbooks_item_name', true) : $product->get_sku();
             $item_meta = new WC_Order_Item_Meta(SV_WC_Plugin_Compatibility::is_wc_version_gte_2_4() ? $item : $item['item_meta']);
             // variation data, item meta, etc
             $meta = $item_meta->display(true, true);
             // grouped products include a &arr; in the name which must be converted back to an arrow
             $item_description = html_entity_decode($product->get_title(), ENT_QUOTES, 'UTF-8') . ($meta ? sprintf(' (%s)', $meta) : '');
         } else {
             $item_name = __('Product', WC_FreshBooks::TEXT_DOMAIN);
             $item_description = $item['name'];
         }
         $line_items[] = array('name' => $item_name, 'description' => apply_filters('wc_freshbooks_line_item_description', $item_description, $item, $order, $item_key), 'unit_cost' => $order->get_item_subtotal($item), 'quantity' => $item['qty']);
     }
     $coupons = $order->get_items('coupon');
     // add coupons
     foreach ($coupons as $coupon_item) {
         $coupon = new WC_Coupon($coupon_item['name']);
         $coupon_post = get_post($coupon->id);
         $coupon_type = false !== strpos($coupon->type, '_cart') ? __('Cart Discount', WC_FreshBooks::TEXT_DOMAIN) : __('Product Discount', WC_FreshBooks::TEXT_DOMAIN);
         $line_items[] = array('name' => $coupon_item['name'], 'description' => is_object($coupon_post) && $coupon_post->post_excerpt ? sprintf(__('%s - %s', WC_FreshBooks::TEXT_DOMAIN), $coupon_type, $coupon_post->post_excerpt) : $coupon_type, 'unit_cost' => wc_format_decimal($coupon_item['discount_amount'] * -1, 2), 'quantity' => 1);
     }
     // manually created orders can't have coupon items, but may have an order discount set
     // which must be added as a line item
     if (0 == count($coupons) && $order->get_total_discount() > 0) {
         $line_items[] = array('name' => __('Order Discount', WC_FreshBooks::TEXT_DOMAIN), 'description' => __('Order Discount', WC_FreshBooks::TEXT_DOMAIN), 'unit_cost' => wc_format_decimal($order->get_total_discount() * -1, 2), 'quantity' => 1);
     }
     // add fees
     foreach ($order->get_fees() as $fee_key => $fee) {
         $line_items[] = array('name' => __('Fee', WC_FreshBooks::TEXT_DOMAIN), 'description' => $fee['name'], 'unit_cost' => $order->get_line_total($fee), 'quantity' => 1);
     }
     // add shipping
     foreach ($order->get_shipping_methods() as $shipping_method_key => $shipping_method) {
         $line_items[] = array('name' => __('Shipping', WC_FreshBooks::TEXT_DOMAIN), 'description' => ucwords(str_replace('_', ' ', $shipping_method['method_id'])), 'unit_cost' => $shipping_method['cost'], 'quantity' => 1);
     }
     // add taxes
     foreach ($order->get_tax_totals() as $tax_code => $tax) {
         $line_items[] = array('name' => $tax_code, 'description' => $tax->label, 'unit_cost' => number_Format($tax->amount, 2, '.', ''), 'quantity' => 1);
     }
     return $line_items;
 }
 /**
  * Get the order data for a single CSV row
  *
  * Note items are keyed according to the column header keys above so these can be modified using
  * the provider filter without needing to worry about the array order
  *
  * @since 3.0
  * @param int $order_id the WC_Order ID
  * @return array order data in the format key => content
  */
 private function get_orders_csv_row($order_id)
 {
     $order = wc_get_order($order_id);
     $line_items = $shipping_items = $fee_items = $tax_items = $coupon_items = array();
     // get line items
     foreach ($order->get_items() as $item_id => $item) {
         $product = $order->get_product_from_item($item);
         if (!is_object($product)) {
             $product = new WC_Product(0);
         }
         $item_meta = new WC_Order_Item_Meta(SV_WC_Plugin_Compatibility::is_wc_version_gte_2_4() ? $item : $item['item_meta']);
         $meta = $item_meta->display(true, true);
         if ($meta) {
             // remove newlines
             $meta = str_replace(array("\r", "\r\n", "\n"), '', $meta);
             // switch reserved chars (:;|) to =
             $meta = str_replace(array(': ', ':', ';', '|'), '=', $meta);
         }
         $line_item = array('name' => html_entity_decode($product->get_title() ? $product->get_title() : $item['name'], ENT_NOQUOTES, 'UTF-8'), 'sku' => $product->get_sku(), 'quantity' => $item['qty'], 'total' => wc_format_decimal($order->get_line_total($item), 2), 'refunded' => wc_format_decimal($order->get_total_refunded_for_item($item_id), 2), 'meta' => html_entity_decode($meta, ENT_NOQUOTES, 'UTF-8'));
         // add line item tax
         $line_tax_data = isset($item['line_tax_data']) ? $item['line_tax_data'] : array();
         $tax_data = maybe_unserialize($line_tax_data);
         $line_item['tax'] = isset($tax_data['total']) ? wc_format_decimal(wc_round_tax_total(array_sum((array) $tax_data['total'])), 2) : '';
         /**
          * CSV Order Export Line Item.
          *
          * Filter the individual line item entry for the default export
          *
          * @since 3.0.6
          * @param array $line_item {
          *     line item data in key => value format
          *     the keys are for convenience and not used for exporting. Make
          *     sure to prefix the values with the desired line item entry name
          * }
          *
          * @param array $item WC order item data
          * @param WC_Product $product the product
          * @param WC_Order $order the order
          * @param \WC_Customer_Order_CSV_Export_Generator $this, generator instance
          */
         $line_item = apply_filters('wc_customer_order_csv_export_order_line_item', $line_item, $item, $product, $order, $this);
         if ('default_one_row_per_item' !== $this->order_format && is_array($line_item)) {
             foreach ($line_item as $name => $value) {
                 $line_item[$name] = $name . ':' . $value;
             }
             $line_item = implode('|', $line_item);
         }
         if ($line_item) {
             $line_items[] = $line_item;
         }
     }
     foreach ($order->get_shipping_methods() as $_ => $shipping_item) {
         $shipping_items[] = implode('|', array('method:' . $shipping_item['name'], 'total:' . wc_format_decimal($shipping_item['cost'], 2)));
     }
     // get fee items & total
     $fee_total = 0;
     $fee_tax_total = 0;
     foreach ($order->get_fees() as $fee_id => $fee) {
         $fee_items[] = implode('|', array('name:' . $fee['name'], 'total:' . wc_format_decimal($fee['line_total'], 2), 'tax:' . wc_format_decimal($fee['line_tax'], 2)));
         $fee_total += $fee['line_total'];
         $fee_tax_total += $fee['line_tax'];
     }
     // get tax items
     foreach ($order->get_tax_totals() as $tax_code => $tax) {
         $tax_items[] = implode('|', array('code:' . $tax_code, 'total:' . wc_format_decimal($tax->amount, 2)));
     }
     // add coupons
     foreach ($order->get_items('coupon') as $_ => $coupon_item) {
         $coupon = new WC_Coupon($coupon_item['name']);
         $coupon_post = get_post($coupon->id);
         $coupon_items[] = implode('|', array('code:' . $coupon_item['name'], 'description:' . (is_object($coupon_post) ? $coupon_post->post_excerpt : ''), 'amount:' . wc_format_decimal($coupon_item['discount_amount'], 2)));
     }
     $order_data = array('order_id' => $order->id, 'order_number' => $order->get_order_number(), 'order_date' => $order->order_date, 'status' => $order->get_status(), 'shipping_total' => $order->get_total_shipping(), 'shipping_tax_total' => wc_format_decimal($order->get_shipping_tax(), 2), 'fee_total' => wc_format_decimal($fee_total, 2), 'fee_tax_total' => wc_format_decimal($fee_tax_total, 2), 'tax_total' => wc_format_decimal($order->get_total_tax(), 2), 'cart_discount' => SV_WC_Plugin_Compatibility::is_wc_version_gte_2_3() ? wc_format_decimal($order->get_total_discount(), 2) : wc_format_decimal($order->get_cart_discount(), 2), 'order_discount' => SV_WC_Plugin_Compatibility::is_wc_version_gte_2_3() ? wc_format_decimal($order->get_total_discount(), 2) : wc_format_decimal($order->get_order_discount(), 2), 'discount_total' => wc_format_decimal($order->get_total_discount(), 2), 'order_total' => wc_format_decimal($order->get_total(), 2), 'refunded_total' => wc_format_decimal($order->get_total_refunded(), 2), 'order_currency' => $order->get_order_currency(), 'payment_method' => $order->payment_method, 'shipping_method' => $order->get_shipping_method(), 'customer_id' => $order->get_user_id(), 'billing_first_name' => $order->billing_first_name, 'billing_last_name' => $order->billing_last_name, 'billing_company' => $order->billing_company, 'billing_email' => $order->billing_email, 'billing_phone' => $order->billing_phone, 'billing_address_1' => $order->billing_address_1, 'billing_address_2' => $order->billing_address_2, 'billing_postcode' => $order->billing_postcode, 'billing_city' => $order->billing_city, 'billing_state' => $order->billing_state, 'billing_country' => $order->billing_country, 'shipping_first_name' => $order->shipping_first_name, 'shipping_last_name' => $order->shipping_last_name, 'shipping_company' => $order->shipping_company, 'shipping_address_1' => $order->shipping_address_1, 'shipping_address_2' => $order->shipping_address_2, 'shipping_postcode' => $order->shipping_postcode, 'shipping_city' => $order->shipping_city, 'shipping_state' => $order->shipping_state, 'shipping_country' => $order->shipping_country, 'customer_note' => $order->customer_note, 'shipping_items' => implode(';', $shipping_items), 'fee_items' => implode(';', $fee_items), 'tax_items' => implode(';', $tax_items), 'coupon_items' => implode(';', $coupon_items), 'order_notes' => implode('|', $this->get_order_notes($order)), 'download_permissions' => $order->download_permissions_granted ? $order->download_permissions_granted : 0);
     if ('default_one_row_per_item' === $this->order_format) {
         $new_order_data = array();
         foreach ($line_items as $item) {
             $order_data['item_name'] = $item['name'];
             $order_data['item_sku'] = $item['sku'];
             $order_data['item_quantity'] = $item['quantity'];
             $order_data['item_tax'] = $item['tax'];
             $order_data['item_total'] = $item['total'];
             $order_data['item_refunded'] = $item['refunded'];
             $order_data['item_meta'] = $item['meta'];
             /**
              * CSV Order Export Row for One Row per Item.
              *
              * Filter the individual row data for the order export
              *
              * @since 3.3.0
              * @param array $order_data {
              *     order data in key => value format
              *     to modify the row data, ensure the key matches any of the header keys and set your own value
              * }
              * @param array $item
              * @param \WC_Order $order WC Order object
              * @param \WC_Customer_Order_CSV_Export_Generator $this, generator instance
              */
             $new_order_data[] = apply_filters('wc_customer_order_csv_export_order_row_one_row_per_item', $order_data, $item, $order, $this);
         }
         $order_data = $new_order_data;
     } else {
         $order_data['line_items'] = implode(';', $line_items);
     }
     /**
      * CSV Order Export Row.
      *
      * Filter the individual row data for the order export
      *
      * @since 3.0
      * @param array $order_data {
      *     order data in key => value format
      *     to modify the row data, ensure the key matches any of the header keys and set your own value
      * }
      * @param \WC_Order $order WC Order object
      * @param \WC_Customer_Order_CSV_Export_Generator $this, generator instance
      */
     return apply_filters('wc_customer_order_csv_export_order_row', $order_data, $order, $this);
 }
 /**
  * Get the order data format for a single column for all line items, compatible with the legacy (pre 3.0) CSV Export format
  *
  * Note this code was adapted from the old code to maintain compatibility as close as possible, so it should
  * not be modified unless absolutely necessary
  *
  * @since 3.0
  * @param array $order_data an array of order data for the given order
  * @param WC_Order $order the WC_Order object
  * @return array modified order data
  */
 private function get_legacy_single_column_line_item($order_data, WC_Order $order)
 {
     $line_items = array();
     foreach ($order->get_items() as $_ => $item) {
         $product = $order->get_product_from_item($item);
         if (!is_object($product)) {
             $product = new WC_Product(0);
         }
         $line_item = $item['name'];
         if ($product->get_sku()) {
             $line_item .= ' (' . $product->get_sku() . ')';
         }
         $line_item .= ' x' . $item['qty'];
         $item_meta = new WC_Order_Item_Meta(SV_WC_Plugin_Compatibility::is_wc_version_gte_2_4() ? $item : $item['item_meta']);
         $variation = $item_meta->display(true, true);
         if ($variation) {
             $line_item .= ' - ' . str_replace(array("\r", "\r\n", "\n"), '', $variation);
         }
         $line_items[] = str_replace(array('“', '”'), '', $line_item);
     }
     $order_data['order_items'] = implode('; ', $line_items);
     // convert country codes to full name
     if (isset(WC()->countries->countries[$order->billing_country])) {
         $order_data['billing_country'] = WC()->countries->countries[$order->billing_country];
     }
     if (isset(WC()->countries->countries[$order->shipping_country])) {
         $order_data['shipping_country'] = WC()->countries->countries[$order->shipping_country];
     }
     // set order ID to order number
     $order_data['order_id'] = ltrim($order->get_order_number(), _x('#', 'hash before the order number', WC_Customer_Order_CSV_Export::TEXT_DOMAIN));
     return $order_data;
 }
 /**
  * Mark an order as voided. Because WC has no status for "void", we use
  * refunded.
  *
  * @since 3.1.0
  * @param WC_Order $order order object
  */
 public function mark_order_as_voided($order, $response)
 {
     $message = sprintf(_x('%s Void in the amount of %s approved.', 'Supports voids', $this->text_domain), $this->get_method_title(), wc_price($order->refund->amount, array('currency' => $order->get_order_currency())));
     // adds the transaction id (if any) to the order note
     if ($response->get_transaction_id()) {
         $message .= ' ' . sprintf(_x('(Transaction ID %s)', 'Supports voids', $this->text_domain), $response->get_transaction_id());
     }
     // mark order as cancelled, since no money was actually transferred
     if (!$order->has_status('cancelled')) {
         $this->voided_order_message = $message;
         // voids are fully "refunded" so cancel the voided order instead of marking as refunded
         if (SV_WC_Plugin_Compatibility::is_wc_version_gte_2_4()) {
             // filter in WC 2.4+ allows us to skip the "refunded" then "cancelled" transition
             add_filter('woocommerce_order_fully_refunded_status', array($this, 'maybe_cancel_voided_order'), 10, 2);
         } else {
             // WC 2.3/2.2 requires changing the order status to cancelled after it's already been changed to refunded ಠ_ಠ
             add_action('woocommerce_order_refunded', array($this, 'maybe_cancel_voided_order_2_3'));
         }
     } else {
         $order->add_order_note($message);
     }
 }