<?php

use RCP\Membership_Level;

/**
 * RCP Payments class
 *
 * This class handles querying, inserting, updating, and removing payments
 * Also handles calculating earnings
 *
 * @package     Kadence Memberships Pro
 * @subpackage  Classes/Payments
 * @copyright   Copyright (c) 2017, Kadence Memberships Pro
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.5
 */

class RCP_Payments {

	/**
	 * Holds the name of our payments database table
	 *
	 * @access  public
	 * @since   1.5
	 */
	public $db_name;

	/**
	 * Holds the name of our payment meta database table
	 *
	 * @access  public
	 * @since   2.6
	 */
	public $meta_db_name;

	/**
	 * Holds the version number of our discounts database table
	 *
	 * @access  public
	 * @since   1.5
	 */
	public $db_version;

	/**
	 * Get things going.
	 *
	 * @return void
	 */
	function __construct() {

		$this->db_name      = rcp_get_payments_db_name();
		$this->meta_db_name = rcp_get_payment_meta_db_name();
		$this->db_version   = '1.5';

	}


	/**
	 * Add a payment to the database
	 *
	 * @access  public
	 * @param   array $payment_data Array All of the payment data, such as amount, date, user ID, etc
	 * @since   1.5
	 * @return  int|false ID of the newly created payment, or false on failure.
	 */
	public function insert( $payment_data = array() ) {

		global $wpdb;

		$defaults = array(
			'subscription'          => '',
			'object_id'             => 0,
			'object_type'           => 'subscription',
			'date'                  => date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ),
			'amount'                => 0.00, // Total amount after fees/credits/discounts are added.
			'user_id'               => 0,
			'customer_id'           => 0,
			'membership_id'         => 0,
			'payment_type'          => '',
			'transaction_type'      => 'new',
			'subscription_key'      => '',
			'transaction_id'        => '',
			'status'                => 'complete',
			'gateway'               => '',
			'subtotal'              => 0.00, // Base price of the membership level.
			'credits'               => 0.00, // Proration credits.
			'fees'                  => 0.00, // Fees.
			'discount_amount'       => 0.00, // Discount amount from discount code.
			'discount_code'         => ''
		);

		$args = wp_parse_args( $payment_data, $defaults );

		if( ! empty( $args['transaction_id'] ) && $this->payment_exists( $args['transaction_id'] ) ) {
			return false;
		}

		// Backwards compatibility: make sure we store the subscription ID as well.
		if ( empty( $args['object_id'] ) && ! empty( $args['subscription'] ) ) {
			$membership_level = rcp_get_membership_level_by( 'name', $args['subscription'] );

			if ( $membership_level instanceof Membership_Level ) {
				$args['object_id'] = $membership_level->get_id();
			}
		}

		// Backwards compatibility: include customer ID.
		if ( empty( $args['customer_id'] ) && ! empty( $args['user_id'] ) ) {
			$customer = rcp_get_customer_by_user_id( $args['user_id'] );

			if ( ! empty( $customer ) ) {
				$args['customer_id'] = $customer->get_id();
			}
		}

		// Backwards compatibility: update pending payment instead of creating a new one.
		if ( ! empty( $args['user_id'] ) && 'complete' == $args['status'] ) {
			$last_pending_payment = get_user_meta( $args['user_id'], 'rcp_pending_payment_id', true );
			$pending_payment      = ! empty( $last_pending_payment ) ? $this->get_payment( $last_pending_payment ) : false;

			if ( ! empty( $pending_payment ) && $args['amount'] == $pending_payment->amount && $args['subscription'] == $pending_payment->subscription ) {
				$this->update( $pending_payment->id, $args );

				return $pending_payment->id;
			}
		}

		$add = $wpdb->insert( $this->db_name, $args, array( '%s', '%d', '%s', '%s', '%s', '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) );

		// if insert was successful, return the payment ID
		if( $add ) {

			$payment_id = $wpdb->insert_id;

			// clear the payment caches
			delete_transient( 'rcp_earnings' );
			delete_transient( 'rcp_payments_count' );

			// Remove trialing status, if it exists
			delete_user_meta( $args['user_id'], 'rcp_is_trialing' );

			/**
			 * Triggers when the payment's status is changed. This is here to spoof a status
			 * change when a payment is first inserted.
			 *
			 * @see RCP_Payment::update() - Action is also run here when status is changed.
			 *
			 * @param string $new_status New status being set.
			 * @param int    $payment_id ID of the payment.
			 *
			 * @since 2.9
			 */
			do_action( 'rcp_update_payment_status', $args['status'], $payment_id );
			do_action( 'rcp_update_payment_status_' . $args['status'], $payment_id );

			if ( 'complete' == $args['status'] ) {
				/**
				 * Runs only when a new payment is inserted as "complete". This is to
				 * ensure backwards compatibility from before payments were inserted
				 * as "pending" before payment is taken.
				 *
				 * @deprecated 2.9 - Use rcp_create_payment to run actions whenever a payment is
				 *             inserted, regardless of status.
				 *
				 * @see RCP_Payment::update() - Action is also run here when status is updated to complete.
				 *
				 * @param int   $payment_id ID of the payment that was just inserted.
				 * @param array $args       Array of all payment information.
				 * @param float $amount     Amount the payment was for.
				 */
				do_action( 'rcp_insert_payment', $payment_id, $args, $args['amount'] );
			}

			/**
			 * Runs when a new payment is successfully inserted.
			 *
			 * @param int   $payment_id ID of the payment that was just inserted.
			 * @param array $args       Array of all payment information.
			 *
			 * @since 2.9
			 */
			do_action( 'rcp_create_payment', $payment_id, $args );

			rcp_log( sprintf( 'New payment inserted. ID: %d; User ID: %d; Amount: %.2f; Subscription: %s; Status: %s', $payment_id, $args['user_id'], $args['amount'], $args['subscription'], $args['status'] ) );

			return $payment_id;

		} else {
			rcp_log( 'Failed inserting new payment into database.', true );
		}

		return false;

	}


	/**
	 * Checks if a payment exists in the DB
	 *
	 * @param   string $transaction_id The transaction ID of the payment record.
	 *
	 * @access  public
	 * @since   1.5
	 * @return  bool
	 */
	public function payment_exists( $transaction_id = '' ) {

		global $wpdb;

		$found = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM " . $this->db_name . " WHERE `transaction_id`='%s' LIMIT 1;",
				$transaction_id
			)
		);

		return (bool) $found;

	}


	/**
	 * Update a payment in the datbase.
	 *
	 * @param   int   $payment_id   ID of the payment record to update.
	 * @param   array $payment_data Array of all payment data to update.
	 *
	 * @access  public
	 * @since   1.5
	 * @return  int|false The number of rows updated, or false on error.
	 */
	public function update( $payment_id = 0, $payment_data = array() ) {

		global $wpdb;

		do_action( 'rcp_update_payment', $payment_id, $payment_data );

		rcp_log( sprintf( 'Updating payment #%d with new data: %s', $payment_id, var_export( $payment_data, true ) ) );

		if ( array_key_exists( 'status', $payment_data ) ) {
			delete_transient( md5( 'rcp_payments_count_' . serialize( array( 'user_id' => 0, 'status' => '', 's' => '' ) ) ) );
			delete_transient( md5( 'rcp_payments_count_' . serialize( array( 'user_id' => 0, 'status' => 'pending', 's' => '' ) ) ) );
			delete_transient( md5( 'rcp_payments_count_' . serialize( array( 'user_id' => 0, 'status' => 'complete', 's' => '' ) ) ) );
			delete_transient( md5( 'rcp_payments_count_' . serialize( array( 'user_id' => 0, 'status' => 'refunded', 's' => '' ) ) ) );
		}

		$updated = $wpdb->update( $this->db_name, $payment_data, array( 'id' => $payment_id ) );

		if ( $updated && array_key_exists( 'status', $payment_data ) ) {

			$payment = $this->get_payment( $payment_id );

			/**
			 * Triggers when the payment's status is changed.
			 *
			 * @param string $new_status   New status being set.
			 * @param int    $payment_id   ID of the payment.
			 *
			 * @since 2.9
			 */
			do_action( 'rcp_update_payment_status', $payment_data['status'], $payment_id );
			do_action( 'rcp_update_payment_status_' . $payment_data['status'], $payment_id );

			if ( 'complete' == $payment_data['status'] ) {
				$amount  = ! empty( $payment->amount ) ? $payment->amount : 0.00;

				/**
				 * Runs only when a payment is updated to "complete". This is to
				 * ensure backwards compatibility from before payments were inserted
				 * as "pending" before payment is taken.
				 *
				 * @deprecated 2.9 - Use rcp_create_payment to run actions whenever a payment is
				 *             inserted, regardless of status.
				 *
				 * @see RCP_Payments::insert() - Action is also run here.
				 *
				 * @param int   $payment_id ID of the payment that was just updated.
				 * @param array $args       Array of payment information that was just updated.
				 * @param float $amount     Amount the payment was for.
				 */
				do_action( 'rcp_insert_payment', $payment_id, (array) $payment, $amount );
			}

		}

		return $updated;
	}


	/**
	 * Delete a payment from the datbase.
	 *
	 * @param   int $payment_id ID of the payment to delete.
	 *
	 * @access  public
	 * @since   1.5
	 * @return  void
	*/
	public function delete( $payment_id = 0 ) {

		global $wpdb;

		do_action( 'rcp_delete_payment', $payment_id );

		rcp_log( sprintf( 'Deleted payment #%d.', $payment_id ) );

		$wpdb->query( $wpdb->prepare( "DELETE FROM {$this->db_name} WHERE `id` = '%d';", absint( $payment_id ) ) );

	}


	/**
	 * Retrieve a specific payment
	 *
	 * @param   int $payment_id ID of the payment to retrieve.
	 *
	 * @access  public
	 * @since   1.5
	 * @return  object|null Database object or null if payment not found.
	 */
	public function get_payment( $payment_id = 0 ) {

		global $wpdb;

		$payment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->db_name} WHERE id = %d", absint( $payment_id ) ) );

		if ( is_object( $payment ) ) {
			if ( empty( $payment->status ) ) {
				$payment->status = 'complete';
			}

			$payment = $this->backfill_payment_data( $payment );
		}

		return $payment;

	}

	/**
	 * Attempt to guess the payment gateway from the "Payment Type"
	 *
	 * @param int|object $_payment_id_or_object Payment ID or database object.
	 *
	 * @access private
	 * @since 2.9
	 * @return string|false
	 */
	private function get_payment_gateway( $_payment_id_or_object ) {

		if ( is_object( $_payment_id_or_object ) ) {
			$payment = $_payment_id_or_object;
		} elseif ( is_numeric( $_payment_id_or_object ) ) {
			$payment = $this->get_payment( $_payment_id_or_object );
		}

		if ( empty( $payment ) ) {
			return false;
		}

		$type    = strtolower( $payment->payment_type );
		$gateway = false;

		// If we already have a gateway in the DB, use that.
		if ( ! empty( $payment->gateway ) ) {
			return $payment->gateway;
		}

		// If "Payment Type" isn't set, we can't get the gateway.
		if ( empty( $type ) ) {
			return $gateway;
		}

		switch ( $type ) {

			case 'web_accept' :
			case 'paypal express one time' :
			case 'recurring_payment' :
			case 'subscr_payment' :
			case 'recurring_payment_profile_created' :
				$gateway = 'paypal';
				break;

			case 'credit card' :
			case 'credit card one time' :
				if ( false !== strpos( $payment->transaction_id, 'ch_' ) ) {
					$gateway = 'stripe';
				} elseif ( false !== strpos( $payment->transaction_id, 'anet_' ) ) {
					$gateway = 'authorizenet';
				} elseif ( is_numeric( $payment->transaction_id ) ) {
					$gateway = 'twocheckout';
				}
				break;

			case 'braintree credit card one time' :
			case 'braintree credit card initial payment' :
			case 'braintree credit card' :
				$gateway = 'braintree';
				break;

			case 'manual' :
				$gateway = 'manual';
				break;

		}

		return $gateway;

	}


	/**
	 * Retrieve a specific payment by a field
	 *
	 * @param   string $field Name of the field to check against.
	 * @param   mixed  $value Value of the field.
	 *
	 * @access  public
	 * @since   1.8.2
	 * @return  object
	 */
	public function get_payment_by( $field = 'id', $value = '' ) {

		global $wpdb;

		$query   = $wpdb->prepare( "SELECT * FROM {$this->db_name} WHERE {$field} = %s", sanitize_text_field( $value ) );
		$payment = $wpdb->get_row( $query );

		if( is_object( $payment ) && empty( $payment->status ) ) {
			$payment->status = 'complete';
		}

		return $payment;

	}


	/**
	 * Retrieve payments from the database
	 *
	 * @param   array $args Query arguments to override the defaults.
	 *
	 * @access  public
	 * @since   1.5
	 * @return  array Array of objects.
	 */
	public function get_payments( $args = array() ) {

		global $wpdb;

		$defaults = array(
			'number'           => 20,
			'offset'           => 0,
			'subscription'     => 0,
			'user_id'          => 0,
			'customer_id'      => 0,
			'membership_id'    => 0,
			'date'             => array(),
			'fields'           => false,
			'status'           => '',
			's'                => '',
			'order'            => 'DESC',
			'orderby'          => 'id',
			'object_type'      => '',
			'object_id'        => '',
			'transaction_type' => '',
			'gateway'          => ''
		);

		$args  = wp_parse_args( $args, $defaults );

		$where  = ' WHERE 1=1 ';
		$values = array();

		// payments for a specific membership level
		if( ! empty( $args['subscription'] ) ) {
			$where   .= " AND `subscription`= %s ";
			$values[] = $args['subscription'];
		}

		// payments for specific users
		if( ! empty( $args['user_id'] ) ) {

			if( is_array( $args['user_id'] ) ) {
				$user_ids = implode( ',', array_map( 'absint', $args['user_id'] ) );
			} else {
				$user_ids = intval( $args['user_id'] );
			}

			$where .= " AND `user_id` IN( {$user_ids} ) ";

		}

		// payments for specific customers
		if( ! empty( $args['customer_id'] ) ) {

			if( is_array( $args['customer_id'] ) ) {
				$customer_ids = implode( ',', array_map( 'absint', $args['customer_id'] ) );
			} else {
				$customer_ids = intval( $args['customer_id'] );
			}

			$where .= " AND `customer_id` IN( {$customer_ids} ) ";

		}

		// payments for specific memberships
		if( ! empty( $args['membership_id'] ) ) {

			if( is_array( $args['membership_id'] ) ) {
				$membership_ids = implode( ',', array_map( 'absint', $args['membership_id'] ) );
			} else {
				$membership_ids = intval( $args['membership_id'] );
			}

			$where .= " AND `membership_id` IN( {$membership_ids} ) ";

		}

		// payments for specific statuses
		if( ! empty( $args['status'] ) ) {

			if( is_array( $args['status'] ) ) {
				$status_count       = count( $args['status'] );
				$status_placeholder = array_fill( 0, $status_count, '%s' );
				$statuses           = implode( ', ', $status_placeholder );

				$where .= "AND `status` IN ( $statuses )";
				$values = $values + $args['status'];
			} else {
				$where   .= "AND `status` = %s";
				$values[] = $args['status'];
			}

		}

		// Setup the date query
		if( ! empty( $args['date'] ) && is_array( $args['date'] ) ) {

			if ( ! empty( $args['date']['start'] ) || ! empty( $args['date']['end'] ) ) {

				if ( ! empty( $args['date']['start'] ) ) {

					$start    = date( 'Y-m-d 00:00:00', strtotime( $args['date']['start'] ) );
					$where   .= " AND `date` >= %s";
					$values[] = $start;

				}

				if ( ! empty( $args['date']['end'] ) ) {

					$end    = date( 'Y-m-d 23:59:59', strtotime( $args['date']['end'] ) );
					$where   .= " AND `date` <= %s";
					$values[] = $end;

				}

			} else {

				$day        = ! empty( $args['date']['day'] ) ? absint( $args['date']['day'] ) : null;
				$month      = ! empty( $args['date']['month'] ) ? absint( $args['date']['month'] ) : null;
				$year       = ! empty( $args['date']['year'] ) ? absint( $args['date']['year'] ) : null;
				$date_where = '';

				$date_where .= ! is_null( $year ) ? $year . " = YEAR ( date ) " : '';

				if ( ! is_null( $month ) ) {
					$date_where = $month . " = MONTH ( date ) AND " . $date_where;
				}

				if ( ! is_null( $day ) ) {
					$date_where = $day . " = DAY ( date ) AND " . $date_where;
				}

				$where .= " AND (" . $date_where . ")";

			}
		}

		// Fields to return
		if( $args['fields'] ) {
			$fields = $args['fields'];
		} else {
			$fields = '*';
		}

		// Search
		if( ! empty( $args['s'] ) ) {

			// Search by email
			if( is_email( $args['s'] ) ) {

				$user = get_user_by( 'email', $args['s'] );

				if ( is_a( $user, 'WP_User' ) ) {
					$where   .= " AND `user_id` = %d";
					$values[] = $user->ID;
				}

			} else {

				// Search by subscription key
				if( strlen( $args['s'] ) == 32 ) {

					$where   .= " AND `subscription_key` = %s";
					$values[] = $args['s'];

				} elseif( rcp_get_membership_level_by( 'name', $args['s'] ) ) {

					// Matching membership level found so search for payments with this level
					$where   .= " AND `subscription` = %s";
					$values[] = $args['s'];
				} else {
					$where   .= " AND `transaction_id` = %s";
					$values[] = $args['s'];
				}
			}

		}

		if ( ! empty( $args['object_type'] ) ) {
			$where   .= " AND `object_type` = %s";
			$values[] = $args['object_type'];
		}

		if ( ! empty( $args['object_id'] ) ) {
			$where   .= " AND `object_id` = %d AND `object_id` != 0";
			$values[] = $args['object_id'];
		}

		if ( ! empty( $args['transaction_type'] ) ) {
			$where   .= " AND `transaction_type` = %s";
			$values[] = $args['transaction_type'];
		}

		if ( ! empty( $args['gateway'] ) ) {
			$where   .= " AND `gateway` = %s";
			$values[] = $args['gateway'];
		}

		if ( 'DESC' === strtoupper( $args['order'] ) ) {
			$order = 'DESC';
		} else {
			$order = 'ASC';
		}

		$columns = array(
			'id',
			'user_id',
			'subscription',
			'subscription_key',
			'transaction_id',
			'status',
			'date'
		);

		$orderby = array_key_exists( $args['orderby'], $columns ) ? $args['orderby'] : 'id';

		$values[] = absint( $args['offset'] );
		$values[] = absint( $args['number'] );

		$payments = $wpdb->get_results( $wpdb->prepare( "SELECT {$fields} FROM " . $this->db_name . " {$where} ORDER BY {$orderby} {$order} LIMIT %d,%d;", $values ) );

		foreach ( $payments as $key => $payment ) {

			if ( '*' === $fields ) {
				$payment = $this->backfill_payment_data( $payment );
			}

			$payments[ $key ] = $payment;

		}

		return $payments;

	}

	/**
	 * Backfills any missing payment data introduced in RCP 2.9+
	 * and updates the payment record accordingly.
	 *
	 * @access private
	 * @since 2.9
	 *
	 * @param stdClass $payment The payment object.
	 *
	 * @return stdClass The updated payment object.
	 */
	private function backfill_payment_data( $payment ) {

		$data_to_update = array();

		/** Backfill the membership level ID. */
		if ( empty( $payment->object_id ) && ! empty( $payment->subscription ) ) {

			$membership_level = rcp_get_membership_level_by( 'name', $payment->subscription );

			if ( $membership_level instanceof Membership_Level ) {
				$payment->object_id = $membership_level->get_id();
				$data_to_update['object_id'] = $membership_level->get_id();
			}

		}

		/** Backfill the gateway */
		if ( empty( $payment->gateway ) && ! empty( $payment->payment_type ) ) {

			$gateway = $this->get_payment_gateway( $payment );

			if ( ! empty( $gateway ) ) {
				$payment->gateway = $gateway;
				$data_to_update['gateway'] = $gateway;
			}

		}

		/** Backfill empty subtotal */
		if ( ! property_exists( $payment, 'subtotal' ) || '' === $payment->subtotal || null === $payment->subtotal ) {
			$payment->subtotal          = $payment->amount;
			$data_to_update['subtotal'] = $payment->amount;
		}

		/** Backfill empty credits */
		if ( ! property_exists( $payment, 'credits' ) || '' === $payment->credits || null === $payment->credits ) {
			$payment->credits          = 0;
			$data_to_update['credits'] = 0;
		}

		/** Backfill empty fees */
		if ( ! property_exists( $payment, 'fees' ) || '' === $payment->fees || null === $payment->fees ) {
			$payment->fees          = 0;
			$data_to_update['fees'] = 0;
		}

		/** Backfill empty discount_amount */
		if ( ! property_exists( $payment, 'discount_amount' ) || '' === $payment->discount_amount || null === $payment->discount_amount ) {
			$payment->discount_amount          = 0;
			$data_to_update['discount_amount'] = 0;
		}

		/** Backfill customer ID */
		if ( ! empty( $payment->user_id ) && empty( $payment->customer_id ) ) {
			$customer = rcp_get_customer_by_user_id( $payment->user_id );

			if ( ! empty( $customer ) ) {
				$data_to_update['customer_id'] = $customer->get_id();
			}
		}

		if ( ! empty( $data_to_update ) ) {
			$this->update( $payment->id, $data_to_update );
		}

		return $payment;
	}

	/**
	 * Count the total number of payments in the database
	 *
	 * @param   array $args Query arguments to override the defaults.
	 *
	 * @access  public
	 * @since   1.5
	 * @return  int
	 */
	public function count( $args = array() ) {

		global $wpdb;

		$defaults = array(
			'user_id'          => 0,
			'date'             => array(),
			'status'           => '',
			's'                => '',
			'object_id'        => '',
			'object_type'      => '',
			'transaction_type' => '',
			'gateway'          => ''
		);

		$args  = wp_parse_args( $args, $defaults );

		$where  = ' WHERE 1=1 ';
		$values = array();

		// Filter by user ID
		if( ! empty( $args['user_id'] ) ) {

			if( is_array( $args['user_id'] ) ) {
				$user_ids = implode( ',', array_map( 'absint', $args['user_id'] ) );
			} else {
				$user_ids = intval( $args['user_id'] );
			}

			$where .= " AND `user_id` IN( {$user_ids} ) ";

		}

		// Setup the date query
		if( ! empty( $args['date'] ) && is_array( $args['date'] ) ) {

			if ( ! empty( $args['date']['start'] ) || ! empty( $args['date']['end'] ) ) {

				if ( ! empty( $args['date']['start'] ) ) {

					$start    = date( 'Y-m-d 00:00:00', strtotime( $args['date']['start'] ) );
					$where   .= " AND `date` >= %s";
					$values[] = $start;

				}

				if ( ! empty( $args['date']['end'] ) ) {

					$end    = date( 'Y-m-d 23:59:59', strtotime( $args['date']['end'] ) );
					$where   .= " AND `date` <= %s";
					$values[] = $end;

				}

			} else {

				$day        = ! empty( $args['date']['day'] ) ? absint( $args['date']['day'] ) : null;
				$month      = ! empty( $args['date']['month'] ) ? absint( $args['date']['month'] ) : null;
				$year       = ! empty( $args['date']['year'] ) ? absint( $args['date']['year'] ) : null;
				$date_where = '';

				$date_where .= ! is_null( $year ) ? $year . " = YEAR ( date ) " : '';

				if ( ! is_null( $month ) ) {
					$date_where = $month . " = MONTH ( date ) AND " . $date_where;
				}

				if ( ! is_null( $day ) ) {
					$date_where = $day . " = DAY ( date ) AND " . $date_where;
				}

				$where .= " AND (" . $date_where . ")";

			}
		}

		// Filter by status
		if( ! empty( $args['status'] ) ) {

			if( is_array( $args['status'] ) ) {
				$status_count       = count( $args['status'] );
				$status_placeholder = array_fill( 0, $status_count, '%s' );
				$statuses           = implode( ', ', $status_placeholder );

				$where .= "AND `status` IN ( $statuses )";
				$values = $values + $args['status'];
			} else {
				$where   .= "AND `status` = %s";
				$values[] = $args['status'];
			}

		}

		// Search
		if( ! empty( $args['s'] ) ) {

			// Search by email
			if( is_email( $args['s'] ) ) {

				$user = get_user_by( 'email', $args['s'] );

				if ( is_a( $user, 'WP_User' ) ) {
					$where   .= " AND `user_id` = %d";
					$values[] = $user->ID;
				}

			} else {

				// Search by subscription key
				if( strlen( $args['s'] ) == 32 ) {

					$where   .= " AND `subscription_key` = %s";
					$values[] = $args['s'];

				} elseif( rcp_get_membership_level_by( 'name', $args['s'] ) ) {

					// Matching membership level found so search for payments with this level
					$where   .= " AND `subscription` = %s";
					$values[] = $args['s'];
				} else {
					$where   .= " AND `transaction_id` = %s";
					$values[] = $args['s'];
				}
			}

		}

		// Object type
		if ( ! empty( $args['object_type'] ) ) {
			$where   .= " AND `object_type` = %s";
			$values[] = $args['object_type'];
		}

		// Object ID
		if ( ! empty( $args['object_id'] ) ) {
			$where   .= " AND `object_id` = %d AND `object_id` != 0";
			$values[] = $args['object_id'];
		}

		// Transaction type (new, renewal, upgrade, downgrade)
		if ( ! empty( $args['transaction_type'] ) ) {
			$where   .= " AND `transaction_type` = %s";
			$values[] = $args['transaction_type'];
		}

		// Gateway
		if ( ! empty( $args['gateway'] ) ) {
			$where   .= " AND `gateway` = %s";
			$values[] = $args['gateway'];
		}

		$key   = md5( 'rcp_payments_count_' . serialize( $args ) );
		$count = get_transient( $key );

		if( $count === false ) {

			$query = "SELECT COUNT(ID) FROM " . $this->db_name . "{$where};";

			if ( ! empty( $values ) ) {
				$query = $wpdb->prepare( $query, $values );
			}

			$count = $wpdb->get_var( $query );
			set_transient( $key, $count, 10800 );
		}

		return absint( $count );

	}


	/**
	 * Calculate the total earnings of all payments in the database
	 *
	 * @param   array $args Query arguments to override the defaults.
	 *
	 * @access  public
	 * @since   1.5
	 * @return  float
	 */
	public function get_earnings( $args = array() ) {

		global $wpdb;

		$defaults = array(
			'earnings'     => 1, // Just for the cache key
			'subscription' => 0,
			'user_id'      => 0,
			'date'         => array()
		);

		$args = wp_parse_args( $args, $defaults );

		$cache_args = $args;
		$cache_args['user_id'] = is_array( $args['user_id'] ) ? implode( ',', $args['user_id'] ) : $args['user_id'];
		$cache_args['date']    = implode( ',', $args['date'] );
		$cache_key = md5( implode( ',', $cache_args ) );

		$where = ' WHERE 1=1 ';

		// payments for a specific membership level
		if( ! empty( $args['subscription'] ) ) {
			$where   .= " AND `subscription` = %s ";
			$values[] = $args['subscription'];
		}

		// payments for specific users
		if( ! empty( $args['user_id'] ) ) {

			if( is_array( $args['user_id'] ) )
				$user_ids = implode( ',', array_map( 'absint', $args['user_id'] ) );
			else
				$user_ids = intval( $args['user_id'] );

			$where .= " AND `user_id` IN( {$user_ids} ) ";

		}

		// Setup the date query
		if( ! empty( $args['date'] ) && is_array( $args['date'] ) ) {

			if ( ! empty( $args['date']['start'] ) || ! empty( $args['date']['end'] ) ) {

				if ( ! empty( $args['date']['start'] ) ) {

					$start    = date( 'Y-m-d 00:00:00', strtotime( $args['date']['start'] ) );
					$where   .= " AND `date` >= %s";
					$values[] = $start;

				}

				if ( ! empty( $args['date']['end'] ) ) {

					$end    = date( 'Y-m-d 23:59:59', strtotime( $args['date']['end'] ) );
					$where   .= " AND `date` <= %s";
					$values[] = $end;

				}

			} else {

				$day        = ! empty( $args['date']['day'] ) ? absint( $args['date']['day'] ) : null;
				$month      = ! empty( $args['date']['month'] ) ? absint( $args['date']['month'] ) : null;
				$year       = ! empty( $args['date']['year'] ) ? absint( $args['date']['year'] ) : null;
				$date_where = '';

				$date_where .= ! is_null( $year ) ? $year . " = YEAR ( date ) " : '';

				if ( ! is_null( $month ) ) {
					$date_where = $month . " = MONTH ( date ) AND " . $date_where;
				}

				if ( ! is_null( $day ) ) {
					$date_where = $day . " = DAY ( date ) AND " . $date_where;
				}

				$where .= " AND (" . $date_where . ")";

			}
		}

		// Exclude refunded payments
		$where .= " AND ( `status` = 'complete' OR `status` IS NULL OR `status` = '' )";

		$earnings = get_transient( $cache_key );

		if( empty( $earnings ) ) {
			$query = "SELECT SUM(amount) FROM " . $this->db_name . " {$where};";

			if ( ! empty( $values ) ) {
				$query = $wpdb->prepare( $query, $values );
			}

			$earnings = $wpdb->get_var( $query );

			set_transient( $cache_key, $earnings, 3600 );
		}

		$earnings = empty( $earnings ) ? 0 : $earnings;

		return round( $earnings, 2 );

	}


	/**
	 * Calculate the total refunds of all payments in the database
	 *
	 * @param   array $args Query arguments to override the defaults.
	 *
	 * @access  public
	 * @since   2.5
	 * @return  float
	 */
	public function get_refunds( $args = array() ) {

		global $wpdb;

		$defaults = array(
			'refunds'      => 2, // Just for the cache key
			'subscription' => 0,
			'user_id'      => 0,
			'date'         => array()
		);

		$args = wp_parse_args( $args, $defaults );

		$cache_args = $args;
		$cache_args['date'] = implode( ',', $args['date'] );
		$cache_key = md5( implode( ',', $cache_args ) );

		$where = '';

		// refunds for a specific membership level
		if( ! empty( $args['subscription'] ) ) {
			$where .= "WHERE `subscription`= '{$args['subscription']}' ";
		}

		// refunds for specific users
		if( ! empty( $args['user_id'] ) ) {

			if( is_array( $args['user_id'] ) )
				$user_ids = implode( ',', $args['user_id'] );
			else
				$user_ids = intval( $args['user_id'] );

			if( ! empty( $args['subscription'] ) ) {
				$where .= "`user_id` IN( {$user_ids} ) ";
			} else {
				$where .= "WHERE `user_id` IN( {$user_ids} ) ";
			}

		}

		// Setup the date query
		if( ! empty( $args['date'] ) && is_array( $args['date'] ) ) {

			$day   = ! empty( $args['date']['day'] )   ? absint( $args['date']['day'] )   : null;
			$month = ! empty( $args['date']['month'] ) ? absint( $args['date']['month'] ) : null;
			$year  = ! empty( $args['date']['year'] )  ? absint( $args['date']['year'] )  : null;
			$date_where = '';

			$date_where .= ! is_null( $year )  ? $year . " = YEAR ( date ) " : '';

			if( ! is_null( $month ) ) {
				$date_where = $month  . " = MONTH ( date ) AND " . $date_where;
			}

			if( ! is_null( $day ) ) {
				$date_where = $day . " = DAY ( date ) AND " . $date_where;
			}

			if( ! empty( $args['user_id'] ) || ! empty( $args['subscription'] ) ) {
				$where .= "AND (" . $date_where . ") ";
			} else {
				$where .= "WHERE ( " . $date_where . " ) ";
			}
		}

		// Exclude refunded payments
		if( false !== strpos( $where, 'WHERE' ) ) {

			$where .= "AND ( `status` = 'refunded' )";

		} else {

			$where .= "WHERE ( `status` = 'refunded' )";

		}

		$refunds = get_transient( $cache_key );

		if( $refunds === false ) {
			$refunds = $wpdb->get_var( "SELECT SUM(amount) FROM " . $this->db_name . " {$where};" );
			set_transient( $cache_key, $refunds, 3600 );
		}

		return round( $refunds, 2 );

	}


	/**
	 * Retrieves the last payment made by a user
	 *
	 * @param   int $user_id ID of the user to check.
	 *
	 * @access  public
	 * @since   1.5
	 * @return  int|float|false Amount of last payment or false if none is found.
	*/
	public function last_payment_of_user( $user_id = 0 ) {
		global $wpdb;
		$query = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM " . $this->db_name . " WHERE `user_id`='%d' ORDER BY id DESC LIMIT 1;", $user_id ) );
		if( $query )
			return $query[0]->amount;
		return false;
	}

	/**
	 * Retrieve payment meta field for a payment.
	 *
	 * @param   int    $payment_id    Payment ID.
	 * @param   string $meta_key      The meta key to retrieve.
	 * @param   bool   $single        Whether to return a single value.
	 *
	 * @access  public
	 * @since   2.6
	 * @return  mixed                 Will be an array if $single is false. Will be value of meta data field if $single is true.
	 */
	public function get_meta( $payment_id = 0, $meta_key = '', $single = false ) {
		return get_metadata( 'rcp_payment', $payment_id, $meta_key, $single );
	}

	/**
	 * Add meta data field to a payment.
	 *
	 * @param   int    $payment_id    Payment ID.
	 * @param   string $meta_key      Metadata name.
	 * @param   mixed  $meta_value    Metadata value.
	 * @param   bool   $unique        Optional, default is false. Whether the same key should not be added.
	 *
	 * @access  public
	 * @since   2.6
	 * @since 3.5.10                  Removing default values for $payment_id and $meta_key
	 *
	 * @return  bool                  False for failure. True for success.
	 */
	public function add_meta( $payment_id, $meta_key, $meta_value, $unique = false ) {
		return add_metadata( 'rcp_payment', $payment_id, $meta_key, $meta_value, $unique );
	}

	/**
	 * Update payment meta field based on Payment ID.
	 *
	 * Use the $prev_value parameter to differentiate between meta fields with the
	 * same key and Payment ID.
	 *
	 * If the meta field for the payment does not exist, it will be added.
	 *
	 * @param   int    $payment_id    Payment ID.
	 * @param   string $meta_key      Metadata key.
	 * @param   mixed  $meta_value    Metadata value.
	 * @param   mixed  $prev_value    Optional. Previous value to check before removing.
	 *
	 * @access  public
	 * @since   2.6
	 * @since   3.5.10                Removing default value from $payment_id and $meta_key
	 *
	 * @return  bool                  False on failure, true if success.
	 */
	public function update_meta( $payment_id, $meta_key, $meta_value, $prev_value = '' ) {
		return update_metadata( 'rcp_payment', $payment_id, $meta_key, $meta_value, $prev_value );
	}

	/**
	 * Remove metadata matching criteria from a payment.
	 *
	 * You can match based on the key, or key and value. Removing based on key and
	 * value, will keep from removing duplicate metadata with the same key. It also
	 * allows removing all metadata matching key, if needed.
	 *
	 * @param   int    $payment_id    Payment ID.
	 * @param   string $meta_key      Metadata name.
	 * @param   mixed  $meta_value    Optional. Metadata value.
	 *
	 * @access  public
	 * @since   2.6
	 * @return  bool                  False for failure. True for success.
	 */
	public function delete_meta( $payment_id = 0, $meta_key = '', $meta_value = '' ) {
		return delete_metadata( 'rcp_payment', $payment_id, $meta_key, $meta_value );
	}

}
