/** * Update recurring tax for subscriptions * Not executed if Subscriptions plugin is not active * * @since 4.4 * @return void */ function wootax_update_recurring_tax() { global $wpdb; // Exit if subs is not active if (!WT_SUBS_ACTIVE) { return; } // Find date/time 12 hours from now $twelve_hours = mktime(date('H') + 12); $date = new DateTime(date('c', $twelve_hours)); $date = $date->format('Y-m-d H:i:s'); // Set up logger $logger = false; if (WT_LOG_REQUESTS) { $logger = class_exists('WC_Logger') ? new WC_Logger() : WC()->logger(); $logger->add('wootax', 'Starting recurring tax update. Subscriptions with payments due before ' . $date . ' are being considered.'); } // Get all scheduled "scheduled_subscription_payment" actions with post_date <= $twelve_hours $scheduled = $wpdb->get_results($wpdb->prepare("SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = %s AND post_title = %s AND post_date <= %s", "pending", "scheduled_subscription_payment", $date)); // Update recurring totals if necessary if (count($scheduled) > 0) { // This will hold any warning messages that need to be sent to the admin $warnings = array(); foreach ($scheduled as $action) { $temp_warnings = array(); $show_warnings = false; // Run json_decode on post_content to extract user_id and subscription_key $args = json_decode($action->post_content); // Parse subscription_key to get order_id/product_id (format: ORDERID_PRODUCTID) $subscription_key = $args->subscription_key; $key_parts = explode('_', $subscription_key); $order_id = (int) $key_parts[0]; $product_id = (int) $key_parts[1]; if (get_post_status($order_id) == false) { continue; // Skip if the order no longer exists } // Determine if changes to subscription amounts are allowed by the current gateway $chosen_gateway = WC_Subscriptions_Payment_Gateways::get_payment_gateway(get_post_meta($order_id, '_recurring_payment_method', true)); $manual_renewal = WC_Subscriptions_Order::requires_manual_renewal($order_id); $changes_supported = $chosen_gateway === false || $manual_renewal == 'true' || $chosen_gateway->supports('subscription_amount_changes') ? true : false; // Load order $order = WT_Orders::get_order($order_id); // Collect data for Lookup request $item_data = $type_array = array(); // Add subscription $product = WC_Subscriptions::get_product($product_id); // Get order item ID $item_id = $wpdb->get_var("SELECT i.order_item_id FROM {$wpdb->prefix}woocommerce_order_items i LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta im ON im.order_item_id = i.order_item_id WHERE im.meta_key = '_product_id' AND im.meta_value = {$product_id} AND i.order_id = {$order_id}"); // Get price $recurring_subtotal = $order->get_item_meta($item_id, '_recurring_line_subtotal'); $regular_subtotal = $order->get_item_meta($item_id, '_line_subtotal'); $price = $recurring_subtotal === '0' || !empty($recurring_subtotal) ? $recurring_subtotal : $regular_subtotal; // Special case: If _subscription_sign_up_fee is set and $price is equal to its value, fall back to product price if ($order->get_item_meta($item_id, '_subscription_sign_up_fee') == $price) { $price = $product->get_price(); } $item_info = array('Index' => '', 'ItemID' => $item_id, 'Qty' => 1, 'Price' => $price, 'Type' => 'cart'); $tic = get_post_meta($product_id, 'wootax_tic', true); if (!empty($tic) && $tic) { $item_info['TIC'] = $tic; } $item_data[] = $item_info; $type_array[$item_id] = 'cart'; // Add recurring shipping items foreach ($order->order->get_items('recurring_shipping') as $item_id => $shipping) { $item_data[] = array('Index' => '', 'ItemID' => $item_id, 'TIC' => WT_SHIPPING_TIC, 'Qty' => 1, 'Price' => $shipping['cost'], 'Type' => 'shipping'); $type_array[$item_id] = 'shipping'; } // Reset "captured" meta so lookup always sent $captured = WT_Orders::get_meta($order_id, 'captured'); WT_Orders::update_meta($order_id, 'captured', false); // Issue Lookup request $res = $order->do_lookup($item_data, $type_array, true); // Set "captured" back to original value WT_Orders::update_meta($order_id, 'captured', $captured); // If lookup is successful, use result to update recurring tax totals as described here: http://docs.woothemes.com/document/subscriptions/add-or-modify-a-subscription/#change-recurring-total if (is_array($res)) { // Find recurring tax item and determine original tax/shipping tax totals $wootax_item_id = $wpdb->get_var($wpdb->prepare("SELECT i.order_item_id FROM {$wpdb->prefix}woocommerce_order_items i LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta im ON im.order_item_id = i.order_item_id WHERE im.meta_key = %s AND im.meta_value = %d AND i.order_id = %d AND i.order_item_type = %s", "rate_id", WT_RATE_ID, $order_id, "recurring_tax")); $old_tax = empty($wootax_item_id) ? 0 : $order->get_item_meta($wootax_item_id, 'tax_amount'); $old_shipping_tax = empty($wootax_item_id) ? 0 : $order->get_item_meta($wootax_item_id, 'shipping_tax_amount'); // Find new tax/shipping tax totals // Update _recurring_line_tax meta for each item $tax = $shipping_tax = 0; foreach ($res as $item) { $item_id = $item->ItemID; $item_tax = $item->TaxAmount; if ($type_array[$item_id] == 'shipping') { $shipping_tax += $item_tax; } else { $tax += $item_tax; } if ($changes_supported) { wc_update_order_item_meta($item_id, '_recurring_line_tax', $item_tax); wc_update_order_item_meta($item_id, '_recurring_line_subtotal_tax', $item_tax); } else { $temp_warnings[] = 'Recurring tax for item #' . $item_id . ' changed to ' . wc_round_tax_total($item_tax); } } // Update recurring tax if necessary if ($old_tax != $tax || $old_shipping_tax != $shipping_tax) { if ($changes_supported) { if (!empty($wootax_item_id)) { wc_update_order_item_meta($wootax_item_id, 'tax_amount', $tax); wc_update_order_item_meta($wootax_item_id, 'cart_tax', $tax); wc_update_order_item_meta($wootax_item_id, 'shipping_tax_amount', $shipping_tax); wc_update_order_item_meta($wootax_item_id, 'shipping_tax', $shipping_tax); } // Determine rounded difference in old/new tax totals $tax_diff = $tax + $shipping_tax - ($old_tax + $old_shipping_tax); $rounded_tax_diff = wc_round_tax_total($tax_diff); // Set new recurring total by adding difference between old and new tax to existing total $new_recurring_total = get_post_meta($order_id, '_order_recurring_total', true) + $rounded_tax_diff; update_post_meta($order_id, '_order_recurring_total', $new_recurring_total); if ($logger) { $logger->add('wootax', 'Set recurring total for order ' . $order_id . ' to: ' . $new_recurring_total); } } else { $temp_warnings[] = 'Total recurring tax changed from ' . wc_round_tax_total($old_tax) . ' to ' . wc_round_tax_total($tax); $temp_warnings[] = 'Total recurring shipping tax changed from ' . wc_round_tax_total($old_shipping_tax) . ' to ' . wc_round_tax_total($shipping_tax); $show_warnings = true; } } // Add to warnings array if necessary if ($show_warnings) { $warnings[$order_id] = $temp_warnings; } } } // Send out a single warning email to the admin if necessary // Ex: Email sent if a change in tax rates is detected and the gateway used by an order doesn't allow modification of sub. details if (count($warnings) > 0) { $email = wootax_get_notification_email(); if (!empty($email) && is_email($email)) { $subject = 'WooTax Warning: Recurring Tax Totals Need To Be Updated'; $message = 'Hello,' . "\r\n\r\n" . 'During a routine check on ' . date('m/d/Y') . ', WooTax discovered ' . count($warnings) . ' subscription orders whose recurring tax totals need to be updated. Unfortunately, the payment gateway(s) used for these orders does not allow subscription details to be altered, so the required changes must be implemented manually. All changes are listed below for your convenience.' . "\r\n\r\n"; foreach ($warnings as $order_id => $errors) { $message .= 'Order ' . $order_id . ': ' . "\r\n\r\n"; foreach ($errors as $error) { $message .= '- ' . $error . "\r\n"; } $message .= "\r\n\r\n"; } $message .= 'For assistance, please contact the WooTax support team at sales@wootax.com.'; wp_mail($email, $subject, $message); } } } else { if ($logger) { $logger->add('wootax', 'Ending recurring tax update. No subscriptions due before ' . $date . '.'); } } }
/** * Process partial refunds; prepare data and send Returned request to TaxCloud * * @since 4.4 * @param (WC_WooTax_Refund) $refund refund order object * @param (bool) $cron is this method being called from a WooTax cronjob? */ public static function process_refund($refund, $cron = false) { $refund_items = array(); // Get order object (use original order ID, not refund order ID) $order_id = $refund->post->post_parent; $order = WT_Orders::get_order($order_id); // Holds the key of the first found origin address for the order $first_found = WT_Orders::get_meta($order_id, 'first_found'); $first_found = empty($first_found) ? false : $first_found; // Create array mapping product IDs to locations // This needs to be done since the refund order and original order will not have same item IDs $mapping_array = array(); $id_array = array(); $identifiers = WT_Orders::get_meta($order_id, 'identifiers'); foreach ($order->order->get_items() as $item_id => $item) { $product_id = !empty($item['variation_id']) ? $item['variation_id'] : $item['product_id']; $mapping_array[$product_id] = $order->get_item_meta($item_id, '_wootax_location_id'); if (isset($identifiers[$product_id])) { $identifier = $identifiers[$product_id]; } else { $identifier = $item_id; } $id_array[$product_id] = $identifier; } foreach ($order->order->get_fees() as $fee_id => $item) { $fee_key = sanitize_title($item['name']); if (isset($identifiers[$fee_key])) { $identifier = $fee_key; } else { $identifier = $fee_id; } $id_array[$fee_key] = $identifier; } $id_array[WT_SHIPPING_ITEM] = isset($identifiers[WT_SHIPPING_ITEM]) ? $identifiers[WT_SHIPPING_ITEM] : WT_SHIPPING_ITEM; if (version_compare(WOOCOMMERCE_VERSION, '2.2', '>=') && !isset($identifiers[WT_SHIPPING_ITEM])) { foreach ($order->order->get_shipping_methods() as $method_id => $method) { $id_array[WT_SHIPPING_ITEM] = $method_id; } } // Add items foreach ($refund->get_items() as $item_id => $item) { $product = $refund->get_product_from_item($item); if ($item['qty'] == 0) { continue; } $product_id = !empty($item['variation_id']) ? $item['variation_id'] : $item['product_id']; $tic = get_post_meta($item['product_id'], 'wootax_tic', true); // Get location key for item $location_key = $mapping_array[$product_id]; // Get real item ID // When a Lookup has not been sent from the backend yet, this will be the item key sent during checkout $product_id = $id_array[$product_id]; // Set first found if needed if ($first_found === false) { $first_found = $location_key; } if (!isset($refund_items[$location_key])) { $refund_items[$location_key] = array(); } $new_item = array('Index' => '', 'ItemID' => $product_id, 'TIC' => '', 'Qty' => $item['qty'], 'Price' => $item['line_total'] * -1 / $item['qty']); if ($tic !== false && !empty($tic)) { $new_item['TIC'] = $tic; } $refund_items[$location_key][] = $new_item; } // Add fees foreach ($refund->get_fees() as $fee_id => $fee) { if ($fee['line_total'] == 0) { continue; } // Get item ID $key = sanitize_title($fee['name']); $real_id = $id_array[$key]; $refund_items[$first_found][] = array('Index' => '', 'ItemID' => $real_id, 'TIC' => WT_FEE_TIC, 'Qty' => 1, 'Price' => $fee['line_total'] * -1); } // Add shipping costs // Shipping costs are always associated with first found location $shipping_cost = $refund->get_total_shipping(); if ($shipping_cost != 0) { $item_id = $id_array[WT_SHIPPING_ITEM]; $refund_items[$first_found][] = array('Index' => '', 'ItemID' => $item_id, 'TIC' => WT_SHIPPING_TIC, 'Qty' => 1, 'Price' => $shipping_cost * -1); } // Process refund $res = WT_Orders::refund_order($order_id, $cron, $refund_items); if ($res !== true && !$cron) { // Delete refund wp_delete_post($refund->post->ID); // Throw exception so refund is halted throw new Exception('Refund failed: ' . $res); } else { if ($res !== true && $cron) { return $res; } else { if ($cron) { return true; } } } }
/** * Outputs HTML for Sales Tax metabox * * @since 4.2 * @param (WP_Post) $post WordPress post object */ public static function output_tax_metabox($post) { // Load order $order = WT_Orders::get_order($post->ID); // Display tax totals ?> <p>The status of this order in TaxCloud is displayed below. There are three possible values for the order status: "Pending Capture," "Captured," and "Refunded."</p> <p>Eventually, all of your orders should have a status of "Captured." To mark an order as captured, set its status to "Completed" and save it.</p> <p><strong>Please note that tax can only be calculated using the "Calculate Taxes" button if the status below is "Pending Capture."</strong></p> <p><strong>TaxCloud Status:</strong> <?php echo $order->get_status(); ?> <br /></p> <?php }
/** * Notify TaxCloud of tax collected on renewals * * @since 4.0 * @param (int) $order_id the ID of the renewal order */ public function handle_renewal_order($order_id) { // Create a new WC_WooTax_Order object $order = WT_Orders::get_order($order_id); // Set destination address based on original order $renewal_parent = get_post_meta($order_id, '_original_order', true); $original_order = new WC_Order($renewal_parent); $order->destination_address = $this->get_destination_address($original_order); // Reset order meta values to default foreach (WT_Orders::$defaults as $key => $val) { WT_Orders::update_meta($order_id, $key, $val); } // Build order items array $final_items = array(); $type_array = array(); if (version_compare(WOOCOMMERCE_VERSION, '2.2', '>=')) { $order_items = $order->order->get_items() + $order->order->get_fees() + $order->order->get_shipping_methods(); } else { $order_items = $order->order->get_items() + $order->order->get_fees(); } // Construct items array from POST data foreach ($order_items as $item_id => $item) { $product_id = isset($item['product_id']) ? $item['product_id'] : -1; $qty = isset($item['qty']) ? $item['qty'] : 1; $type = $item['type']; $cost = isset($item['cost']) ? $item['cost'] : $item['line_total']; // 'cost' key used by shipping methods in 2.2 switch ($type) { case 'shipping': $tic = WT_SHIPPING_TIC; $type = 'shipping'; break; case 'fee': $tic = WT_FEE_TIC; $type = 'fee'; break; case 'line_item': $tic = get_post_meta($product_id, 'wootax_tic', true); $type = 'cart'; break; } // Calculate unit price $unit_price = $cost / $qty; // Add item to final items array if ($unit_price != 0) { // Map item_id to item type $type_array[$item_id] = $type == 'shipping' ? 'shipping' : 'cart'; // Add to items array $item_data = array('Index' => '', 'ItemID' => $item_id, 'Qty' => $qty, 'Price' => $unit_price, 'Type' => $type); if (!empty($tic) && $tic) { $item_data['TIC'] = $tic; } $final_items[] = $item_data; } } // If we are not using WC 2.2, we need to add a shipping item manually if (version_compare(WOOCOMMERCE_VERSION, '2.2', '<')) { if ($order->order->get_total_shipping() > 0) { $item_data = array('Index' => '', 'ItemID' => WT_SHIPPING_ITEM, 'Qty' => 1, 'Price' => $order->order->get_total_shipping(), 'Type' => 'shipping', 'TIC' => WT_SHIPPING_TIC); $type_array[WT_SHIPPING_ITEM] = 'shipping'; $final_items[] = $item_data; } } // Perform a tax lookup with the renewal prices $result = $order->do_lookup($final_items, $type_array); // Add errors as notes if necessary if ($result != true) { $original_order->add_order_note(sprintf(__('Tax lookup for renewal order %s failed. Reason: ' . $result, 'woocommerce-subscriptions'), $order_id)); } else { // Mark order as captured $order->order->update_status('completed'); // Add success note $original_order->add_order_note(sprintf(__('TaxCloud was successfully notified of renewal order %s.', 'woocommerce-subscriptions'), $order_id)); } }