setup_payment( $payment_id ); } /** * Magic GET function * * @since 2.5 * * @param string $key The property * * @return mixed The value */ public function __get( $key ) { if ( method_exists( $this, "get_{$key}" ) ) { $value = call_user_func( array( $this, "get_{$key}" ) ); } elseif ( 'id' === $key ) { $value = $this->ID; } elseif ( 'post_type' === $key ) { $value = 'edd_payment'; } elseif ( 'post_date' === $key ) { $value = $this->date; } else { $value = $this->$key; } return $value; } /** * Magic SET function * * Sets up the pending array for the save method * * @since 2.5 * * @param string $key The property name * @param mixed $value The value of the property */ public function __set( $key, $value ) { $ignore = array( 'downloads', 'cart_details', 'fees', '_ID' ); if ( $key === 'status' ) { $this->old_status = $this->status; } if ( ! in_array( $key, $ignore ) ) { $this->pending[ $key ] = $value; } if ( '_ID' !== $key ) { $this->$key = $value; } } /** * Magic ISSET function, which allows empty checks on protected elements * * @since 2.5 * * @param string $name The attribute to get * * @return boolean If the item is set or not */ public function __isset( $name ) { if ( property_exists( $this, $name ) ) { return false === empty( $this->$name ); } else { return null; } } /** * Setup payment properties * * @since 2.5 * * @param int $payment_id The payment ID * * @return bool If the setup was successful or not */ private function setup_payment( $payment_id ) { $this->pending = array(); if ( empty( $payment_id ) ) { return false; } $this->order = $this->_shim_edd_get_order( $payment_id ); if ( ! $this->order || is_wp_error( $this->order ) ) { return _edd_get_final_payment_id() ? $this->_setup_compat_payment( $payment_id ) : false; } // Allow extensions to perform actions before the payment is loaded do_action( 'edd_pre_setup_payment', $this, $payment_id ); // Primary Identifier $this->ID = absint( $payment_id ); // Protected ID that can never be changed $this->_ID = absint( $payment_id ); // Status and Dates $this->date = $this->order->date_created; $this->completed_date = $this->setup_completed_date(); $this->status = $this->order->status; $this->post_status = $this->order->status; $this->mode = $this->order->mode; $this->parent_payment = $this->order->parent; $all_payment_statuses = edd_get_payment_statuses(); $this->status_nicename = array_key_exists( $this->status, $all_payment_statuses ) ? $all_payment_statuses[ $this->status ] : ucfirst( $this->status ); // Items $this->fees = $this->setup_fees(); $this->cart_details = $this->setup_cart_details(); $this->downloads = $this->setup_downloads(); // Currency Based $this->total = $this->order->total; $this->tax = $this->order->tax; $this->tax_rate = $this->setup_tax_rate(); $this->fees_total = $this->setup_fees_total(); $this->subtotal = $this->order->subtotal; $this->currency = $this->setup_currency(); // Gateway based $this->gateway = $this->order->gateway; $this->transaction_id = $this->setup_transaction_id(); // User based $this->ip = $this->order->ip; $this->customer_id = $this->order->customer_id; $this->user_id = $this->setup_user_id(); $this->email = $this->setup_email(); $this->discounts = $this->setup_discounts(); $this->user_info = $this->setup_user_info(); $this->first_name = $this->user_info['first_name']; $this->last_name = $this->user_info['last_name']; $this->address = $this->setup_address(); // Other Identifiers $this->key = $this->order->payment_key; $this->number = $this->setup_payment_number(); // Additional Attributes $this->has_unlimited_downloads = $this->setup_has_unlimited(); // We have a payment, get the generic payment_meta item to reduce calls to it // This only exists for backwards compatibility purposes. $this->payment_meta = $this->get_meta(); // Allow extensions to add items to this object via hook do_action( 'edd_setup_payment', $this, $payment_id ); return true; } /** * Create the base of a payment. * * @since 2.5 * @since 3.0 Updated to insert orders to the new custom tables. * * @return int|bool False on failure, the order ID on success. */ private function insert_payment() { $payment_data = array( 'price' => $this->total, 'date' => $this->date, 'user_email' => $this->email, 'purchase_key' => $this->key, 'currency' => $this->currency, 'downloads' => $this->downloads, 'user_info' => array( 'id' => $this->user_id, 'email' => $this->email, 'first_name' => $this->first_name, 'last_name' => $this->last_name, 'discount' => $this->discounts, 'address' => $this->address, ), 'cart_details' => $this->cart_details, 'status' => $this->status, 'fees' => $this->fees, ); // Create an order $order_args = array( 'parent' => $this->parent_payment, 'status' => $this->status, 'user_id' => $this->user_id, 'email' => $this->email, 'ip' => $this->ip, 'gateway' => $this->gateway, 'mode' => $this->mode, 'currency' => $this->currency, 'payment_key' => $this->key, ); $order_id = edd_add_order( $order_args ); if ( ! empty( $order_id ) ) { $this->ID = $order_id; $this->_ID = $order_id; $customer = $this->maybe_create_customer(); $this->customer_id = $customer->id; $customer->attach_payment( $this->ID, false ); $order_data = array( 'customer_id' => $this->customer_id, ); /** * This run of the edd_payment_meta filter is for backwards compatibility purposes. The filter will also run * in the EDD_Payment::save method. By keeping this here, it retains compatibility of adding payment meta * prior to the payment being inserted, as was previously supported by edd_insert_payment(). * * @reference: https://github.com/easydigitaldownloads/easy-digital-downloads/issues/5838 */ $this->payment_meta = apply_filters( 'edd_payment_meta', $this->payment_meta, $payment_data ); if ( ! empty( $this->payment_meta['fees'] ) ) { $this->fees = array_merge( $this->payment_meta['fees'], $this->fees ); foreach ( $this->fees as $key => $fee ) { add_filter( 'edd_prices_include_tax', '__return_false' ); $tax = ( isset( $fee['no_tax'] ) && false === $fee['no_tax'] ) || $fee['amount'] < 0 ? floatval( edd_calculate_tax( $fee['amount'] ) ) : 0.00; remove_filter( 'edd_prices_include_tax', '__return_false' ); $adjustment_id = edd_add_order_adjustment( array( 'object_id' => $this->ID, 'object_type' => 'order', 'type_key' => $key, 'type' => 'fee', 'description' => $fee['label'], 'subtotal' => floatval( $fee['amount'] ), 'tax' => $tax, 'total' => floatval( $fee['amount'] ) + $tax, ) ); $this->increase_fees( $fee['amount'] ); } } if ( edd_get_option( 'enable_sequential' ) ) { $number = edd_get_next_payment_number(); $this->number = edd_format_payment_number( $number ); $this->update_meta( '_edd_payment_number', $this->number ); $order_data['order_number'] = $this->number; update_option( 'edd_last_payment_number', $number ); } edd_update_order( $order_id, $order_data ); $this->update_meta( '_edd_payment_meta', $this->payment_meta ); $tax_rate = $this->tax_rate; if ( ! empty( $tax_rate ) && $this->tax_rate > 0 && $this->tax_rate < 1 ) { $tax_rate = $tax_rate * 100; } $order_meta = array( 'tax_rate' => $tax_rate, ); foreach ( $order_meta as $key => $value ) { if ( ! empty( $value ) ) { edd_add_order_meta( $order_id, $key, $value ); } } $this->new = true; } return $this->ID; } /** * One items have been set, an update is needed to save them to the database. * * @since 3.0 Refactored to work with the new query methods. * * @return bool True of the save occurred, false if it failed or wasn't needed. */ public function save() { $saved = false; if ( empty( $this->ID ) ) { $payment_id = $this->insert_payment(); if ( false === $payment_id ) { $saved = false; } else { $this->ID = $payment_id; } } if ( $this->ID !== $this->_ID ) { $this->ID = $this->_ID; } // If the order is null, it means a new order is being added $this->order = $this->_shim_edd_get_order( $this->ID ); $customer = $this->maybe_create_customer(); if ( $this->customer_id !== $customer->id ) { $this->customer_id = $customer->id; $this->pending['customer_id'] = $this->customer_id; } // If we have something pending, let's save it if ( ! empty( $this->pending ) ) { foreach ( $this->pending as $key => $value ) { switch ( $key ) { case 'downloads': case 'fees': break; case 'status': $this->update_status( $this->status ); break; case 'gateway': edd_update_order( $this->ID, array( 'gateway' => $this->gateway, ) ); break; case 'mode': edd_update_order( $this->ID, array( 'mode' => $this->mode, ) ); break; case 'transaction_id': $this->update_meta( 'transaction_id', $this->transaction_id ); break; case 'customer_id': edd_update_order( $this->ID, array( 'customer_id' => $this->customer_id, ) ); $customer = new EDD_Customer( $this->customer_id ); $customer->attach_payment( $this->ID, false ); break; case 'user_id': edd_update_order( $this->ID, array( 'user_id' => $this->user_id, ) ); $this->user_info['id'] = $this->user_id; break; case 'first_name': $this->user_info['first_name'] = $this->first_name; break; case 'last_name': $this->user_info['last_name'] = $this->last_name; break; case 'discounts': if ( ! is_array( $this->discounts ) ) { $this->discounts = explode( ',', $this->discounts ); } $cart_subtotal = 0.00; foreach ( $this->cart_details as $item ) { $cart_subtotal += $item['subtotal']; } if ( 'none' === $this->discounts[0] ) { break; } foreach ( $this->discounts as $discount ) { /** @var EDD_Discount $discount_obj */ $discount_obj = edd_get_discount_by( 'code', $discount ); $args = array( 'object_id' => $this->ID, 'object_type' => 'order', 'description' => $discount, ); if ( false === $discount_obj ) { $args['type'] = 'fee'; $args['subtotal'] = floatval( $this->total - $cart_subtotal - $this->tax ); $args['total'] = floatval( $this->total - $cart_subtotal - $this->tax ); } else { $args['type_id'] = $discount_obj->id; $args['type'] = 'discount'; $args['subtotal'] = floatval( $cart_subtotal - $discount_obj->get_discounted_amount( $cart_subtotal ) ); $args['total'] = floatval( $cart_subtotal - $discount_obj->get_discounted_amount( $cart_subtotal ) ); } edd_add_order_adjustment( $args ); } $this->user_info['discount'] = implode( ',', $this->discounts ); break; case 'address': $this->user_info['address'] = $this->address; break; case 'email': $this->payment_meta['email'] = $this->email; $this->user_info['email'] = $this->email; edd_update_order( $this->ID, array( 'email' => $this->email, ) ); break; case 'key': edd_update_order( $this->ID, array( 'payment_key' => $this->key, ) ); break; case 'tax_rate': $tax_rate = $this->tax_rate > 1 ? $this->tax_rate : ( $this->tax_rate * 100 ); $this->update_meta( '_edd_payment_tax_rate', $tax_rate ); break; case 'number': edd_update_order( $this->ID, array( 'order_number' => $this->number, ) ); break; case 'date': edd_update_order( $this->ID, array( 'date_created' => $this->date, ) ); break; case 'completed_date': edd_update_order( $this->ID, array( 'date_completed' => $this->completed_date, ) ); break; case 'has_unlimited_downloads': $this->update_meta( 'unlimited_downloads', $this->has_unlimited_downloads ); break; case 'parent_payment': edd_update_order( $this->ID, array( 'parent' => $this->parent_payment, ) ); break; default: /** * Used to save non-standard data. Developers can hook here if they want to save * specific payment data when $payment->save() is run and their item is in the $pending array */ do_action( 'edd_payment_save', $this, $key ); break; } } $discount = 0.00; foreach ( $this->cart_details as $item ) { $discount += $item['discount']; } edd_update_order( $this->ID, array( 'subtotal' => (float) $this->subtotal, 'tax' => (float) $this->tax, 'discount' => $discount, 'total' => (float) $this->total, ) ); $this->downloads = array_values( $this->downloads ); $new_meta = array( 'downloads' => $this->downloads, 'cart_details' => $this->cart_details, 'fees' => $this->fees, 'user_info' => is_array( $this->user_info ) ? $this->user_info : array(), 'date' => $this->date, 'email' => $this->email, 'tax' => $this->tax, ); // Do some merging of user_info before we merge it all, to honor the edd_payment_meta filter if ( ! empty( $this->payment_meta['user_info'] ) ) { $stored_discount = ! empty( $new_meta['user_info']['discount'] ) ? $new_meta['user_info']['discount'] : ''; $new_meta['user_info'] = array_replace_recursive( (array) $this->payment_meta['user_info'], $new_meta['user_info'] ); if ( 'none' !== $stored_discount ) { $new_meta['user_info']['discount'] = $stored_discount; } } $merged_meta = array_merge( $this->payment_meta, $new_meta ); $payment_data = array( 'price' => $this->total, 'date' => $this->date, 'user_email' => $this->email, 'downloads' => $this->downloads, 'user_info' => array( 'id' => $this->user_id, 'email' => $this->email, 'first_name' => $this->first_name, 'last_name' => $this->last_name, 'discount' => $this->discounts, 'address' => $this->address, ), 'cart_details' => $this->cart_details, 'status' => $this->status, 'fees' => $this->fees, 'tax' => $this->tax, ); $merged_meta = apply_filters( 'edd_payment_meta', $merged_meta, $payment_data ); // Only save the payment meta if it's changed if ( md5( serialize( $this->payment_meta ) ) !== md5( serialize( $merged_meta ) ) ) { // First, update the order. $order_info = array( 'email' => $merged_meta['email'], ); if ( isset( $merged_meta['user_info']['id'] ) ) { $order_info['user_id'] = $merged_meta['user_info']['id']; } if ( ! empty( $merged_meta['date'] ) ) { $order_info['date'] = $merged_meta['date']; } edd_update_order( $this->ID, $order_info ); // We need to check if all of the order items exist in the database. $items = edd_get_order_items( array( 'order_id' => $this->ID, ) ); // If an empty set was returned, this is a new payment. if ( empty( $items ) ) { foreach ( $merged_meta['cart_details'] as $key => $item ) { edd_add_order_item( array( 'order_id' => $this->ID, 'product_id' => $item['id'], 'product_name' => $item['name'], 'price_id' => isset( $item['item_number']['options']['price_id'] ) && is_numeric( $item['item_number']['options']['price_id'] ) ? absint( $item['item_number']['options']['price_id'] ) : null, 'cart_index' => $key, 'quantity' => $item['quantity'], 'amount' => $item['item_price'], 'subtotal' => $item['subtotal'], 'discount' => $item['discount'], 'tax' => $item['tax'], 'total' => $item['price'], 'status' => ! empty( $item['status'] ) ? $item['status'] : $this->status, ) ); } } /** * Re-fetch the order with the new items from the database as it is used for the synchronization * between cart_details and the database. */ $this->order = $this->_shim_edd_get_order( $this->ID ); $updated = $this->update_meta( '_edd_payment_meta', $merged_meta ); if ( false !== $updated ) { $saved = true; } } $this->pending = array(); $saved = true; } if ( true === $saved ) { $this->setup_payment( $this->ID ); /** * This action fires anytime that $payment->save() is run, allowing developers to run actions * when a payment is updated */ do_action( 'edd_payment_saved', $this->ID, $this ); } $customer = new EDD_Customer( $this->customer_id ); $customer->recalculate_stats(); /** * Update the payment in the object cache */ $cache_key = md5( 'edd_payment' . $this->ID ); wp_cache_set( $cache_key, $this, 'payments' ); return $saved; } /** * Add a download to a given payment * * @since 2.5 * * @param int $download_id The download to add * @param array $args Other arguments to pass to the function * @param array $options List of download options * * @return bool True when successful, false otherwise */ public function add_download( $download_id = 0, $args = array(), $options = array() ) { $download = new EDD_Download( $download_id ); // Bail if this post isn't a download. if ( ! $download || 'download' !== $download->post_type ) { return false; } // Set up defaults. $defaults = array( 'quantity' => 1, 'price_id' => false, 'item_price' => false, 'discount' => 0, 'tax' => 0.00, 'fees' => array(), 'status' => $this->status, ); $args = wp_parse_args( apply_filters( 'edd_payment_add_download_args', $args, $download->ID ), $defaults ); // Allow overriding the price. if ( false !== $args['item_price'] ) { $item_price = $args['item_price']; } else { // Deal with variable pricing. if ( edd_has_variable_prices( $download->ID ) ) { $prices = get_post_meta( $download->ID, 'edd_variable_prices', true ); if ( $args['price_id'] && array_key_exists( $args['price_id'], (array) $prices ) ) { $item_price = $prices[ $args['price_id'] ]['amount']; } else { $item_price = edd_get_lowest_price_option( $download->ID ); $args['price_id'] = edd_get_lowest_price_id( $download->ID ); } } else { $item_price = edd_get_download_price( $download->ID ); } } // Sanitizing the price here so we don't have a dozen calls later $item_price = edd_sanitize_amount( $item_price ); $quantity = edd_item_quantities_enabled() ? absint( $args['quantity'] ) : 1; $amount = round( $item_price * $quantity, edd_currency_decimal_filter() ); // Setup the downloads meta item $new_download = array( 'id' => $download->ID, 'quantity' => $quantity, ); $default_options = array( 'quantity' => $quantity, ); if ( false !== $args['price_id'] ) { $default_options['price_id'] = (int) $args['price_id']; } $options = wp_parse_args( $options, $default_options ); $new_download['options'] = $options; $this->downloads[] = $new_download; $discount = $args['discount']; $subtotal = $amount; $tax = $args['tax']; if ( edd_prices_include_tax() ) { $subtotal -= round( $tax, edd_currency_decimal_filter() ); } $fees = 0; if ( ! empty( $args['fees'] ) && is_array( $args['fees'] ) ) { foreach ( $args['fees'] as $feekey => $fee ) { $fees += $fee['amount']; } $fees = round( $fees, edd_currency_decimal_filter() ); } $total = $subtotal - $discount + $tax + $fees; // Do not allow totals to go negative if ( $total < 0 ) { $total = 0; } // Silly item_number array $item_number = array( 'id' => $download->ID, 'quantity' => $quantity, 'options' => $options, ); $this->cart_details[] = array( 'name' => edd_get_download_name( $download->ID, $args['price_id'] ), 'id' => $download->ID, 'item_number' => $item_number, 'item_price' => round( $item_price, edd_currency_decimal_filter() ), 'quantity' => $quantity, 'discount' => $discount, 'subtotal' => round( $subtotal, edd_currency_decimal_filter() ), 'tax' => round( $tax, edd_currency_decimal_filter() ), 'fees' => $args['fees'], 'price' => round( $total, edd_currency_decimal_filter() ), ); $added_download = end( $this->cart_details ); $added_download['action'] = 'add'; // We need to add the cart index from 3.0+ as it gets stored in the database. $added_download['cart_index'] = key( $this->cart_details ); $this->pending['downloads'][] = $added_download; reset( $this->cart_details ); $this->increase_subtotal( $subtotal - $discount ); $this->increase_tax( $tax ); return true; } /** * Remove a download from the payment * * @since 2.5 * * @param int $download_id The download ID to remove * @param array $args Arguments to pass to identify (quantity, amount, price_id) * * @return bool If the item was removed or not */ public function remove_download( $download_id, $args = array() ) { // Set some defaults $defaults = array( 'quantity' => 1, 'item_price' => false, 'price_id' => false, 'cart_index' => false, ); $args = wp_parse_args( $args, $defaults ); $download = new EDD_Download( $download_id ); /** * Bail if this post isn't a download post type. * * We need to allow this to process though for a missing post ID, in case it's a download that was deleted. */ if ( ! empty( $download->ID ) && 'download' !== $download->post_type ) { return false; } foreach ( $this->downloads as $key => $item ) { if ( (int) $download_id !== (int) $item['id'] ) { continue; } if ( false !== $args['price_id'] ) { if ( isset( $item['options']['price_id'] ) && (int) $args['price_id'] !== (int) $item['options']['price_id'] ) { continue; } } elseif ( false !== $args['cart_index'] ) { $cart_index = absint( $args['cart_index'] ); $cart_item = ! empty( $this->cart_details[ $cart_index ] ) ? $this->cart_details[ $cart_index ] : false; if ( ! empty( $cart_item ) ) { // If the cart index item isn't the same download ID, don't remove it if ( $cart_item['id'] !== $item['id'] ) { continue; } // If this item has a price ID, make sure it matches the cart indexed item's price ID before removing if ( ( isset( $item['options']['price_id'] ) && isset( $cart_item['item_number']['options']['price_id'] ) ) && (int) $item['options']['price_id'] !== (int) $cart_item['item_number']['options']['price_id'] ) { continue; } } } $item_quantity = $this->downloads[ $key ]['quantity']; if ( $item_quantity > $args['quantity'] ) { $this->downloads[ $key ]['quantity'] -= $args['quantity']; break; } else { unset( $this->downloads[ $key ] ); break; } } $found_cart_key = false; if ( false === $args['cart_index'] ) { foreach ( $this->cart_details as $cart_key => $item ) { if ( (int) $download_id !== (int) $item['id'] ) { continue; } if ( false !== $args['price_id'] ) { if ( isset( $item['item_number']['options']['price_id'] ) && (int) $args['price_id'] !== (int) $item['item_number']['options']['price_id'] ) { continue; } } if ( false !== $args['item_price'] ) { if ( isset( $item['item_price'] ) && (float) $args['item_price'] !== (float) $item['item_price'] ) { continue; } } $found_cart_key = (int) $cart_key; break; } } else { $cart_index = absint( $args['cart_index'] ); if ( ! array_key_exists( $cart_index, $this->cart_details ) ) { return false; // Invalid cart index passed. } if ( (int) $this->cart_details[ $cart_index ]['id'] !== (int) $download_id ) { return false; // We still need the proper Download ID to be sure. } $found_cart_key = $cart_index; } $orig_quantity = $this->cart_details[ $found_cart_key ]['quantity']; if ( $orig_quantity > $args['quantity'] ) { $this->cart_details[ $found_cart_key ]['quantity'] -= $args['quantity']; $item_price = $this->cart_details[ $found_cart_key ]['item_price']; $tax = $this->cart_details[ $found_cart_key ]['tax']; $discount = ! empty( $this->cart_details[ $found_cart_key ]['discount'] ) ? $this->cart_details[ $found_cart_key ]['discount'] : 0; // The total reduction equals the number removed * the item_price $total_reduced = round( $item_price * $args['quantity'], edd_currency_decimal_filter() ); $tax_reduced = round( ( $tax / $orig_quantity ) * $args['quantity'], edd_currency_decimal_filter() ); $new_quantity = $this->cart_details[ $found_cart_key ]['quantity']; $new_tax = $this->cart_details[ $found_cart_key ]['tax'] - $tax_reduced; $new_subtotal = $new_quantity * $item_price; $new_discount = 0; $new_total = 0; $this->cart_details[ $found_cart_key ]['subtotal'] = $new_subtotal; $this->cart_details[ $found_cart_key ]['discount'] = $new_discount; $this->cart_details[ $found_cart_key ]['tax'] = $new_tax; $this->cart_details[ $found_cart_key ]['price'] = $new_subtotal - $new_discount + $new_tax; } else { $total_reduced = $this->cart_details[ $found_cart_key ]['item_price']; $tax_reduced = $this->cart_details[ $found_cart_key ]['tax']; $found_fees = array(); if ( ! empty( $this->cart_details[ $found_cart_key ]['fees'] ) ) { $found_fees = $this->cart_details[ $found_cart_key ]['fees']; foreach ( $found_fees as $key => $fee ) { $this->remove_fee( $key ); } } unset( $this->cart_details[ $found_cart_key ] ); } $pending_args = $args; $pending_args['id'] = $download_id; $pending_args['amount'] = $total_reduced; $pending_args['price_id'] = false !== $args['price_id'] ? $args['price_id'] : false; $pending_args['quantity'] = $args['quantity']; $pending_args['action'] = 'remove'; $pending_args['fees'] = isset( $found_fees ) ? $found_fees : array(); $pending_args['cart_index'] = $found_cart_key; $this->pending['downloads'][] = $pending_args; /** * Remove/modify the order item from the database at this point in lieu of having to synchronise with cart_details * later on in update_meta(). */ // Find the order item based on the cart index. $order_item = array_filter( $this->order->items, function ( $i ) use ( $found_cart_key ) { /** @var EDD\Orders\Order_Item $i */ return (int) $i->cart_index === (int) $found_cart_key; } ); // Reset array index. $order_item = array_values( $order_item ); $order_item = ( 1 === count( $order_item ) ) ? $order_item[0] : null; /** @var EDD\Orders\Order_Item $order_item */ // Ensure an order item exists in the database. if ( ! is_null( $order_item ) ) { // Update the order item if the quantity is being modified. if ( isset( $this->cart_details[ $found_cart_key ] ) ) { edd_update_order_item( $order_item->id, array( 'quantity' => $this->cart_details[ $found_cart_key ]['quantity'], 'amount' => $this->cart_details[ $found_cart_key ]['item_price'], 'subtotal' => $this->cart_details[ $found_cart_key ]['subtotal'], 'discount' => $this->cart_details[ $found_cart_key ]['discount'], 'tax' => $this->cart_details[ $found_cart_key ]['tax'], 'total' => $this->cart_details[ $found_cart_key ]['price'], ) ); // Remove the order item. } else { edd_delete_order_item( $order_item->id ); } } $this->decrease_subtotal( $total_reduced ); $this->decrease_tax( $tax_reduced ); return true; } /** * Alter a limited set of properties of a cart item * * @since 2.7 * * @param bool $cart_index * @param array $args * * @return bool */ public function modify_cart_item( $cart_index = false, $args = array() ) { if ( false === $cart_index ) { return false; } if ( ! array_key_exists( $cart_index, $this->cart_details ) ) { return false; } $current_args = $this->cart_details[ $cart_index ]; $allowed_items = apply_filters( 'edd_allowed_cart_item_modifications', array( 'item_price', 'tax', 'discount', 'quantity', ) ); // Remove any items we don't want to modify. foreach ( $args as $key => $arg ) { if ( ! in_array( $key, $allowed_items, true ) ) { unset( $args[ $key ] ); } } $merged_item = array_merge( $current_args, $args ); if ( md5( json_encode( $current_args ) ) === md5( json_encode( $merged_item ) ) ) { return false; } // Format the item_price correctly now $merged_item['item_price'] = edd_sanitize_amount( $merged_item['item_price'] ); $discount = isset( $merged_item['discount'] ) ? (float) $merged_item['discount'] : 0.00; $new_subtotal = floatval( $merged_item['item_price'] ) * $merged_item['quantity']; $merged_item['tax'] = edd_sanitize_amount( $merged_item['tax'] ); $merged_item['price'] = edd_prices_include_tax() ? $new_subtotal - $discount : $new_subtotal + $merged_item['tax'] - $discount; $this->cart_details[ $cart_index ] = $merged_item; // Sort the current and new args, and checksum them. If no changes. No need to fire a modification. ksort( $current_args ); ksort( $merged_item ); $modified_download = $merged_item; $modified_download['action'] = 'modify'; $modified_download['previous_data'] = $current_args; $this->pending['downloads'][] = $modified_download; if ( $new_subtotal > $current_args['subtotal'] ) { $this->increase_subtotal( ( $new_subtotal - (float) $modified_download['discount'] ) - (float) $current_args['subtotal'] ); } else { $this->decrease_subtotal( (float) $current_args['subtotal'] - ( $new_subtotal - (float) $modified_download['discount'] ) ); } if ( (float) $modified_download['tax'] > (float) $current_args['tax'] ) { $this->increase_tax( (float) $modified_download['tax'] - (float) $current_args['tax'] ); } else { $this->decrease_tax( (float) $current_args['tax'] - (float) $modified_download['tax'] ); } /** * Remove/modify the order item from the database at this point in lieu of having to synchronise with cart_details * later on in update_meta(). */ // Find the order item. $order_item_id = 0; foreach ( $this->order->items as $item ) { if ( (int) $item->cart_index === (int) $cart_index ) { $order_item_id = $item->id; break; } } if ( $order_item_id ) { edd_update_order_item( $order_item_id, array( 'quantity' => $modified_download['quantity'], 'amount' => (float) $modified_download['item_price'], 'subtotal' => (float) $new_subtotal, 'tax' => (float) $modified_download['tax'], 'discount' => (float) $modified_download['discount'], 'total' => (float) $modified_download['price'], ) ); } return true; } /** * Add a fee to a given payment. * * @since 2.5 * * @param array $args Array of arguments for the fee to add. * @param bool $global * * @return bool If the fee was added. */ public function add_fee( $args, $global = true ) { $default_args = array( 'label' => '', 'amount' => 0, 'type' => 'fee', 'id' => '', 'no_tax' => false, 'download_id' => 0, ); $fee = wp_parse_args( $args, $default_args ); $this->fees[] = $fee; $added_fee = $fee; $added_fee['action'] = 'add'; $this->pending['fees'][] = $added_fee; reset( $this->fees ); $this->increase_fees( $fee['amount'] ); return true; } /** * Remove a fee from the payment * * @since 2.5 * * @param int $key The array key index to remove * * @return bool If the fee was removed successfully */ public function remove_fee( $key ) { $removed = $this->remove_fee_by( 'index', $key ); return $removed; } /** * Remove a fee by the defined attributed * * @since 2.5 * * @param string $key The key to remove by * @param int|string $value The value to search for * @param boolean $global False - removes the first value it finds, True - removes all matches * * @return boolean If the item is removed. */ public function remove_fee_by( $key, $value, $global = false ) { $allowed_fee_keys = apply_filters( 'edd_payment_fee_keys', array( 'index', 'label', 'amount', 'type', ) ); if ( ! in_array( $key, $allowed_fee_keys, true ) ) { return false; } $removed = false; if ( 'index' === $key && array_key_exists( $value, $this->fees ) ) { $removed_fee = $this->fees[ $value ]; $removed_fee['action'] = 'remove'; $this->pending['fees'][] = $removed_fee; $this->decrease_fees( $removed_fee['amount'] ); unset( $this->fees[ $value ] ); $removed = true; } elseif ( 'index' !== $key ) { foreach ( $this->fees as $index => $fee ) { if ( isset( $fee[ $key ] ) && $fee[ $key ] === $value ) { $removed_fee = $fee; $removed_fee['action'] = 'remove'; $this->pending['fees'][] = $removed_fee; $this->decrease_fees( $removed_fee['amount'] ); unset( $this->fees[ $index ] ); $removed = true; if ( false === $global ) { break; } } } } /** * Remove the fee from the database at this point in lieu of having to synchronise with payment meta * later on in update_meta()/save(). */ if ( true === $removed ) { $fee = end( $this->pending['fees'] ); $fee_id = 'index' === $key ? $value : null; // Find by fee ID, if set. if ( ! is_null( $fee_id ) ) { foreach ( $this->order->get_fees() as $id => $f ) { if ( $id === $fee_id ) { edd_delete_order_adjustment( $f->id ); if ( false === $global ) { break; } } } // Find by fee label. } else { foreach ( $this->order->get_fees() as $f ) { if ( $fee['label'] === $f->description ) { edd_delete_order_adjustment( $f->id ); if ( false === $global ) { break; } } } } } return $removed; } /** * Get the fees, filterable by type. * * @since 2.5 * * @param string $type All, item, fee. * * @return array Fees for the type specified. */ public function get_fees( $type = 'all' ) { $fees = array(); if ( ! empty( $this->fees ) && is_array( $this->fees ) ) { foreach ( $this->fees as $fee_id => $fee ) { if ( 'all' !== $type && ! empty( $fee['type'] ) && $type !== $fee['type'] ) { continue; } $fee['id'] = $fee_id; $fees[] = $fee; } } return apply_filters( 'edd_get_payment_fees', $fees, $this->ID, $this ); } /** * Add a note to an order. * * @since 2.5 * @since 3.0 Return true if note was inserted successfully. * * @param string $note The note to add. * * @return bool Whether or not the note was inserted. */ public function add_note( $note = '' ) { // Bail if no note specified. if ( empty( $note ) ) { return false; } $note_id = edd_insert_payment_note( $this->ID, $note ); if ( $note_id ) { return true; } return false; } /** * Increase the payment's subtotal * * @since 2.5 * * @param float $amount The amount to increase the payment subtotal by * * @return void */ private function increase_subtotal( $amount = 0.00 ) { $amount = (float) $amount; $this->subtotal += $amount; $this->recalculate_total(); } /** * Decrease the payment's subtotal * * @since 2.5 * * @param float $amount The amount to decrease the payment subtotal by * * @return void */ private function decrease_subtotal( $amount = 0.00 ) { $amount = (float) $amount; $this->subtotal -= $amount; if ( $this->subtotal < 0 ) { $this->subtotal = 0; } $this->recalculate_total(); } /** * Increase the payment's subtotal * * @since 2.5 * * @param float $amount The amount to increase the payment subtotal by * * @return void */ private function increase_fees( $amount = 0.00 ) { $amount = (float) $amount; $this->fees_total += $amount; $this->recalculate_total(); } /** * Decrease the payment's subtotal * * @since 2.5 * * @param float $amount The amount to decrease the payment subtotal by * * @return void */ private function decrease_fees( $amount = 0.00 ) { $amount = (float) $amount; $this->fees_total -= $amount; if ( $this->fees_total < 0 ) { $this->fees_total = 0; } $this->recalculate_total(); } /** * Set or update the total for a payment * * @since 2.5 * @return void */ private function recalculate_total() { $this->total = $this->subtotal + $this->tax + $this->fees_total; } /** * Increase the payment's tax by the provided amount * * @since 2.5 * * @param float $amount The amount to increase the payment tax by * * @return void */ public function increase_tax( $amount = 0.00 ) { $amount = (float) $amount; $this->tax += $amount; $this->recalculate_total(); } /** * Decrease the payment's tax by the provided amount * * @since 2.5 * * @param float $amount The amount to reduce the payment tax by * * @return void */ public function decrease_tax( $amount = 0.00 ) { $amount = (float) $amount; $this->tax -= $amount; if ( $this->tax < 0 ) { $this->tax = 0; } $this->recalculate_total(); } /** * Change the status of an order to refunded, and run the necessary changes. * * @since 2.5.7 */ public function refund() { $this->old_status = $this->status; $this->status = 'refunded'; $this->pending['status'] = $this->status; $this->save(); } /** * Set the order status and run any status specific changes necessary. * * @since 2.5 * @since 3.0 Updated to work with the new refunds API and new query methods * introduced. * * @param string $status New order status. * @return bool True if the status was successfully updated, false otherwise. */ public function update_status( $status = '' ) { if ( ! $this->order ) { return false; } // Bail if an empty status is passed. if ( empty( $status ) || ! $status ) { return false; } // Override to `complete` since 3.0. if ( 'completed' === $status || 'publish' === $status ) { $status = 'complete'; } // Get the old (current) status. $old_status = ! empty( $this->old_status ) ? $this->old_status : false; // We do not allow status changes if the status is the same to that stored in the database. // This prevents the `edd_update_payment_status` action from being triggered unnecessarily. if ( $old_status === $status ) { return false; } $do_change = apply_filters( 'edd_should_update_payment_status', true, $this->ID, $status, $old_status ); $updated = false; if ( $do_change ) { do_action( 'edd_before_payment_status_change', $this->ID, $status, $old_status ); $update_fields = apply_filters( 'edd_update_payment_status_fields', array( 'status' => $status, ) ); // Account for someone filtering and using `post_status` if ( isset( $update_fields['post_status'] ) ) { _edd_generic_deprecated( 'EDD_Payment::update_status', '3.0', __( 'Array key "post_status" is no longer a supported attribute for the "edd_update_payment_status_fields" filter. Please use "status" instead.', 'easy-digital-downloads' ) ); $update_fields['status'] = $update_fields['post_status']; unset( $update_fields['post_status'] ); } // Strip data that does not need to be passed to `edd_update_order()`. unset( $update_fields['ID'] ); /** * As per the new refund API introduced in 3.0, the order is only * marked as refunded when `EDD_Payment::process_refund()` has called * `edd_refund_order()` and a new order has been generated with a * type of `refund`. * * @since 3.0 * @see EDD_Payment::process_refund() * @see edd_refund_order() * @see https://github.com/easydigitaldownloads/easy-digital-downloads/issues/2721 */ if ( 'refunded' !== $status ) { edd_update_order( $this->ID, $update_fields ); // Update each order item. foreach ( $this->order->items as $item ) { edd_update_order_item( $item->id, $update_fields ); } } /** * Albeit the order itself is not updated (for refunds), the EDD_Payment * class vars are updated for backwards compatibility purposes and * for anyone/anything that is checking that the status of the object * has successfully changed. */ $this->status = $status; $this->post_status = $status; $all_payment_statuses = edd_get_payment_statuses(); $this->status_nicename = array_key_exists( $status, $all_payment_statuses ) ? $all_payment_statuses[ $status ] : ucfirst( $status ); // Process any specific status functions. switch ( $status ) { case 'refunded': $this->process_refund(); do_action( 'edd_update_payment_status', $this->ID, $status, $old_status ); break; case 'failed': $this->process_failure(); break; case 'pending' || 'processing': $this->process_pending(); break; } } return $updated; } /** * Get a post meta item for the payment * * @since 2.5 * * @param string $meta_key The Meta Key * @param boolean $single Return single item or array * * @return mixed The value from the post meta */ public function get_meta( $meta_key = '_edd_payment_meta', $single = true ) { if ( $this->is_edd_payment ) { return get_post_meta( $this->ID, $meta_key, $single ); } $meta = edd_get_order_meta( $this->ID, $meta_key, $single ); // Backwards compatibility. switch ( $meta_key ) { case '_edd_payment_purchase_key': $meta = $this->order->payment_key; break; case '_edd_payment_transaction_id': $transactions = array_values( edd_get_order_transactions( array( 'number' => 1, 'object_id' => $this->ID, 'object_type' => 'order', 'orderby' => 'date_created', 'order' => 'ASC', 'fields' => 'transaction_id', ) ) ); $transaction_id = ''; if ( $transactions ) { $transaction_id = esc_attr( $transactions[0] ); } $meta = $transaction_id; break; case '_edd_payment_user_email': $meta = $this->order->email; break; case '_edd_completed_date': $meta = $this->completed_date; break; case '_edd_payment_gateway': $meta = $this->order->gateway; break; case '_edd_payment_user_id': $meta = $this->order->user_id; break; case '_edd_payment_user_ip': $meta = $this->order->ip; break; case '_edd_payment_mode': $meta = $this->order->mode; break; case '_edd_payment_tax_rate': $meta = $this->order->get_tax_rate(); break; case '_edd_payment_customer_id': $meta = $this->order->customer_id; break; case '_edd_payment_tax': $meta = $this->order->tax; break; case '_edd_payment_number': $meta = $this->order->get_number(); break; } if ( '_edd_payment_meta' === $meta_key ) { if ( empty( $meta ) ) { $meta = array(); } // Payment meta was simplified in EDD v1.5, so these are here for backwards compatibility if ( empty( $meta['key'] ) ) { $meta['key'] = $this->key; } if ( empty( $meta['email'] ) ) { $meta['email'] = $this->email; } if ( empty( $meta['date'] ) ) { $meta['date'] = $this->date; } // We need to back fill the returned meta for backwards compatibility purposes. $meta['key'] = $this->key; $meta['email'] = $this->email; $meta['date'] = $this->date; $meta['user_info'] = $this->user_info; $meta['downloads'] = $this->downloads; $meta['cart_details'] = $this->cart_details; $meta['fees'] = $this->fees; $meta['currency'] = $this->currency; $meta['tax'] = $this->tax; $migrated_payment_meta = edd_get_order_meta( $this->ID, 'payment_meta', true ); // This is no longer stored in _edd_payment_meta. $core_meta_keys = array( 'key', 'email', 'date', 'user_info', 'downloads', 'cart_details', 'quantity', 'discount', 'subtotal', 'tax', 'fees', 'currency' ); $migrated_payment_meta = array_diff_key( (array) $migrated_payment_meta, array_flip( $core_meta_keys ) ); if ( is_array( $migrated_payment_meta ) && 0 < count( $migrated_payment_meta ) ) { $meta = array_merge( $meta, $migrated_payment_meta ); } // #5228 Fix possible data issue introduced in 2.6.12 if ( is_array( $meta ) && isset( $meta[0] ) ) { $bad_meta = $meta[0]; unset( $meta[0] ); if ( is_array( $bad_meta ) ) { $meta = array_merge( $meta, $bad_meta ); } } } $meta = apply_filters( 'edd_get_payment_meta_' . $meta_key, $meta, $this->ID ); if ( is_serialized( $meta ) ) { preg_match( '/[oO]\s*:\s*\d+\s*:\s*"\s*(?!(?i)(stdClass))/', $meta, $matches ); if ( ! empty( $matches ) ) { $meta = array(); } } return apply_filters( 'edd_get_payment_meta', $meta, $this->ID, $meta_key ); } /** * Update the order meta. * * @since 2.5 * @since 3.0 Updated to use the new custom tables. * * @param string $meta_key The meta key to update. * @param string $meta_value The meta value. * @param string $prev_value Previous meta value. * * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure. */ public function update_meta( $meta_key = '', $meta_value = '', $prev_value = '' ) { if ( empty( $meta_key ) || empty( $this->ID ) ) { return false; } $meta_value = apply_filters( 'edd_update_payment_meta_' . $meta_key, $meta_value, $this->ID ); switch ( $meta_key ) { case '_edd_payment_meta': if ( isset( $meta_value['tax'] ) && ! empty( $meta_value['tax'] ) ) { edd_update_order( $this->ID, array( 'tax' => $meta_value['tax'], ) ); } if ( isset( $meta_value['key'] ) && ! empty( $meta_value['key'] ) ) { edd_update_order( $this->ID, array( 'key' => $meta_value['key'], ) ); } if ( isset( $meta_value['email'] ) && ! empty( $meta_value['email'] ) ) { edd_update_order( $this->ID, array( 'email' => $meta_value['email'], ) ); } if ( isset( $meta_value['currency'] ) && ! empty( $meta_value['currency'] ) ) { edd_update_order( $this->ID, array( 'currency' => $meta_value['currency'], ) ); } if ( isset( $meta_value['user_info'] ) && ! empty( $meta_value['user_info'] ) ) { // Handle discounts. $discounts = isset( $meta_value['user_info']['discount'] ) && ! empty( $meta_value['user_info']['discount'] ) ? $meta_value['user_info']['discount'] : array(); if ( ! is_array( $discounts ) ) { $discounts = explode( ',', $discounts ); } if ( ! empty( $discounts ) && ( 'none' !== $discounts[0] ) ) { foreach ( $discounts as $discount ) { /** @var EDD_Discount $discount */ $discount = edd_get_discount_by( 'code', $discount ); if ( false === $discount ) { continue; } $adjustments = $this->order->adjustments; $found_discount = array_filter( $adjustments, function( $adjustment ) use ( $discount ) { /** @var EDD\Orders\Order_Adjustment $adjustment */ return (string) $adjustment->description === (string) $discount->code; } ); // Discount exists so update the amount. if ( 1 === count( $found_discount ) ) { $found_discount = $found_discount[0]; /** @var EDD\Orders\Order_Adjustment $found_discount */ edd_update_order_adjustment( $found_discount->id, array( 'amount' => $this->subtotal - $discount->get_discounted_amount( $this->subtotal ), ) ); } else { // Add the discount as an adjustment. edd_add_order_adjustment( array( 'object_id' => $this->ID, 'object_type' => 'order', 'type_id' => $discount->id, 'type' => 'discount', 'description' => $discount->code, 'subtotal' => $this->subtotal - $discount->get_discounted_amount( $this->subtotal ), 'total' => $this->subtotal - $discount->get_discounted_amount( $this->subtotal ), ) ); } } } $user_info = array_diff_key( $meta_value['user_info'], array_flip( array( 'id', 'email', 'discount' ) ) ); $defaults = array( 'first_name' => '', 'last_name' => '', 'address' => array( 'line1' => '', 'line2' => '', 'city' => '', 'state' => '', 'country' => '', 'zip' => '', ), ); if ( isset( $user_info['address'] ) ) { $user_info['address'] = wp_parse_args( $user_info['address'], $defaults['address'] ); } $user_info = wp_parse_args( $user_info, $defaults ); $name = $user_info['first_name'] . ' ' . $user_info['last_name']; if ( null !== $this->order && $this->order->get_address()->id ) { $order_address = $this->order->get_address(); edd_update_order_address( $order_address->id, array( 'name' => $name, 'address' => $user_info['address']['line1'], 'address2' => $user_info['address']['line2'], 'city' => $user_info['address']['city'], 'region' => $user_info['address']['state'], 'postal_code' => $user_info['address']['zip'], 'country' => $user_info['address']['country'], ) ); } else { edd_add_order_address( array( 'order_id' => $this->ID, 'name' => $name, 'address' => $user_info['address']['line1'], 'address2' => $user_info['address']['line2'], 'city' => $user_info['address']['city'], 'region' => $user_info['address']['state'], 'postal_code' => $user_info['address']['zip'], 'country' => $user_info['address']['country'], ) ); } $remaining_user_info = array_diff_key( $meta_value['user_info'], array_flip( array( 'id', 'first_name', 'last_name', 'email', 'address', 'discount' ) ) ); if ( ! empty( $remaining_user_info ) ) { edd_update_order_meta( $this->ID, 'user_info', $remaining_user_info ); } } if ( isset( $meta_value['fees'] ) && ! empty( $meta_value['fees'] ) ) { foreach ( $meta_value['fees'] as $fee_id => $fee ) { if ( ! empty( $fee['download_id'] ) && 0 < $fee['download_id'] ) { $order_item_id = edd_get_order_items( array( 'number' => 1, 'order_id' => $this->ID, 'product_id' => $fee['download_id'], 'price_id' => isset( $fee['price_id'] ) && ! is_null( $fee['price_id'] ) ? intval( $fee['price_id'] ) : 0, 'fields' => 'ids', ) ); if ( is_array( $order_item_id ) ) { $order_item_id = (int) $order_item_id[0]; } $adjustment_id = edd_get_order_adjustments( array( 'number' => 1, 'object_id' => $order_item_id, 'object_type' => 'order_item', 'type' => 'fee', 'fields' => 'ids', 'type_key' => $fee_id, ) ); if ( is_array( $adjustment_id ) && ! empty( $adjustment_id ) ) { $adjustment_id = $adjustment_id[0]; edd_update_order_adjustment( $adjustment_id, array( 'description' => $fee['label'], 'subtotal' => (float) $fee['amount'], ) ); } else { add_filter( 'edd_prices_include_tax', '__return_false' ); $tax = ( isset( $fee['no_tax'] ) && false === $fee['no_tax'] ) || $fee['amount'] < 0 ? floatval( edd_calculate_tax( $fee['amount'] ) ) : 0.00; remove_filter( 'edd_prices_include_tax', '__return_false' ); $adjustment_id = edd_add_order_adjustment( array( 'object_id' => $order_item_id, 'object_type' => 'order_item', 'type_key' => $fee_id, 'type' => 'fee', 'description' => $fee['label'], 'subtotal' => floatval( $fee['amount'] ), 'tax' => $tax, 'total' => floatval( $fee['amount'] ) + $tax ) ); } } else { $adjustment_id = edd_get_order_adjustments( array( 'number' => 1, 'object_id' => $this->ID, 'object_type' => 'order', 'type' => 'fee', 'fields' => 'ids', 'type_key' => $fee_id, ) ); if ( is_array( $adjustment_id ) && ! empty( $adjustment_id ) ) { $adjustment_id = $adjustment_id[0]; edd_update_order_adjustment( $adjustment_id, array( 'description' => $fee['label'], 'subtotal' => (float) $fee['amount'], ) ); } else { add_filter( 'edd_prices_include_tax', '__return_false' ); $tax = ( isset( $fee['no_tax'] ) && false === $fee['no_tax'] ) || $fee['amount'] < 0 ? floatval( edd_calculate_tax( $fee['amount'] ) ) : 0.00; remove_filter( 'edd_prices_include_tax', '__return_false' ); $adjustment_id = edd_add_order_adjustment( array( 'object_id' => $this->ID, 'object_type' => 'order', 'type_key' => $fee_id, 'type' => 'fee', 'description' => $fee['label'], 'subtotal' => floatval( $fee['amount'] ), 'tax' => $tax, 'total' => floatval( $fee['amount'] ) + $tax ) ); } } } } /** * As of 3.0, the cart details array is no longer used for payments; it's purpose is for backwards compatibility * purposes only. Due to the way EDD_Payment, the cart_details array needs to be synchronized with the data * stored in the database as it could be different to the other class vars in the instance of EDD_Payment. */ if ( isset( $meta_value['cart_details'] ) && ! empty( $meta_value['cart_details'] ) ) { // Totals need to be updated based on cart details. $new_tax = 0.00; $new_subtotal = 0.00; foreach ( $meta_value['cart_details'] as $key => $item ) { $order_item_id = edd_get_order_items( array( 'number' => 1, 'fields' => 'ids', 'order_id' => $this->ID, 'product_id' => $item['id'], 'product_name' => $item['name'], ) ); $item['item_number']['options']['price_id'] = isset( $item['item_number']['options']['price_id'] ) && is_numeric( $item['item_number']['options']['price_id'] ) ? absint( $item['item_number']['options']['price_id'] ) : null; if ( is_array( $order_item_id ) && ! empty( $order_item_id ) ) { $order_item_id = $order_item_id[0]; edd_update_order_item( $order_item_id, array( 'order_id' => $this->ID, 'product_id' => $item['id'], 'product_name' => $item['name'], 'price_id' => $item['item_number']['options']['price_id'], 'cart_index' => $key, 'quantity' => $item['quantity'], 'amount' => $item['item_price'], 'subtotal' => $item['subtotal'], 'discount' => $item['discount'], 'tax' => $item['tax'], 'total' => $item['price'], ) ); $new_subtotal = $item['subtotal']; $new_tax += $item['tax']; } else { $order_item_id = edd_add_order_item( array( 'order_id' => $this->ID, 'product_id' => $item['id'], 'product_name' => $item['name'], 'price_id' => $item['item_number']['options']['price_id'], 'cart_index' => $key, 'quantity' => $item['quantity'], 'amount' => $item['item_price'], 'subtotal' => $item['subtotal'], 'discount' => $item['discount'], 'tax' => $item['tax'], 'total' => $item['price'], 'status' => ! empty( $item['status'] ) ? $item->status : $this->status, ) ); $new_tax += $item['tax']; $new_subtotal += $item['subtotal']; if ( isset( $item['fees'] ) && ! empty( $item['fees'] ) ) { foreach ( $item['fees'] as $fee_id => $fee ) { add_filter( 'edd_prices_include_tax', '__return_false' ); $tax = ( isset( $fee['no_tax'] ) && false === $fee['no_tax'] ) || $fee['amount'] < 0 ? floatval( edd_calculate_tax( $fee['amount'] ) ) : 0.00; remove_filter( 'edd_prices_include_tax', '__return_false' ); $adjustment_id = edd_add_order_adjustment( array( 'object_id' => $order_item_id, 'object_type' => 'order_item', 'type_key' => $fee_id, 'type' => 'fee', 'description' => $fee['label'], 'subtotal' => floatval( $fee['amount'] ), 'tax' => $tax, 'total' => floatval( $fee['amount'] ) + $tax, ) ); $new_tax += $tax; } } } } } // This is no longer stored in _edd_payment_meta. $core_meta_keys = array( 'key', 'email', 'date', 'user_info', 'downloads', 'cart_details', 'quantity', 'discount', 'subtotal', 'tax', 'fees', 'currency' ); $meta_value = array_diff_key( $meta_value, array_flip( $core_meta_keys ) ); // If the above checks fall through, store anything else in a "payment_meta" meta key. return edd_update_order_meta( $this->ID, 'payment_meta', $meta_value ); case '_edd_completed_date': $meta_value = empty( $meta_value ) ? null : $meta_value; edd_update_order( $this->ID, array( 'date_completed' => $meta_value, ) ); return true; case '_edd_payment_gateway': edd_update_order( $this->ID, array( 'gateway' => $meta_value, ) ); return true; case '_edd_payment_user_id': edd_update_order( $this->ID, array( 'user_id' => $meta_value, ) ); return true; case '_edd_payment_user_email': case 'email': edd_update_order( $this->ID, array( 'email' => $meta_value, ) ); return true; case '_edd_payment_user_ip': edd_update_order( $this->ID, array( 'ip' => $meta_value, ) ); return true; case '_edd_payment_purchase_key': case 'key': edd_update_order( $this->ID, array( 'payment_key' => $meta_value, ) ); return true; case '_edd_payment_mode': edd_update_order( $this->ID, array( 'mode' => $meta_value, ) ); return true; case '_edd_payment_tax_rate': $tax_rate = $meta_value > 0 ? $meta_value : ( $meta_value * 100 ); edd_update_order_meta( $this->ID, 'tax_rate', $tax_rate, $prev_value ); return true; case '_edd_payment_customer_id': edd_update_order( $this->ID, array( 'customer_id' => $meta_value, ) ); return true; case '_edd_payment_total': edd_update_order( $this->ID, array( 'total' => $meta_value, ) ); return true; case '_edd_payment_tax': edd_update_order( $this->ID, array( 'tax' => $meta_value, ) ); return true; case '_edd_payment_number': edd_update_order( $this->ID, array( 'order_number' => $meta_value, ) ); return true; case '_edd_payment_transaction_id': case 'transaction_id': $transaction_ids = array_values( edd_get_order_transactions( array( 'fields' => 'ids', 'number' => 1, 'object_id' => $this->ID, 'object_type' => 'order', 'orderby' => 'date_created', 'order' => 'ASC', ) ) ); if ( $transaction_ids ) { $transaction_id = $transaction_ids[0]; return edd_update_order_transaction( $transaction_id, array( 'transaction_id' => $meta_value, 'gateway' => $this->gateway, ) ); } else { return edd_add_order_transaction( array( 'object_id' => $this->ID, 'object_type' => 'order', 'transaction_id' => $meta_value, 'gateway' => $this->gateway, 'status' => 'complete', 'total' => $this->total, ) ); } } return edd_update_order_meta( $this->ID, $meta_key, $meta_value, $prev_value ); } /** * Add an item to the payment meta * * @since 2.8 * * @param string $meta_key * @param string $meta_value * @param bool $unique * * @return bool|false|int */ public function add_meta( $meta_key = '', $meta_value = '', $unique = false ) { if ( empty( $meta_key ) ) { return false; } return edd_add_order_meta( $this->ID, $meta_key, $meta_value, $unique ); } /** * Delete an item from payment meta * * @since 2.8 * * @param string $meta_key * @param string $meta_value * * @return bool */ public function delete_meta( $meta_key = '', $meta_value = '' ) { if ( empty( $meta_key ) ) { return false; } return edd_delete_order_meta( $this->ID, $meta_key, $meta_value ); } /** * Determines if this payment is able to be resumed by the user. * * @since 2.7 * * @return bool */ public function is_recoverable() { return $this->order->is_recoverable(); } /** * Returns the URL that a customer can use to resume a payment, or false if it's not recoverable. * * @since 2.7 * * @return bool|string */ public function get_recovery_url() { return $this->order->get_recovery_url(); } /** * When a payment is set to a status of 'refunded' process the necessary actions to reduce stats * * @since 2.5.7 * @access private */ private function process_refund() { $process_refund = true; // If the payment was not in publish or revoked status, don't decrement stats as they were never incremented if ( ( 'complete' !== $this->old_status && 'revoked' !== $this->old_status ) || 'refunded' !== $this->status ) { $process_refund = false; } // Allow extensions to filter for their own payment types, Example: Recurring Payments $process_refund = apply_filters( 'edd_should_process_refund', $process_refund, $this ); if ( false === $process_refund ) { return; } do_action( 'edd_pre_refund_payment', $this ); $decrease_store_earnings = apply_filters( 'edd_decrease_store_earnings_on_refund', true, $this ); $decrease_customer_value = apply_filters( 'edd_decrease_customer_value_on_refund', true, $this ); $decrease_purchase_count = apply_filters( 'edd_decrease_customer_purchase_count_on_refund', true, $this ); $this->maybe_alter_stats( $decrease_store_earnings, $decrease_customer_value, $decrease_purchase_count ); // Clear the This Month earnings (this_monththis_month is NOT a typo) delete_transient( md5( 'edd_earnings_this_monththis_month' ) ); do_action( 'edd_post_refund_payment', $this ); } /** * Process when a payment is set to failed, decrement discount usages and other stats. * * @since 2.5.7 * @access private */ private function process_failure() { $discounts = $this->discounts; if ( 'none' === $discounts || empty( $discounts ) ) { return; } if ( ! is_array( $discounts ) ) { $discounts = array_map( 'trim', explode( ',', $discounts ) ); } foreach ( $discounts as $discount ) { edd_decrease_discount_usage( $discount ); } } /** * Process when a payment moves to pending. * * @since 2.5.10 * @access private */ private function process_pending() { $process_pending = true; // If the payment was not in publish or revoked status, don't decrement stats as they were never incremented if ( ( 'complete' !== $this->old_status && 'revoked' !== $this->old_status ) || ! $this->in_process() ) { $process_pending = false; } // Allow extensions to filter for their own payment types, Example: Recurring Payments $process_pending = apply_filters( 'edd_should_process_pending', $process_pending, $this ); if ( false === $process_pending ) { return; } $decrease_store_earnings = apply_filters( 'edd_decrease_store_earnings_on_pending', true, $this ); $decrease_customer_value = apply_filters( 'edd_decrease_customer_value_on_pending', true, $this ); $decrease_purchase_count = apply_filters( 'edd_decrease_customer_purchase_count_on_pending', true, $this ); $this->maybe_alter_stats( $decrease_store_earnings, $decrease_customer_value, $decrease_purchase_count ); $this->completed_date = false; $this->update_meta( '_edd_completed_date', '' ); // Clear the This Month earnings (this_monththis_month is NOT a typo) delete_transient( md5( 'edd_earnings_this_monththis_month' ) ); } /** * Used during the process of moving to refunded or pending, to decrement stats * * @since 2.5.10 * @access private * * @param bool $alter_store_earnings If the method should alter the store earnings * @param bool $alter_customer_value If the method should reduce the customer value * @param bool $alter_customer_purchase_count If the method should reduce the customer's purchase count */ private function maybe_alter_stats( $alter_store_earnings, $alter_customer_value, $alter_customer_purchase_count ) { if ( edd_undo_purchase( false, $this->ID ) ) { $this->status = 'refunded'; $this->post_status = 'refunded'; $statuses = edd_get_payment_statuses(); $this->status_nicename = array_key_exists( 'refunded', $statuses ) ? $statuses['refunded'] : ucfirst( 'refunded' ); } // Decrease store earnings if ( true === $alter_store_earnings ) { edd_decrease_total_earnings( $this->total ); } // Decrement the stats for the customer if ( ! empty( $this->customer_id ) ) { $customer = new EDD_Customer( $this->customer_id ); if ( ! empty( $alter_customer_value || $alter_customer_purchase_count ) ) { $customer->recalculate_stats(); } } } /** * Delete sales logs for this purchase * * @since 2.5.10 * @deprecated Deprecated since 3.0 as sales logs are no longer used. */ private function delete_sales_logs() { _doing_it_wrong( __FUNCTION__, 'Sales logs are deprecated and are no longer used.', 'EDD 3.0' ); } /** * Setup functions only, these are not to be used by developers. * These functions exist only to allow the setup routine to be backwards compatible with our old * helper functions. * * These will run whenever setup_payment is called, which should only be called once. * To update an attribute, update it directly instead of re-running the setup routine */ /** * Setup the payment completed date. * * @since 2.5 * @since 3.0 Updated to use the new custom tables. * * @return string The date the payment was completed. */ private function setup_completed_date() { /** @var EDD\Orders\Order $order */ $order = $this->_shim_edd_get_order( $this->ID ); if ( 'pending' === $order->status || 'preapproved' === $order->status || 'processing' === $order->status ) { return false; // This payment was never completed } return $order->date_completed ? $order->date_completed : ''; } /** * Setup the payment total. * * @since 2.5 * * @return float Payment total. */ private function setup_total() { $amount = $this->get_meta( '_edd_payment_total', true ); if ( empty( $amount ) && '0.00' !== $amount ) { $meta = $this->get_meta( '_edd_payment_meta', true ); $meta = maybe_unserialize( $meta ); if ( isset( $meta['amount'] ) ) { $amount = $meta['amount']; } } return $amount; } /** * Setup the payment tax rate. * * @since 2.7 * * @return float Tax rate for the payment. */ private function setup_tax_rate() { $tax_rate = $this->order->get_tax_rate(); if ( ! empty( $tax_rate ) && $tax_rate > 1 ) { $tax_rate = $tax_rate / 100; } return $tax_rate; } /** * Setup the total fee amount applied to the payment. * * @since 2.5.10 * * @return float Total fee amount applied to the payment. */ private function setup_fees_total() { $fees_total = array_reduce( $this->fees, function( $carry, $item ) { $carry += (float) $item['amount']; return $carry; }, (float) 0.00 ); return $fees_total; } /** * Setup the payment subtotal. * * @since 2.5 * * @return float Payment subtotal. */ private function setup_subtotal() { $subtotal = 0; $cart_details = $this->cart_details; if ( is_array( $cart_details ) ) { foreach ( $cart_details as $item ) { if ( isset( $item['subtotal'] ) ) { $subtotal += $item['subtotal']; } } } else { $subtotal = $this->total; $tax = edd_use_taxes() ? $this->tax : 0; $subtotal -= $tax; } return $subtotal; } /** * Setup the payments discount codes. * * @since 2.5 * * @return string Discount codes on this payment. */ private function setup_discounts() { $discounts = array(); $order_discounts = $this->order->get_discounts(); foreach ( $order_discounts as $discount ) { $discounts[] = $discount->description; } $discounts = implode( ', ', $discounts ); return $discounts; } /** * Setup the currency code * * @since 2.5 * * @return string The currency for the payment. */ private function setup_currency() { $currency = $this->order->currency; return ! empty( $currency ) ? $currency : apply_filters( 'edd_payment_currency_default', edd_get_currency(), $this ); } /** * Setup any fees associated with the payment. * * @since 2.5 * @return array Payment fees. */ private function setup_fees() { $fees = array(); if ( $this->order->get_fees() ) { /* * Build up an array of order item IDs with values set to their respective download/price IDs. * This is so we can easily get that information when configuring order item fees. */ $order_items = array(); foreach ( $this->order->get_items() as $order_item ) { /** * @var \EDD\Orders\Order_Item $order_item */ $order_items[ intval( $order_item->id ) ] = array( 'download_id' => $order_item->product_id, 'price_id' => $order_item->price_id ); } foreach ( $this->order->get_fees() as $order_fee ) { /** * @var \EDD\Orders\Order_Adjustment $order_fee */ $download_id = 0; $price_id = null; if ( 'order_item' === $order_fee->object_type && array_key_exists( intval( $order_fee->object_id ), $order_items ) ) { $download_id = $order_items[ intval( $order_fee->object_id ) ]['download_id']; $price_id = $order_items[ intval( $order_fee->object_id ) ]['price_id']; } $no_tax = (bool) 0.00 === $order_fee->tax; $id = is_null( $order_fee->type_key ) ? $order_fee->id : $order_fee->type_key; if ( array_key_exists( $id, $fees ) ) { $id .= '_2'; } if ( $id != $order_fee->type_key ) { /* * We run an update here because if we don't, then we'll send back a key of `23_2` when in the * DB it's actually `null`, and if this value gets updated via the payment meta array, it * will actually add a brand *new* fee instead of updating the existing one. * * @link https://github.com/easydigitaldownloads/easy-digital-downloads/issues/8412 */ edd_update_order_adjustment( $order_fee->id, array( 'type_key' => $id ) ); } $fees[ $id ] = array( 'amount' => $order_fee->subtotal, 'label' => $order_fee->description, 'no_tax' => $no_tax, 'type' => 'fee', 'price_id' => $price_id, 'download_id' => $download_id, ); } } return $fees; } /** * Setup the transaction ID. * * @since 2.5 * * @return string The transaction ID for the payment. */ private function setup_transaction_id() { $transaction_id = $this->get_meta( '_edd_payment_transaction_id', true ); if ( empty( $transaction_id ) || (int) $transaction_id === (int) $this->ID ) { $gateway = $this->gateway; $transaction_id = apply_filters( 'edd_get_payment_transaction_id-' . $gateway, $this->ID ); } return $transaction_id; } /** * Setup the User ID associated with the purchase. * * @since 2.5 * * @return int User ID. */ private function setup_user_id() { $user_id = $this->get_meta( '_edd_payment_user_id', true ); $customer = new EDD_Customer( $this->customer_id ); // Make sure it exists, and that it matches that of the associated customer record if ( ! empty( $customer->user_id ) && ( empty( $user_id ) || (int) $user_id !== (int) $customer->user_id ) ) { $user_id = $customer->user_id; // Backfill the user ID, or reset it to be correct in the event of data corruption $this->update_meta( '_edd_payment_user_id', $user_id ); } return $user_id; } /** * Setup the email address for the purchase * * @since 2.5 * @return string The email address for the payment */ private function setup_email() { $email = $this->order->email; if ( empty( $email ) ) { $email = EDD()->customers->get_column( 'email', $this->customer_id ); } return $email; } /** * Setup the user info. * * @since 2.5 * * @return array The user info associated with the payment. */ private function setup_user_info() { $order_address = $this->order->get_address(); $user_info = array( 'id' => $this->user_id, 'first_name' => $order_address->first_name, 'last_name' => $order_address->last_name, 'discount' => $this->discounts, ); // Ensure email index is in the old user info array if ( empty( $user_info['email'] ) ) { $user_info['email'] = $this->email; } if ( empty( $user_info ) ) { // Get the customer, but only if it's been created $customer = new EDD_Customer( $this->customer_id ); if ( $customer->id > 0 ) { $name = explode( ' ', $customer->name, 2 ); $user_info = array( 'first_name' => $name[0], 'last_name' => $name[1], 'email' => $customer->email, 'discount' => 'none', ); } } else { // Get the customer, but only if it's been created $customer = new EDD_Customer( $this->customer_id ); if ( $customer->id > 0 ) { foreach ( $user_info as $key => $value ) { if ( ! empty( $value ) ) { continue; } switch ( $key ) { case 'first_name': $name = explode( ' ', $customer->name, 2 ); $user_info[ $key ] = $name[0]; break; case 'last_name': $name = explode( ' ', $customer->name, 2 ); $last_name = ! empty( $name[1] ) ? $name[1] : ''; $user_info[ $key ] = $last_name; break; case 'email': $user_info[ $key ] = $customer->email; break; } } } } $country = $order_address->country; // Add address to array if one exists. if ( ! empty( $country ) ) { $user_info['address'] = array( 'line1' => $order_address->address, 'line2' => $order_address->address2, 'city' => $order_address->city, 'state' => $order_address->region, 'country' => $country, 'zip' => $order_address->postal_code, ); } // Check for old `user_info` meta which may still exist. $old_meta = edd_get_order_meta( $this->ID, 'payment_meta', true ); if ( ! empty( $old_meta['user_info'] ) ) { $user_info = array_merge( $user_info, $old_meta['user_info'] ); } return $user_info; } /** * Setup the address for the payment. * * @since 2.5 * * @return array The address information for the payment. */ private function setup_address() { $address = ! empty( $this->user_info['address'] ) ? $this->user_info['address'] : array(); $defaults = array( 'line1' => '', 'line2' => '', 'city' => '', 'country' => '', 'state' => '', 'zip' => '' ); $address = wp_parse_args( $address, $defaults ); return $address; } /** * Setup the payment number. * * @since 2.5 * @since 3.0 Refactor to use EDD\Orders\Order. * * @return int|string Integer by default, or string if sequential order numbers is enabled. */ private function setup_payment_number() { return $this->order->order_number; } /** * Setup the cart details * * @since 2.5 * @since 3.0 Refactored as cart_details is no longer used and this is here for backwards compatibility purposes. * * @return array Cart details of an order. */ private function setup_cart_details() { $order_items = $this->order->items; $cart_details = array(); foreach ( $order_items as $item ) { /** @var EDD\Orders\Order_Item $item */ $item_fees = array(); foreach ( $item->fees as $key => $item_fee ) { /** @var EDD\Orders\Order_Adjustment $item_fee */ $download_id = $item->product_id; $price_id = $item->price_id; $no_tax = (bool) 0.00 === $item_fee->tax; $id = is_null( $item_fee->type_key ) ? $item_fee->id : $item_fee->type_key; if ( array_key_exists( $id, $item_fees ) ) { $id .= '_2'; } $item_fees[ $id ] = array( 'amount' => $item_fee->total, 'label' => $item_fee->description, 'no_tax' => $no_tax ? $no_tax : false, 'type' => 'fee', 'price_id' => $price_id ? $price_id : null, 'download_id' => 0, ); if ( $download_id ) { $item_fees[ $id ]['download_id'] = $download_id; } } $item_options = array( 'quantity' => $item->quantity, 'price_id' => $item->price_id, ); /* * For backwards compatibility from pre-3.0: add in order item meta prefixed with `_option_`. * While saving, we've migrated these values to order item meta, but people may still be looking * for them in this cart details array, so we need to fill them back in. */ $order_item_meta = edd_get_order_item_meta( $item->id ); if ( ! empty( $order_item_meta ) ) { foreach ( $order_item_meta as $item_meta_key => $item_meta_value ) { if ( '_option_' === substr( $item_meta_key, 0, 8 ) && isset( $item_meta_value[0] ) ) { $item_options[ str_replace( '_option_', '', $item_meta_key ) ] = $item_meta_value[0]; } } } $cart_details[ $item->cart_index ] = array( 'name' => $item->product_name, 'id' => $item->product_id, 'item_number' => array( 'id' => $item->product_id, 'quantity' => $item->quantity, 'options' => $item_options, ), 'item_price' => $item->amount, 'quantity' => $item->quantity, 'discount' => $item->discount, 'subtotal' => $item->subtotal, 'tax' => $item->tax, 'fees' => $item_fees, 'price' => $item->total, 'order_item_id' => $item->id, ); } return $cart_details; } /** * Setup the downloads array. * * @since 2.5 * * @internal This exists for backwards compatibility purposes. * * @return array Downloads associated with this payment. */ private function setup_downloads() { $order_items = $this->order->items; $downloads = array(); foreach ( $order_items as $item ) { /** @var EDD\Orders\Order_Item $item */ $downloads[] = array( 'id' => $item->product_id, 'quantity' => $item->quantity, 'options' => array( 'quantity' => $item->quantity, 'price_id' => $item->price_id, ) ); } return $downloads; } /** * Setup the Unlimited downloads setting * * @since 2.5 * @return bool If this payment has unlimited downloads */ private function setup_has_unlimited() { $unlimited = (bool) $this->order->has_unlimited_downloads(); return $unlimited; } /** * Converts this object into an array for special cases. * * @return array The payment object as an array. */ public function array_convert() { return get_object_vars( $this ); } /** * Retrieve payment cart details. * * @since 2.5.1 * * @return array Cart details array. */ private function get_cart_details() { return apply_filters( 'edd_payment_cart_details', $this->cart_details, $this->ID, $this ); } /** * Retrieve payment completion date * * @since 2.5.1 * @since 3.0 Updated for backwards compatibility. * * @return string Date payment was completed. */ private function get_completed_date() { if ( is_null( $this->completed_date ) ) { $date = false; } else { $date = $this->completed_date; } return apply_filters( 'edd_payment_completed_date', $date, $this->ID, $this ); } /** * Retrieve payment tax. * * @since 2.5.1 * * @return float Payment tax */ private function get_tax() { return apply_filters( 'edd_get_payment_tax', $this->tax, $this->ID, $this ); } /** * Retrieve payment subtotal. * * @since 2.5.1 * * @return float Payment subtotal. */ private function get_subtotal() { return apply_filters( 'edd_get_payment_subtotal', $this->subtotal, $this->ID, $this ); } /** * Retrieve payment discounts. * * @since 2.5.1 * * @return array Discount codes on payment. */ private function get_discounts() { return apply_filters( 'edd_payment_discounts', $this->discounts, $this->ID, $this ); } /** * Return the discounted amount of the payment. * * @since 2.8.7 * * @return float Discounted amount. */ private function get_discounted_amount() { return floatval( apply_filters( 'edd_payment_discounted_amount', $this->order->discount, $this ) ); } /** * Retrieve payment currency. * * @since 2.5.1 * * @return string Payment currency code. */ private function get_currency() { return apply_filters( 'edd_payment_currency_code', $this->currency, $this->ID, $this ); } /** * Retrieve payment gateway. * * @since 2.5.1 * * @return string Payment gateway used. */ private function get_gateway() { return apply_filters( 'edd_payment_gateway', $this->gateway, $this->ID, $this ); } /** * Retrieve payment transaction ID. * * @since 2.5.1 * * @return string Transaction ID from merchant processor. */ private function get_transaction_id() { return apply_filters( 'edd_get_payment_transaction_id', $this->transaction_id, $this->ID, $this ); } /** * Retrieve payment IP. * * @since 2.5.1 * * @return string Payment IP address. */ private function get_ip() { return apply_filters( 'edd_payment_user_ip', $this->ip, $this->ID, $this ); } /** * Retrieve payment customer ID. * * @since 2.5.1 * * @return int Payment customer ID. */ private function get_customer_id() { return apply_filters( 'edd_payment_customer_id', $this->customer_id, $this->ID, $this ); } /** * Retrieve payment user ID. * * @since 2.5.1 * * @return int Payment user ID. */ private function get_user_id() { return apply_filters( 'edd_payment_user_id', $this->user_id, $this->ID, $this ); } /** * Retrieve payment email. * * @since 2.5.1 * * @return string Payment customer email. */ private function get_email() { return apply_filters( 'edd_payment_user_email', $this->email, $this->ID, $this ); } /** * Retrieve payment user info. * * @since 2.5.1 * * @return array Payment user info. */ private function get_user_info() { return apply_filters( 'edd_payment_meta_user_info', $this->user_info, $this->ID, $this ); } /** * Retrieve payment billing address. * * @since 2.5.1 * * @return array Payment billing address. */ private function get_address() { return apply_filters( 'edd_payment_address', $this->address, $this->ID, $this ); } /** * Retrieve payment key. * * @since 2.5.1 * * @return string Payment key. */ private function get_key() { return apply_filters( 'edd_payment_key', $this->key, $this->ID, $this ); } /** * Retrieve payment number. * * @since 2.5.1 * * @return int|string Payment number. */ private function get_number() { return $this->order instanceof EDD\Orders\Order ? $this->order->get_number() : $this->ID; } /** * Retrieve downloads on payment. * * @since 2.5.1 * * @return array Payment downloads. */ private function get_downloads() { return apply_filters( 'edd_payment_meta_downloads', $this->downloads, $this->ID, $this ); } /** * Retrieve unlimited file downloads status. * * @since 2.5.1 * * @return bool True if unlimited downloads are enabled, false otherwise. */ private function get_unlimited() { return apply_filters( 'edd_payment_unlimited_downloads', $this->unlimited, $this->ID, $this ); } /** * Easily determine if the payment is in a status of pending some action. Processing is specifically used for * eChecks. * * @since 2.7 * @return bool */ private function in_process() { $in_process_statuses = array( 'pending', 'processing' ); return in_array( $this->status, $in_process_statuses, true ); } /** * Determines if a customer needs to be created given the current payment details. * * @since 2.8.4 * * @return EDD_Customer The customer object of the existing customer or new customer. */ private function maybe_create_customer() { $customer = new stdClass(); if ( did_action( 'edd_pre_process_purchase' ) && is_user_logged_in() ) { $customer = new EDD_customer( get_current_user_id(), true ); // Customer is logged in but used a different email to purchase with so assign to their customer record if ( ! empty( $customer->id ) && $this->email !== $customer->email ) { $customer->add_email( $this->email ); } } if ( empty( $customer->id ) ) { $customer = new EDD_Customer( $this->email ); } if ( empty( $customer->id ) ) { if ( empty( $this->first_name ) && empty( $this->last_name ) ) { $name = $this->email; } else { $name = $this->first_name . ' ' . $this->last_name; } $customer_data = array( 'name' => $name, 'email' => $this->email, 'user_id' => $this->user_id, ); $customer->create( $customer_data ); } return $customer; } /** * Sets up a payment object from a post. * This is only intended to be used when a 3.0 migration is in process and the * new order object is not yet available. * * @todo deprecate in 3.1 * * @since 3.0 * @param int $payment_id * @return bool */ private function _setup_compat_payment( $payment_id ) { $payment = get_post( $payment_id ); if ( ! $payment || is_wp_error( $payment ) ) { return false; } if ( 'edd_payment' !== $payment->post_type ) { return false; } // Set the compatibility property to true. $this->is_edd_payment = true; // Allow extensions to perform actions before the payment is loaded do_action( 'edd_pre_setup_payment', $this, $payment_id ); // Primary Identifier $this->ID = absint( $payment_id ); // Protected ID that can never be changed $this->_ID = absint( $payment_id ); include_once EDD_PLUGIN_DIR . 'includes/compat/class-edd-payment-compat.php'; $payment_compat = new EDD_Payment_Compat( $this->ID ); // We have a payment; get the generic payment_meta item to reduce calls to it $this->payment_meta = $payment_compat->payment_meta; // Status and Dates $this->date = $payment->post_date; $this->completed_date = $payment_compat->completed_date; $this->status = $payment_compat->status; $this->post_status = $this->status; $this->mode = $payment_compat->mode; $this->parent_payment = $payment->post_parent; $all_payment_statuses = edd_get_payment_statuses(); $this->status_nicename = array_key_exists( $this->status, $all_payment_statuses ) ? $all_payment_statuses[ $this->status ] : ucfirst( $this->status ); // Items $this->fees = $payment_compat->fees; $this->cart_details = $payment_compat->cart_details; $this->downloads = $payment_compat->downloads; // Currency Based $this->total = $payment_compat->total; $this->tax = $payment_compat->tax; $this->tax_rate = $payment_compat->tax_rate; $this->fees_total = $payment_compat->fees_total; $this->subtotal = $payment_compat->subtotal; $this->currency = $payment_compat->currency; // Gateway based $this->gateway = $payment_compat->gateway; $this->transaction_id = $payment_compat->transaction_id; // User based $this->ip = $payment_compat->ip; $this->customer_id = $payment_compat->customer_id; $this->user_id = $payment_compat->user_id; $this->email = $payment_compat->email; $this->user_info = $payment_compat->user_info; $this->address = $payment_compat->address; $this->discounts = $this->user_info['discount']; $this->first_name = $this->user_info['first_name']; $this->last_name = $this->user_info['last_name']; // Other Identifiers $this->key = $payment_compat->key; $this->number = $payment_compat->number; // Additional Attributes $this->has_unlimited_downloads = $payment_compat->has_unlimited_downloads; $this->order = $payment_compat->order; // Allow extensions to add items to this object via hook do_action( 'edd_setup_payment', $this, $payment_id ); return true; } /** * Gets the order from the database. * This is a duplicate of edd_get_order, but is defined separately here * for pending migration purposes. * * @todo deprecate in 3.1 * * @param int $order_id * @return false|EDD\Orders\Order */ private function _shim_edd_get_order( $order_id ) { $orders = new EDD\Database\Queries\Order(); // Return order return $orders->get_item( $order_id ); } }