order_type = 'shop_subscription'; $this->schedule = new stdClass(); } /** * Populates a subscription from the loaded post data. * * @param mixed $result */ public function populate( $result ) { parent::populate( $result ); if ( $this->post->post_parent > 0 ) { $this->order = wc_get_order( $this->post->post_parent ); } } /** * Returns the unique ID for this object. * * @return int */ public function get_id() { return $this->id; } /** * Get parent order ID. * * @since 2.2.0 * @return int */ public function get_parent_id() { return $this->post->post_parent; } /** * Gets order currency. * * @return string */ public function get_currency() { return $this->get_order_currency(); } /** * Get customer_note. * * @param string $context * @return string */ public function get_customer_note( $context = 'view' ) { return $this->customer_note; } /** * Get prices_include_tax. * * @param string $context * @return bool */ public function get_prices_include_tax( $context = 'view' ) { return $this->prices_include_tax; } /** * Get the payment method. * * @param string $context * @return string */ public function get_payment_method( $context = 'view' ) { return $this->payment_method; } /** * Get the payment method's title. * * @param string $context * @return string */ public function get_payment_method_title( $context = 'view' ) { return $this->payment_method_title; } /** Address Getters **/ /** * Get billing_first_name. * * @param string $context * @return string */ public function get_billing_first_name( $context = 'view' ) { return $this->billing_first_name; } /** * Get billing_last_name. * * @param string $context * @return string */ public function get_billing_last_name( $context = 'view' ) { return $this->billing_last_name; } /** * Get billing_company. * * @param string $context * @return string */ public function get_billing_company( $context = 'view' ) { return $this->billing_company; } /** * Get billing_address_1. * * @param string $context * @return string */ public function get_billing_address_1( $context = 'view' ) { return $this->billing_address_1; } /** * Get billing_address_2. * * @param string $context * @return string $value */ public function get_billing_address_2( $context = 'view' ) { return $this->billing_address_2; } /** * Get billing_city. * * @param string $context * @return string $value */ public function get_billing_city( $context = 'view' ) { return $this->billing_city; } /** * Get billing_state. * * @param string $context * @return string */ public function get_billing_state( $context = 'view' ) { return $this->billing_state; } /** * Get billing_postcode. * * @param string $context * @return string */ public function get_billing_postcode( $context = 'view' ) { return $this->billing_postcode; } /** * Get billing_country. * * @param string $context * @return string */ public function get_billing_country( $context = 'view' ) { return $this->billing_country; } /** * Get billing_email. * * @param string $context * @return string */ public function get_billing_email( $context = 'view' ) { return $this->billing_email; } /** * Get billing_phone. * * @param string $context * @return string */ public function get_billing_phone( $context = 'view' ) { return $this->billing_phone; } /** * Get shipping_first_name. * * @param string $context * @return string */ public function get_shipping_first_name( $context = 'view' ) { return $this->shipping_first_name; } /** * Get shipping_last_name. * * @param string $context * @return string */ public function get_shipping_last_name( $context = 'view' ) { return $this->shipping_last_name; } /** * Get shipping_company. * * @param string $context * @return string */ public function get_shipping_company( $context = 'view' ) { return $this->shipping_company; } /** * Get shipping_address_1. * * @param string $context * @return string */ public function get_shipping_address_1( $context = 'view' ) { return $this->shipping_address_1; } /** * Get shipping_address_2. * * @param string $context * @return string */ public function get_shipping_address_2( $context = 'view' ) { return $this->shipping_address_2; } /** * Get shipping_city. * * @param string $context * @return string */ public function get_shipping_city( $context = 'view' ) { return $this->shipping_city; } /** * Get shipping_state. * * @param string $context * @return string */ public function get_shipping_state( $context = 'view' ) { return $this->shipping_state; } /** * Get shipping_postcode. * * @param string $context * @return string */ public function get_shipping_postcode( $context = 'view' ) { return $this->shipping_postcode; } /** * Get shipping_country. * * @param string $context * @return string */ public function get_shipping_country( $context = 'view' ) { return $this->shipping_country; } /** * Get order key. * * @since 2.2.0 * @param string $context * @return string */ public function get_order_key( $context = 'view' ) { return $this->order_key; } /** * Get date_created. * * Used by parent::get_date() * * @throws WC_Data_Exception * @return DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_created( $context = 'view' ) { if ( '0000-00-00 00:00:00' != $this->post->post_date_gmt ) { $datetime = new WC_DateTime( $this->post->post_date_gmt, new DateTimeZone( 'UTC' ) ); $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) ); } else { $datetime = new WC_DateTime( $this->post->post_date, new DateTimeZone( wc_timezone_string() ) ); } // Cache it in $this->schedule for backward compatibility if ( ! isset( $this->schedule->start ) ) { $this->schedule->start = wcs_get_datetime_utc_string( $datetime ); } return $datetime; } /** * Get date_modified. * * Used by parent::get_date() * * @throws WC_Data_Exception * @return DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_modified( $context = 'view' ) { if ( '0000-00-00 00:00:00' != $this->post->post_modified_gmt ) { $datetime = new WC_DateTime( $this->post->post_modified_gmt, new DateTimeZone( 'UTC' ) ); $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) ); } else { $datetime = new WC_DateTime( $this->post->post_modified, new DateTimeZone( wc_timezone_string() ) ); } return $datetime; } /** * Check if a given line item on the subscription had a sign-up fee, and if so, return the value of the sign-up fee. * * The single quantity sign-up fee will be returned instead of the total sign-up fee paid. For example, if 3 x a product * with a 10 BTC sign-up fee was purchased, a total 30 BTC was paid as the sign-up fee but this function will return 10 BTC. * * @param array|int Either an order item (in the array format returned by self::get_items()) or the ID of an order item. * @param string $tax_inclusive_or_exclusive Whether or not to adjust sign up fee if prices inc tax - ensures that the sign up fee paid amount includes the paid tax if inc * @return bool * @since 2.0 */ public function get_items_sign_up_fee( $line_item, $tax_inclusive_or_exclusive = 'exclusive_of_tax' ) { if ( ! is_array( $line_item ) ) { $line_item = wcs_get_order_item( $line_item, $this ); } $parent_order = $this->get_parent(); // If there was no original order, nothing was paid up-front which means no sign-up fee if ( false == $parent_order ) { $sign_up_fee = 0; } else { $original_order_item = ''; // Find the matching item on the order foreach ( $parent_order->get_items() as $order_item ) { if ( wcs_get_canonical_product_id( $line_item ) == wcs_get_canonical_product_id( $order_item ) ) { $original_order_item = $order_item; break; } } // No matching order item, so this item wasn't purchased in the original order if ( empty( $original_order_item ) ) { $sign_up_fee = 0; } elseif ( isset( $line_item['item_meta']['_has_trial'] ) ) { // Sign up is total amount paid for this item on original order when item has a free trial $sign_up_fee = $original_order_item['line_total'] / $original_order_item['qty']; } else { // Sign-up fee is any amount on top of recurring amount $sign_up_fee = max( $original_order_item['line_total'] / $original_order_item['qty'] - $line_item['line_total'] / $line_item['qty'], 0 ); } // If prices inc tax, ensure that the sign up fee amount includes the tax if ( 'inclusive_of_tax' === $tax_inclusive_or_exclusive && ! empty( $original_order_item ) && $this->get_prices_include_tax() ) { $proportion = $sign_up_fee / ( $original_order_item['line_total'] / $original_order_item['qty'] ); $sign_up_fee += round( $original_order_item['line_tax'] * $proportion, 2 ); } } return apply_filters( 'woocommerce_subscription_items_sign_up_fee', $sign_up_fee, $line_item, $this, $tax_inclusive_or_exclusive ); } /** * Helper function to make sure when WC_Subscription calls get_prop() from * it's new getters that the property is both retreived from the legacy class * property and done so from post meta. * * For inherited dates props, like date_created, date_modified, date_paid, * date_completed, we want to use our own get_date() function rather simply * getting the stored value. Otherwise, we either get the prop set in memory * or post meta if it's not set yet, because __get() in WC < 3.0 would fallback * to post meta. * * @param string * @param string * @return mixed */ protected function get_prop( $prop, $context = 'view' ) { if ( 'switch_data' == $prop ) { $prop = 'subscription_switch_data'; } // The requires manual renewal prop uses boolean values but is stored as a string so needs special handling, it also needs to be handled before the checks on $this->$prop to avoid triggering __isset() & __get() magic methods for $this->requires_manual_renewal if ( 'requires_manual_renewal' === $prop ) { $value = get_post_meta( $this->get_id(), '_' . $prop, true ); if ( 'false' === $value || '' === $value ) { $value = false; } else { $value = true; } } elseif ( ! isset( $this->$prop ) || empty( $this->$prop ) ) { $value = get_post_meta( $this->get_id(), '_' . $prop, true ); } else { $value = $this->$prop; } return $value; } /** * Get the stored date for a specific schedule. * * @param string $date_type 'date_created', 'trial_end', 'next_payment', 'last_order_date_created' or 'end' */ protected function get_date_prop( $date_type ) { $datetime = parent::get_date_prop( $date_type ); // Cache the string equalivent of it in $this->schedule for backward compatibility if ( ! isset( $this->schedule->{$date_type} ) ) { if ( ! is_object( $datetime ) ) { $this->schedule->{$date_type} = 0; } else { $this->schedule->{$date_type} = wcs_get_datetime_utc_string( $datetime ); } } return wcs_get_datetime_from( wcs_date_to_time( $datetime ) ); } /*** Setters *****************************************************/ /** * Set the unique ID for this object. * * @param int */ public function set_id( $id ) { $this->id = absint( $id ); } /** * Set parent order ID. We don't use WC_Abstract_Order::set_parent_id() because we want to allow false * parent IDs, like 0. * * @since 2.2.0 * @param int $value */ public function set_parent_id( $value ) { // Update the parent in the database wp_update_post( array( 'ID' => $this->id, 'post_parent' => $value, ) ); // And update the parent in memory $this->post->post_parent = $value; $this->order = null; } /** * Set subscription status. * * @param string $new_status Status to change the order to. No internal wc- prefix is required. * @return array details of change */ public function set_status( $new_status, $note = '', $manual_update = false ) { $old_status = $this->get_status(); $prefix = substr( $new_status, 0, 3 ); $new_status = 'wc-' === $prefix ? substr( $new_status, 3 ) : $new_status; wp_update_post( array( 'ID' => $this->get_id(), 'post_status' => wcs_maybe_prefix_key( $new_status, 'wc-' ) ) ); $this->post_status = $this->post->post_status = wcs_maybe_prefix_key( $new_status, 'wc-' ); if ( $old_status !== $new_status ) { $this->status_transition = array( 'from' => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $old_status, 'to' => $new_status, 'note' => $note, 'manual' => (bool) $manual_update, ); } return array( 'from' => $old_status, 'to' => $new_status, ); } /** * Helper function to make sure when WC_Subscription calls set_prop() that property is * both set in the legacy class property and saved in post meta immediately. * * @param string $prop * @param mixed $value */ protected function set_prop( $prop, $value ) { if ( 'switch_data' == $prop ) { $prop = 'subscription_switch_data'; } $this->$prop = $value; // The requires manual renewal prop uses boolean values but it stored as a string if ( 'requires_manual_renewal' === $prop ) { if ( false === $value || '' === $value ) { $value = 'false'; } else { $value = 'true'; } } update_post_meta( $this->get_id(), '_' . $prop, $value ); } /** * Set the stored date for a specific schedule. * * @param string $date_type 'trial_end', 'next_payment', 'cancelled', 'payment_retry' or 'end' * @param int $value UTC timestamp */ protected function set_date_prop( $date_type, $value ) { $datetime = wcs_get_datetime_from( $value ); $date = ! is_null( $datetime ) ? wcs_get_datetime_utc_string( $datetime ) : 0; $this->set_prop( $this->get_date_prop_key( $date_type ), $date ); $this->schedule->{$date_type} = $date; } /** * Set a certain date type for the last order on the subscription. * * @since 2.2.0 * @param string $date_type * @param string|integer|object * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ protected function set_last_order_date( $date_type, $date = null ) { $last_order = $this->get_last_order( 'all' ); if ( $last_order ) { $datetime = wcs_get_datetime_from( $date ); switch ( $date_type ) { case 'date_paid' : update_post_meta( $last_order->id, '_paid_date', ! is_null( $date ) ? $datetime->date( 'Y-m-d H:i:s' ) : '' ); // Preemptively set the UTC timestamp for WC 3.0+ also to avoid incorrect values when the site's timezone is changed between now and upgrading to WC 3.0 update_post_meta( $last_order->id, '_date_paid', ! is_null( $date ) ? $datetime->getTimestamp() : '' ); break; case 'date_completed' : update_post_meta( $last_order->id, '_completed_date', ! is_null( $date ) ? $datetime->date( 'Y-m-d H:i:s' ) : '' ); // Preemptively set the UTC timestamp for WC 3.0+ also to avoid incorrect values when the site's timezone is changed between now and upgrading to WC 3.0 update_post_meta( $last_order->id, '_date_completed', ! is_null( $date ) ? $datetime->getTimestamp() : '' ); break; case 'date_modified' : wp_update_post( array( 'ID' => $last_order->id, 'post_modified' => $datetime->date( 'Y-m-d H:i:s' ), 'post_modified_gmt' => wcs_get_datetime_utc_string( $datetime ), ) ); break; case 'date_created' : wp_update_post( array( 'ID' => $last_order->id, 'post_date' => $datetime->date( 'Y-m-d H:i:s' ), 'post_date_gmt' => wcs_get_datetime_utc_string( $datetime ), ) ); break; } } } /** * Set date_created. * * Used by parent::update_dates() * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. * @throws WC_Data_Exception */ public function set_date_created( $date = null ) { global $wpdb; if ( ! is_null( $date ) ) { $datetime_string = wcs_get_datetime_utc_string( wcs_get_datetime_from( $date ) ); // Don't use wp_update_post() to avoid infinite loops here $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_date = %s, post_date_gmt = %s WHERE ID = %d", get_date_from_gmt( $datetime_string ), $datetime_string, $this->get_id() ) ); $this->post->post_date = get_date_from_gmt( $datetime_string ); $this->post->post_date_gmt = $datetime_string; } } /** * Set discount_total. * * @param string $value * @throws WC_Data_Exception */ public function set_discount_total( $value ) { $this->set_total( $value, 'cart_discount' ); } /** * Set discount_tax. * * @param string $value * @throws WC_Data_Exception */ public function set_discount_tax( $value ) { $this->set_total( $value, 'cart_discount_tax' ); } /** * Set shipping_total. * * @param string $value * @throws WC_Data_Exception */ public function set_shipping_total( $value ) { $this->set_total( $value, 'shipping' ); } /** * Set shipping_tax. * * @param string $value * @throws WC_Data_Exception */ public function set_shipping_tax( $value ) { $this->set_total( $value, 'shipping_tax' ); } /** * Set cart tax. * * @param string $value * @throws WC_Data_Exception */ public function set_cart_tax( $value ) { $this->set_total( $value, 'tax' ); } /** * Save data to the database. Nothing to do here as it's all done separately when calling @see this->set_prop(). * * @return int order ID */ public function save() { $this->status_transition(); return $this->get_id(); } /** * Update meta data by key or ID, if provided. * * @since 2.2.0 * @param string $key * @param string $value * @param int $meta_id */ public function update_meta_data( $key, $value, $meta_id = '' ) { if ( ! empty( $meta_id ) ) { update_metadata_by_mid( 'post', $meta_id, $value, $key ); } else { update_post_meta( $this->get_id(), $key, $value ); } } /** * Save subscription date changes to the database. * Nothing to do here as all date properties are saved when calling @see $this->set_prop(). * * @since 2.2.6 */ public function save_dates() { // Nothing to do here. } }