<?php

/**
 * Class MM_WPFS_Stripe
 *
 * deals with calls to Stripe API
 *
 */
class MM_WPFS_Stripe {
    use MM_WPFS_Logger_AddOn;
    use MM_WPFS_StaticContext_AddOn;

	const DESIRED_STRIPE_API_VERSION = '2020-08-27';

	/**
	 * @var string
	 */
	const INVALID_NUMBER_ERROR = 'invalid_number';
	/**
	 * @var string
	 */
	const INVALID_NUMBER_ERROR_EXP_MONTH = 'invalid_number_exp_month';
	/**
	 * @var string
	 */
	const INVALID_NUMBER_ERROR_EXP_YEAR = 'invalid_number_exp_year';
	/**
	 * @var string
	 */
	const INVALID_EXPIRY_MONTH_ERROR = 'invalid_expiry_month';
	/**
	 * @var string
	 */
	const INVALID_EXPIRY_YEAR_ERROR = 'invalid_expiry_year';
	/**
	 * @var string
	 */
	const INVALID_CVC_ERROR = 'invalid_cvc';
	/**
	 * @var string
	 */
	const INCORRECT_NUMBER_ERROR = 'incorrect_number';
	/**
	 * @var string
	 */
	const EXPIRED_CARD_ERROR = 'expired_card';
	/**
	 * @var string
	 */
	const INCORRECT_CVC_ERROR = 'incorrect_cvc';
	/**
	 * @var string
	 */
	const INCORRECT_ZIP_ERROR = 'incorrect_zip';
	/**
	 * @var string
	 */
	const CARD_DECLINED_ERROR = 'card_declined';
	/**
	 * @var string
	 */
	const MISSING_ERROR = 'missing';
	/**
	 * @var string
	 */
	const PROCESSING_ERROR = 'processing_error';
	/**
	 * @var string
	 */
	const MISSING_PAYMENT_INFORMATION = 'missing_payment_information';
	/**
	 * @var string
	 */
	const COULD_NOT_FIND_PAYMENT_INFORMATION = 'Could not find payment information';

	/* @var $stripe \StripeWPFS\StripeClient */
	private $stripe;

    /* @var MM_WPFS_Options*/
    private $options;

    /**
	 * MM_WPFS_Stripe constructor.
	 *
	 * @param $token
	 *
	 * @throws Exception
	 */
	public function __construct( $token, $loggerService ) {
        $this->initLogger( $loggerService, MM_WPFS_LoggerService::MODULE_STRIPE );
        $this->options = new MM_WPFS_Options();

        $this->initStaticContext();

        try {
            $this->stripe = self::createStripeClient( $token );
        } catch ( Exception $ex ) {
            $this->logger->error(__FUNCTION__, 'Error while initializing the Stripe client', $ex);
        }
	}

    /**
     * @param $token string
     * @return \StripeWPFS\StripeClient
     */
	public static function createStripeClient( $token ) {
        return new \StripeWPFS\StripeClient([
            "api_key"           => $token,
            "stripe_version"    => self::DESIRED_STRIPE_API_VERSION
        ]);
    }

    /**
     * @param $context MM_WPFS_StaticContext
     * @param $liveMode
     * @return mixed
     */
    public static function getStripeAuthenticationTokenByMode($context, $liveMode) {
        $optionKey = $liveMode ? MM_WPFS_Options::OPTION_API_LIVE_SECRET_KEY : MM_WPFS_Options::OPTION_API_TEST_SECRET_KEY;

        return $context->getOptions()->get($optionKey);
    }

    /**
     * @param $context MM_WPFS_StaticContext
     * @return string
     */
    public static function getStripeAuthenticationToken($context) {
        return MM_WPFS_Stripe::getStripeAuthenticationTokenByMode($context, self::isStripeApiInLiveMode($context));
    }

    /**
     * @param $context MM_WPFS_StaticContext
     * @return bool
     */
    public static function isStripeApiInLiveMode($context) {
        return $context->getOptions()->get(MM_WPFS_Options::OPTION_API_MODE) === MM_WPFS::STRIPE_API_MODE_LIVE;
    }

    /**
     * @return mixed
     */
    public static function getStripeTestAuthenticationToken($context) {
        return $context->getOptions()->get(MM_WPFS_Options::OPTION_API_TEST_SECRET_KEY);
    }

    /**
     * @return mixed
     */
    public static function getStripeLiveAuthenticationToken($context) {
        return $context->getOptions()->get(MM_WPFS_Options::OPTION_API_LIVE_SECRET_KEY);
    }

    function getErrorCodes() {
		return array(
			self::INVALID_NUMBER_ERROR,
			self::INVALID_NUMBER_ERROR_EXP_MONTH,
			self::INVALID_NUMBER_ERROR_EXP_YEAR,
			self::INVALID_EXPIRY_MONTH_ERROR,
			self::INVALID_EXPIRY_YEAR_ERROR,
			self::INVALID_CVC_ERROR,
			self::INCORRECT_NUMBER_ERROR,
			self::EXPIRED_CARD_ERROR,
			self::INCORRECT_CVC_ERROR,
			self::INCORRECT_ZIP_ERROR,
			self::CARD_DECLINED_ERROR,
			self::MISSING_ERROR,
			self::PROCESSING_ERROR,
			self::MISSING_PAYMENT_INFORMATION
		);
	}

    /**
     * @param string $stripeCustomerId
     * @param string $stripePlanId
     *
     * @return \StripeWPFS\Subscription
     * @throws Exception
     */
	public function subscribeCustomerToPlan($stripeCustomerId, $stripePlanId ) {
        $subscriptionData = array(
            'customer'        => $stripeCustomerId,
            'items'           => array(
                array(
                    'price'     => $stripePlanId,
                )
            )
        );

        $stripeSubscription = $this->stripe->subscriptions->create( $subscriptionData );

        return $stripeSubscription;
    }

    /**
     * @param \StripeWPFS\Subscription $stripeSubscription
     * @param string $quantity
     *
     * @throws Exception
     */
    public function createUsageRecordForSubscription( $stripeSubscription, $quantity ) {
        $stripeSubscriptionItem = $stripeSubscription->items->data[0];

        $this->stripe->subscriptionItems->createUsageRecord(
            $stripeSubscriptionItem->id,
            [
                'quantity' => $quantity,
                /*
                 * We add 5 minutes to avoid the following Stripe error message:
                 * "Cannot create the usage record with this timestamp because timestamps must be after
                 *  the subscription's last invoice period (or current period start time)."
                 */
                'timestamp' => time() + 5 * 60,
                'action' => 'set',
            ]
        );
    }

    /**
     * @param $stripePaymentMethodId
     *
     * @return \StripeWPFS\PaymentMethod
     * @throws \StripeWPFS\Exception\ApiErrorException
     * @throws Exception
     */
	public function validatePaymentMethodCVCCheck( $stripePaymentMethodId ) {
		/* @var $paymentMethod \StripeWPFS\PaymentMethod */
		$paymentMethod = $this->stripe->paymentMethods->retrieve( $stripePaymentMethodId );

		if ( is_null( $paymentMethod->card->checks->cvc_check ) ) {
			throw new Exception(
			    /* translators: Validation error message for a card number without a CVC code */
			    __( 'Please enter a CVC code', 'wp-full-stripe' ) );
		}

		return $paymentMethod;
	}

    /**
     * @param $ctx MM_WPFS_CreateSubscriptionContext
     * @param $options MM_WPFS_CreateSubscriptionOptions
     * @return \StripeWPFS\Subscription
     *
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function createSubscriptionForCustomer( $ctx, $options ) {
        $stripeCustomer = $this->retrieveCustomer( $ctx->stripeCustomerId );

		$recurringPrice = $this->stripe->prices->retrieve( $ctx->stripePriceId );
		if ( ! isset( $recurringPrice ) ) {
			throw new Exception( "Recurring price with id '" . $ctx->stripePriceId . " doesn't exist." );
		}

		if ( ! is_null( $ctx->stripePaymentMethodId ) ) {
			$paymentMethod = $this->retrievePaymentMethod($ctx->stripePaymentMethodId );
			$paymentMethod->attach( array( 'customer' => $stripeCustomer->id ) );

			$this->stripe->customers->update(
				$stripeCustomer->id,
				array(
					'invoice_settings' => array(
						'default_payment_method' => $ctx->stripePaymentMethodId
					)
				)
			);
		}

		if ( $ctx->setupFee > 0 ) {
            $setupFeeParams = array(
                'customer'    => $stripeCustomer->id,
                'currency'    => $recurringPrice->currency,
                'description' => sprintf(
                    /* translators: It's a line item for the initial payment of a subscription */
                    __( 'One-time setup fee (plan: %s)', 'wp-full-stripe' ), MM_WPFS_Localization::translateLabel( $ctx->productName )),
                'quantity'    => $ctx->stripePlanQuantity,
                'unit_amount' => $ctx->setupFee,
                'metadata'    => [
                    'type'      => 'setupFee'
                ]
            );
            if ( ! $ctx->isStripeTax ) {
                $setupFeeParams[ 'tax_rates' ] = $options->taxRateIds;
            }

            $this->stripe->invoiceItems->create( $setupFeeParams );
		}

        $hasBillingCycleAnchor          = $ctx->billingCycleAnchorDay > 0;
        $hasMonthlyBillingCycleAnchor   = $recurringPrice->recurring['interval'] === 'month' && $hasBillingCycleAnchor;
        $hasTrialPeriod                 = $ctx->trialPeriodDays > 0;

        $subscriptionItemsParams = array(
            array(
                'price'     => $recurringPrice->id,
                'quantity'  => $ctx->stripePlanQuantity,
            )
        );
        if ( ! $ctx->isStripeTax ) {
            $subscriptionItemsParams[0]['tax_rates'] = $options->taxRateIds;
        }

		$subscriptionData = array(
			'customer'        => $stripeCustomer->id,
			'items'           => $subscriptionItemsParams,
			'expand'          => array(
			    'latest_invoice',
				'latest_invoice.payment_intent',
				'latest_invoice.charge',
				'pending_setup_intent'
			)
		);
		if ( ! empty( $ctx->discountId ) ) {
            switch( $ctx->discountType ) {
                case MM_WPFS::DISCOUNT_TYPE_COUPON:
                    $subscriptionData['coupon'] = $ctx->discountId;
                    break;

                case MM_WPFS::DISCOUNT_TYPE_PROMOTION_CODE:
                    $subscriptionData['promotion_code'] = $ctx->discountId;
                    break;

                default:
                    $this->logger->warning( __FUNCTION__, 'Unsupported discount type: ' . $ctx->discountType );
            }
		}
        if ( $hasTrialPeriod ) {
            $subscriptionData['trial_period_days'] = $ctx->trialPeriodDays;
        }
		if ( $hasMonthlyBillingCycleAnchor ) {
		    if ( $hasTrialPeriod ) {
                $subscriptionData['billing_cycle_anchor'] = MM_WPFS_Utils::calculateBillingCycleAnchorFromTimestamp( $ctx->billingCycleAnchorDay, MM_WPFS_Utils::calculateTrialEndFromNow( $ctx->trialPeriodDays ));
            } else {
                $subscriptionData['billing_cycle_anchor'] = MM_WPFS_Utils::calculateBillingCycleAnchorFromNow( $ctx->billingCycleAnchorDay );
            }

            if ( $ctx->prorateUntilAnchorDay === 1 ) {
                $subscriptionData['proration_behavior'] = 'create_prorations';
            } else {
                $subscriptionData['proration_behavior'] = 'none';
            }
        }
		if ( ! is_null( $ctx->metadata ) ) {
			$subscriptionData['metadata'] = $ctx->metadata;
		}
        if ( $ctx->isStripeTax ) {
            $subscriptionData['automatic_tax'] = [
                'enabled' => true
            ];
        }
		$stripeSubscription = $this->stripe->subscriptions->create( $subscriptionData );

		return $stripeSubscription;
	}

	/**
	 * @param $customerId
	 *
	 * @return \StripeWPFS\Customer
	 */
	public function retrieveCustomer( $customerId ) {
		return $this->stripe->customers->retrieve( $customerId );
	}

    /**
     * @param $customerId
     * @param $params
     *
     * @return \StripeWPFS\Customer
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function retrieveCustomerWithParams( $customerId, $params ) {
        return $this->stripe->customers->retrieve( $customerId, $params );
    }

    /**
     * @param $productId
     *
     * @return \StripeWPFS\Product
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function retrieveProduct( $productId ) {
        return $this->stripe->products->retrieve( $productId );
    }

	/**
	 * @param $code
	 *
	 * @return string|void
	 */
	function resolveErrorMessageByCode($code ) {
		if ( $code === self::INVALID_NUMBER_ERROR ) {
			$resolved_message =  /* translators: message for Stripe error code 'invalid_number' */
				__( 'Your card number is invalid.', 'wp-full-stripe' );
		} elseif ( $code === self::INVALID_EXPIRY_MONTH_ERROR || $code === self::INVALID_NUMBER_ERROR_EXP_MONTH ) {
			$resolved_message = /* translators: message for Stripe error code 'invalid_expiry_month' */
				__( 'Your card\'s expiration month is invalid.', 'wp-full-stripe' );
		} elseif ( $code === self::INVALID_EXPIRY_YEAR_ERROR || $code === self::INVALID_NUMBER_ERROR_EXP_YEAR ) {
			$resolved_message = /* translators: message for Stripe error code 'invalid_expiry_year' */
				__( 'Your card\'s expiration year is invalid.', 'wp-full-stripe' );
		} elseif ( $code === self::INVALID_CVC_ERROR ) {
			$resolved_message = /* translators: message for Stripe error code 'invalid_cvc' */
				__( 'Your card\'s security code is invalid.', 'wp-full-stripe' );
		} elseif ( $code === self::INCORRECT_NUMBER_ERROR ) {
			$resolved_message = /* translators: message for Stripe error code 'incorrect_number' */
				__( 'Your card number is incorrect.', 'wp-full-stripe' );
		} elseif ( $code === self::EXPIRED_CARD_ERROR ) {
			$resolved_message = /* translators: message for Stripe error code 'expired_card' */
				__( 'Your card has expired.', 'wp-full-stripe' );
		} elseif ( $code === self::INCORRECT_CVC_ERROR ) {
			$resolved_message = /* translators: message for Stripe error code 'incorrect_cvc' */
				__( 'Your card\'s security code is incorrect.', 'wp-full-stripe' );
		} elseif ( $code === self::INCORRECT_ZIP_ERROR ) {
			$resolved_message = /* translators: message for Stripe error code 'incorrect_zip' */
				__( 'Your card\'s zip code failed validation.', 'wp-full-stripe' );
		} elseif ( $code === self::CARD_DECLINED_ERROR ) {
			$resolved_message = /* translators: message for Stripe error code 'card_declined' */
				__( 'Your card was declined.', 'wp-full-stripe' );
		} elseif ( $code === self::MISSING_ERROR ) {
			$resolved_message = /* translators: message for Stripe error code 'missing' */
				__( 'There is no card on a customer that is being charged.', 'wp-full-stripe' );
		} elseif ( $code === self::PROCESSING_ERROR ) {
			$resolved_message = /* translators: message for Stripe error code 'processing_error' */
				__( 'An error occurred while processing your card.', 'wp-full-stripe' );
		} elseif ( $code === self::MISSING_PAYMENT_INFORMATION ) {
			$resolved_message = /* translators: Stripe error message 'Missing payment information' */
				__( 'Missing payment information', 'wp-full-stripe' );
		} elseif ( $code === self::COULD_NOT_FIND_PAYMENT_INFORMATION ) {
			$resolved_message = /* translators: Stripe error message 'Could not find payment information' */
				__( 'Could not find payment information', 'wp-full-stripe' );
		} else {
			$resolved_message = null;
		}

		return $resolved_message;
	}

	function createPlan($id, $name, $currency, $amount, $setupFee, $interval, $trialDays, $intervalCount, $cancellationCount ) {
        $planData = array(
            "amount"         => $amount,
            "interval"       => $interval,
            "nickname"       => $id,
            "product"        => array(
                "name" => $name,
            ),
            "currency"       => $currency,
            "interval_count" => $intervalCount,
            "id"             => $id,
            "metadata"       => array(
                "cancellation_count" => $cancellationCount,
                "setup_fee"          => $setupFee
            )
        );

        if ( $trialDays != 0 ) {
            $planData['metadata']['trial_period_days'] = $trialDays;
        }

        $this->stripe->plans->create( $planData );
	}

    /**
     * @param $id
     * @param $name
     * @param $currency
     * @param $interval
     * @param $intervalCount
     *
     * @return \StripeWPFS\Price
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    function createRecurringDonationPlan($id, $name, $currency, $interval, $intervalCount ) {
        $planData = array(
            "currency"        => $currency,
            "unit_amount"     => "1",
            "nickname"        => $name,
            "recurring"       => array(
                "interval"        => $interval,
                "interval_count"  => $intervalCount,
                "usage_type"      => "metered",
                "aggregate_usage" => "last_ever"
            ),
            "product_data"        => array(
                "name" => $name
            ),
            "lookup_key"      => $id,
        );

        return $this->stripe->prices->create( $planData );
    }


    /**
     * @param $planId
     *
     * @return \StripeWPFS\Price|null
     */
	public function retrievePlan($planId ) {
        $plan = null;
		try {
			$plan = $this->stripe->prices->retrieve( $planId, array( "expand" => array( "product" ) ));
		} catch ( Exception $e ) {
            // plan not found, let's fall through
		}

		return $plan;
	}

    /**
     * @param $planId
     *
     * @return \StripeWPFS\Collection
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function retrieveDonationPlansWithLookupKey( $planId ) {
	    $prices = $this->stripe->prices->all([
	        'active'      => true,
            'lookup_keys' => [ $planId ]
        ]);

	    return $prices;
    }

	public function getCustomersByEmail($email ) {
		$customers = array();

		try {
			do {
				$params        = array( 'limit' => 100, 'email' => $email );
				$last_customer = end( $customers );
				if ( $last_customer ) {
					$params['starting_after'] = $last_customer['id'];
				}
				$customer_collection = $this->stripe->customers->all( $params );
				$customers           = array_merge( $customers, $customer_collection['data'] );
			} while ( $customer_collection['has_more'] );
		} catch ( Exception $ex ) {
            $this->logger->error(__FUNCTION__, 'Error while getting customers by email', $ex);

			$customers = array();
		}

		return $customers;
	}

    /**
     * @param $params
     * @return \StripeWPFS\Collection
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function getCustomersWithParams( $params ) {
        return $this->stripe->customers->all( $params );
    }

	/**
	 * @return array|\StripeWPFS\Collection
	 */
	public function getSubscriptionPlans() {
		$plans = array();
		try {
			do {
				$params    = array( 'type' => 'recurring', 'limit' => 100, 'include[]' => 'total_count', 'expand' => array( 'data.product' ) );
				$last_plan = end( $plans );
				if ( $last_plan ) {
					$params['starting_after'] = $last_plan['id'];
				}
				$plan_collection = $this->stripe->prices->all( $params );
				$plans           = array_merge( $plans, $plan_collection['data'] );
			} while ( $plan_collection['has_more'] );
		} catch ( Exception $ex ) {
            $this->logger->error(__FUNCTION__, 'Error while getting subscription plans', $ex);

			$plans = array();
		}

		return $plans;
	}

    /**
     * @return array|\StripeWPFS\Collection
     */
    public function getOnetimePrices() {
        $prices = array();
        do {
            $params = array(
                'active' => true,
                'type' => 'one_time',
                'limit' => 100,
                'include[]' => 'total_count',
                'expand' => array( 'data.product' )
            );

            $lastPrice = end( $prices );
            if ( $lastPrice ) {
                $params['starting_after'] = $lastPrice['id'];
            }
            $priceCollection = $this->stripe->prices->all( $params );
            $prices          = array_merge( $prices, $priceCollection['data'] );
        } while ( $priceCollection['has_more'] );

        return $prices;
    }

    /**
     * @return array|\StripeWPFS\Collection
     */
    public function getRecurringPrices() {
        $prices = array();
        do {
            $params = array(
                'active' => true,
                'type' => 'recurring',
                'limit' => 100,
                'include[]' => 'total_count',
                'expand' => array( 'data.product' )
            );

            $lastPrice = end( $prices );
            if ( $lastPrice ) {
                $params['starting_after'] = $lastPrice['id'];
            }
            $priceCollection = $this->stripe->prices->all( $params );
            $prices          = array_merge( $prices, $priceCollection['data'] );
        } while ( $priceCollection['has_more'] );

        return $prices;
    }

    /**
     * @return array|\StripeWPFS\Collection
     */
    public function getTaxRates() {
        $taxRates = array();
        do {
            $params = array(
                'active' => true,
                'limit' => 100,
                'include[]' => 'total_count'
            );

            $lastTaxRate = end( $taxRates );
            if ( $lastTaxRate ) {
                $params['starting_after'] = $lastTaxRate['id'];
            }
            $taxRateCollection = $this->stripe->taxRates->all( $params );
            $taxRates          = array_merge( $taxRates, $taxRateCollection['data'] );
        } while ( $taxRateCollection['has_more'] );

        return $taxRates;
    }

	/**
	 * @param $code
	 *
	 * @return \StripeWPFS\Coupon
	 * @throws \StripeWPFS\Exception\ApiErrorException
	 */
	public function retrieveCoupon( $code ) {
		return $this->stripe->coupons->retrieve( $code, [ 'expand' => [ 'applies_to' ] ] );
	}

	/**
	 * @param $code
	 *
	 * @return \StripeWPFS\PromotionCode
	 * @throws \StripeWPFS\Exception\ApiErrorException
	 */
	public function retrievePromotionalCode( $code ) {
		$promotionalCodesCollection = $this->stripe->promotionCodes->all(
			[
				'code'   => $code,
				'expand' => [ 'data.coupon.applies_to' ]
			]
		);
		$result                     = null;

        foreach ( $promotionalCodesCollection->autoPagingIterator() as $promotionCode ) {
			if ( strcasecmp( $code, $promotionCode->code ) === 0 ) {
				$result = $promotionCode;
				break;
			}
		}

		return $result;
	}

    protected function getPromotionalCode( $code ) {
        try {
            return $this->retrievePromotionalCode( $code );
        } catch ( Exception $ex ) {
            $this->logger->debug(__FUNCTION__, "Cannot retrieve promotional code" . $ex);
            return null;
        }
    }

    protected function getCoupon( $code ) {
        try {
            return $this->retrieveCoupon( $code );
        } catch ( Exception $ex ) {
            $this->logger->debug(__FUNCTION__, "Cannot retrieve coupon" . $ex);

            return null;
        }
    }

    /**
     * @param $code string
     * @return \StripeWPFS\Coupon|null
     */
    public function retrieveCouponByPromotionalCodeOrCouponCode( $code ) {
        $result = null;

        try {
            $promotionalCode = $this->getPromotionalCode( $code );

            if ( ! is_null( $promotionalCode ) ) {
                if ( false == $promotionalCode->active ) {
                    $result = $this->getCoupon( $code );
                } else {
                    $result = $promotionalCode->coupon;
                }
            } else {
                $result = $this->getCoupon( $code );
            }
        } catch ( Exception $ex ) {
            $this->logger->error(__FUNCTION__, "Cannot retrieve coupon or promotional code", $ex);
        }

        return $result;
    }


    /**
     * @param $invoiceId
     *
     * @return \StripeWPFS\Invoice
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    function retrieveInvoice( $invoiceId ) {
        return $this->stripe->invoices->retrieve( $invoiceId );
    }

    /**
	 * @param $paymentMethodId
	 * @param $customerName
	 * @param $customerEmail
	 * @param $metadata
	 *
	 * @return \StripeWPFS\Customer
     *
     * @throws StripeWPFS\Exception\ApiErrorException
	 */
	function createCustomerWithPaymentMethod( $paymentMethodId, $customerName, $customerEmail, $metadata, $taxIdType = null, $taxId = null ) {
		$customer = array(
			'payment_method'   => $paymentMethodId,
			'email'            => $customerEmail,
			'invoice_settings' => array(
				'default_payment_method' => $paymentMethodId
			)
		);

		if ( ! is_null( $customerName ) ) {
			$customer['name'] = $customerName;
		}

		if ( ! is_null( $metadata ) ) {
			$customer['metadata'] = $metadata;
		}

		if ( ! empty( $taxIdType ) && ! empty( $taxId )  ) {
		    $customer['tax_id_data'] = [
		        [
                    'type'  =>  $taxIdType,
                    'value' =>  $taxId
                ]
            ];
        }

		return $this->stripe->customers->create( $customer );
	}

	/**
	 * @param $paymentMethodId
	 * @param $customerId
	 * @param $currency
	 * @param $amount
	 * @param $capture
	 * @param null $description
	 * @param null $metadata
	 * @param null $stripeEmail
	 *
	 * @return \StripeWPFS\PaymentIntent
     *
     * @throws StripeWPFS\Exception\ApiErrorException
	 */
	function createPaymentIntent( $paymentMethodId, $customerId, $currency, $amount, $capture, $description, $metadata = null, $stripeEmail = null ) {
		$paymentIntentParameters = array(
            'amount'              => $amount,
            'currency'            => $currency,
            'confirm'             => true,
            'customer'            => $customerId,
			'payment_method'      => $paymentMethodId,
			'confirmation_method' => 'manual',
		);
		if ( ! empty( $description ) ) {
			$paymentIntentParameters['description'] = $description;
		}
		if ( false === $capture ) {
			$paymentIntentParameters['capture_method'] = 'manual';
		}
		if ( isset( $stripeEmail ) ) {
			$paymentIntentParameters['receipt_email'] = $stripeEmail;
		}
		if ( isset( $metadata ) ) {
			$paymentIntentParameters['metadata'] = $metadata;
		}

		$intent = $this->stripe->paymentIntents->create( apply_filters( 'fullstripe_payment_intent_parameters', $paymentIntentParameters ) );

		return $intent;
	}

    /**
     * @param $ctx MM_WPFS_CreateOneTimeInvoiceContext
     * @param $options MM_WPFS_CreateOneTimeInvoiceOptions
     * @return \StripeWPFS\Invoice
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	function createInvoiceForOneTimePayment( $ctx, $options ) {
        $invoiceParams = array(
            'customer'     => $ctx->stripeCustomerId,
            'auto_advance' => $options->autoAdvance
        );

		$invoiceItemParams = array(
            'customer' => $ctx->stripeCustomerId,
        );

		if ( $ctx->stripePriceId !== null ) {
            $invoiceItemParams['price'] = $ctx->stripePriceId;
        } else {
            $invoiceItemParams['amount'] = $ctx->amount;
            $invoiceItemParams['currency'] = $ctx->currency;
            $invoiceItemParams['description'] = __( $ctx->productName, 'wp-full-stripe' );
        }

		if ( isset( $ctx->stripeCouponId ) ) {
			$invoiceItemParams['discounts'] = array(
				array( 'coupon' => $ctx->stripeCouponId )
			);
		}

        if ( $ctx->isStripeTax ) {
            $invoiceParams['automatic_tax'] = [
                'enabled' => true
            ];
        } else if ( isset( $options->taxRateIds ) && count( $options->taxRateIds ) > 0 ) {
            $invoiceItemParams['tax_rates'] = $options->taxRateIds;
        }

		$invoiceItem = $this->stripe->invoiceItems->create( $invoiceItemParams );
		$createdInvoice = $this->stripe->invoices->create( $invoiceParams );

        return $createdInvoice;
	}

    /**
     * @param $ctx MM_WPFS_CreateOneTimeInvoiceContext
     * @param $options MM_WPFS_CreateOneTimeInvoiceOptions
     * @return \StripeWPFS\Invoice
     */
    function createPreviewInvoiceForOneTimePayment( $ctx, $options ) {
        $invoiceParams = [];

        $address = [
            'country'   => $ctx->taxCountry
        ];
        if ( $ctx->isStripeTax ) {
            $invoiceParams['automatic_tax'] = [
                'enabled'   => true
            ];

            if ( ! empty( $ctx->taxPostalCode ) ) {
                $address['postal_code'] = $ctx->taxPostalCode;
            }
        } else {
            if ( ! empty( $ctx->taxState ) ) {
                $address['state'] = $ctx->taxState;
            }
        }
        $invoiceParams['customer_details'] = [
            'address'   => $address
        ];

        if ( $ctx->isStripeTax && ! empty( $ctx->taxIdType ) && ! empty( $ctx->taxId )) {
            $invoiceParams['customer_details']['tax_ids'] = [
                [
                    'type'    => $ctx->taxIdType,
                    'value'   => $ctx->taxId,
                ]
            ];
        }

        $itemParams = [];
        if ( $ctx->stripePriceId !== null ) {
            $itemParams['price'] = $ctx->stripePriceId;
        } else {
            $itemParams['amount'] = $ctx->amount;
            $itemParams['currency'] = $ctx->currency;
            $itemParams['description'] = __( $ctx->productName, 'wp-full-stripe' );
        }

        if ( isset( $ctx->stripeCouponId ) ) {
            $itemParams['discounts'] = array(
                array( 'coupon' => $ctx->stripeCouponId )
            );
        }

        if ( ! $ctx->isStripeTax ) {
            if ( isset( $options->taxRateIds ) && count( $options->taxRateIds ) > 0 ) {
                $itemParams['tax_rates'] = $options->taxRateIds;
            }
        }

        $invoiceParams[ 'invoice_items' ] = [
            $itemParams
        ];

        return $this->getUpcomingInvoice( $invoiceParams );
    }

    /**
     * @param $finalizedInvoice
     * @param $stripePaymentMethodId
     * @param $stripeChargeDescription
     * @param $stripeReceiptEmailAddress
     *
     * @return \StripeWPFS\Invoice
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	function updatePaymentIntentByInvoice( $finalizedInvoice, $stripePaymentMethodId, $stripeChargeDescription, $metadata, $stripeReceiptEmailAddress  ) {
        $generatedPaymentIntent = $this->stripe->paymentIntents->retrieve( $finalizedInvoice->payment_intent );
        $paymentIntentParameters = array();
        if ( ! empty( $stripeChargeDescription ) ) {
            $paymentIntentParameters['description'] = $stripeChargeDescription;
        }
        if ( isset( $stripeReceiptEmailAddress ) ) {
            $paymentIntentParameters['receipt_email'] = $stripeReceiptEmailAddress;
        }
        if ( isset( $metadata ) ) {
            $paymentIntentParameters['metadata'] = $metadata;
        }
        $updatedPaymentIntent = $this->stripe->paymentIntents->update( $generatedPaymentIntent->id, apply_filters( 'fullstripe_payment_intent_parameters', $paymentIntentParameters ));
        $this->stripe->paymentIntents->confirm( $updatedPaymentIntent->id, array( 'payment_method' => $stripePaymentMethodId ) );

        return $this->stripe->invoices->update( $finalizedInvoice->id );
    }

	/**
	 * @param $paymentIntentId
	 *
	 * @return \StripeWPFS\PaymentIntent
     * @throws \StripeWPFS\Exception\ApiErrorException
	 */
	function retrievePaymentIntent( $paymentIntentId ) {
		$intent = $this->stripe->paymentIntents->retrieve( $paymentIntentId );

		return $intent;
	}

    /**
     * @param $invoiceId
     * @param $params
     *
     * @return \StripeWPFS\Invoice
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function retrieveInvoiceWithParams( $invoiceId, $params ) {
	    $invoice = $this->stripe->invoices->retrieve( $invoiceId, $params );

        return $invoice;
    }

    /**
     * @param $sessionId
     * @param $params
     *
     * @return \StripeWPFS\Checkout\Session
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function retrieveCheckoutSessionWithParams( $sessionId, $params ) {
	    $checkoutSession = $this->stripe->checkout->sessions->retrieve( $sessionId, $params );

	    return $checkoutSession;
    }

    /**
     * @param $paymentMethodId
     *
     * @return \StripeWPFS\PaymentMethod
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function retrievePaymentMethod( $paymentMethodId ) {
	    $paymentMethod = $this->stripe->paymentMethods->retrieve( $paymentMethodId );

	    return $paymentMethod;
    }

    /**
     * @param $eventID
     *
     * @return \StripeWPFS\Event
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function retrieveEvent( $eventID ) {
        $event = $this->stripe->events->retrieve( $eventID );

        return $event;
    }


    /**
     * @param $stripePaymentMethodId
     *
     * @return \StripeWPFS\SetupIntent
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function createSetupIntentWithPaymentMethod( $stripePaymentMethodId ) {
		$params = array(
			'usage'                => 'off_session',
			'payment_method_types' => [ 'card' ],
			'payment_method'       => $stripePaymentMethodId,
			'confirm'              => false
		);
		$intent = $this->stripe->setupIntents->create( $params );

		return $intent;
	}

    /**
     * @param $stripeSetupIntentId
     *
     * @return \StripeWPFS\SetupIntent
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	function retrieveSetupIntent( $stripeSetupIntentId ) {
		$intent = $this->stripe->setupIntents->retrieve( $stripeSetupIntentId );

		return $intent;
	}

	/**
	 * Attaches the given PaymentMethod to the given Customer if the Customer do not have an identical PaymentMethod
	 * by card fingerprint.
	 *
	 * @param \StripeWPFS\Customer $stripeCustomer
	 * @param \StripeWPFS\PaymentMethod $currentPaymentMethod
	 * @param bool $setToDefault
	 *
	 * @return \StripeWPFS\PaymentMethod the attached PaymentMethod or the existing one
     *
     * @throws StripeWPFS\Exception\ApiErrorException
	 */
	function attachPaymentMethodToCustomerIfMissing( $stripeCustomer, $currentPaymentMethod, $setToDefault = false ) {
		$attachedPaymentMethod = null;

		if ( $stripeCustomer instanceof \StripeWPFS\Customer && $currentPaymentMethod instanceof \StripeWPFS\PaymentMethod ) {
			// WPFS-983: tnagy find existing PaymentMethod with identical fingerprint and reuse it
			$existingStripePaymentMethod = $this->findExistingPaymentMethodByFingerPrintAndExpiry(
				$stripeCustomer,
				$currentPaymentMethod->card->fingerprint,
				$currentPaymentMethod->card->exp_year,
				$currentPaymentMethod->card->exp_month
			);
			if ( $existingStripePaymentMethod instanceof \StripeWPFS\PaymentMethod ) {
                $this->logger->debug(__FUNCTION__, 'PaymentMethod with identical card fingerprint exists, won\'t attach.');

				$attachedPaymentMethod = $existingStripePaymentMethod;
			} else {
				if ( is_null( $currentPaymentMethod->customer ) ) {
					$currentPaymentMethod->attach( array( 'customer' => $stripeCustomer->id ) );

                    $this->logger->debug(__FUNCTION__, 'PaymentMethod attached.' );
				}
				$attachedPaymentMethod = $currentPaymentMethod;
			}
			if ( $setToDefault ) {
				$this->stripe->customers->update(
					$stripeCustomer->id,
					array(
						'invoice_settings' => array(
							'default_payment_method' => $attachedPaymentMethod->id
						)
					)
				);

                $this->logger->debug(__FUNCTION__, 'Default PaymentMethod updated.' );
			}

		}

		return $attachedPaymentMethod;
	}

	/**
	 * Find a Customer's PaymentMethod by fingerprint if exists.
	 *
	 * @param \StripeWPFS\Customer $stripeCustomer
	 * @param string $paymentMethodCardFingerPrint
	 * @param $expiryYear
	 * @param $expiryMonth
	 *
	 * @return null|\StripeWPFS\PaymentMethod the existing PaymentMethod
	 * @throws StripeWPFS\Exception\ApiErrorException
	 */
	public function findExistingPaymentMethodByFingerPrintAndExpiry( $stripeCustomer, $paymentMethodCardFingerPrint, $expiryYear, $expiryMonth ) {
		if ( empty( $paymentMethodCardFingerPrint ) ) {
			return null;
		}
		$paymentMethods        = $this->stripe->paymentMethods->all( array(
				'customer' => $stripeCustomer->id,
				'type'     => 'card'
			)
		);
		$existingPaymentMethod = null;
		if ( $paymentMethods instanceof \StripeWPFS\Collection ) {
			foreach ( $paymentMethods['data'] as $paymentMethod ) {
				/**
				 * @var \StripeWPFS\PaymentMethod $paymentMethod
				 */
				if ( is_null( $existingPaymentMethod ) ) {
					if ( isset( $paymentMethod ) && isset( $paymentMethod->card ) && isset( $paymentMethod->card->fingerprint ) ) {
						if ( $paymentMethod->card->fingerprint == $paymentMethodCardFingerPrint &&
						     $paymentMethod->card->exp_year == $expiryYear &&
						     $paymentMethod->card->exp_month == $expiryMonth
						) {
							$existingPaymentMethod = $paymentMethod;
						}
					}
				}
			}
		}

		return $existingPaymentMethod;
	}

    /**
     * @param $subscriptionId string
     *
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function activateCancelledSubscription( $subscriptionId ) {
	    $subscription = $this->retrieveSubscription( $subscriptionId );

        do_action( MM_WPFS::ACTION_NAME_BEFORE_SUBSCRIPTION_ACTIVATION, $subscriptionId );

	    $subscription->cancel_at_period_end = false;
        $subscription->save();

        do_action( MM_WPFS::ACTION_NAME_AFTER_SUBSCRIPTION_ACTIVATION, $subscriptionId );
    }

    /**
     * @param $stripeSubscriptionId
     */
    private function fireBeforeSubscriptionCancellationAction( $stripeSubscriptionId ) {
        do_action( MM_WPFS::ACTION_NAME_BEFORE_SUBSCRIPTION_CANCELLATION, $stripeSubscriptionId );
    }

    /**
     * @param $stripeSubscriptionId
     */
    private function fireAfterSubscriptionCancellationAction( $stripeSubscriptionId ) {
        do_action( MM_WPFS::ACTION_NAME_AFTER_SUBSCRIPTION_CANCELLATION, $stripeSubscriptionId );
    }

    /**
     * @param $stripeCustomerId
     * @param $stripeSubscriptionId
     * @param bool $atPeriodEnd
     *
     * @return bool
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function cancelSubscription( $stripeCustomerId, $stripeSubscriptionId, $atPeriodEnd = false ) {
		if ( ! empty( $stripeSubscriptionId ) ) {
            if ( ! empty( $stripeCustomerId )) {
                $subscription = $this->retrieveSubscriptionByCustomer( $stripeCustomerId, $stripeSubscriptionId );
            } else {
                $subscription = $this->retrieveSubscription( $stripeSubscriptionId );
            }

            if ( $subscription ) {
                $this->fireBeforeSubscriptionCancellationAction( $stripeSubscriptionId );

                /** @noinspection PhpUnusedLocalVariableInspection */
                if ( $atPeriodEnd ) {
                    $cancellationResult = $this->stripe->subscriptions->update(
                        $stripeSubscriptionId,
                        array (
                            'cancel_at_period_end' => true
                        )
                    );
                } else {
                    $cancellationResult = $subscription->cancel();
                }

                $this->fireAfterSubscriptionCancellationAction( $stripeSubscriptionId );

                if ( $cancellationResult instanceof \StripeWPFS\Subscription ) {
                    return true;
                }
            }
		}

		return false;
	}

    /**
     * @param $params array
     *
     * @return \StripeWPFS\Collection
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function listSubscriptionsWithParams( $params ) {
        return $this->stripe->subscriptions->all( $params );
    }

    /**
     * @param $params array
     *
     * @return \StripeWPFS\Collection
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function listInvoicesWithParams( $params ) {
        return $this->stripe->invoices->all( $params );
    }

    /**
     * @param $params array
     *
     * @return \StripeWPFS\Collection
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function listPaymentMethodsWithParams( $params ) {
        return $this->stripe->paymentMethods->all( $params );
    }

    /**
     * @param $customerID
     * @param $subscriptionID
     *
     * @return array|\StripeWPFS\StripeObject
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	function retrieveSubscriptionByCustomer( $customerID, $subscriptionID ) {
		$cu = $this->stripe->customers->retrieve( $customerID, [ 'expand' => [ 'subscriptions' ] ] );

		return $cu->subscriptions->retrieve( $subscriptionID );
	}

	/**
	 * @param $subscriptionID
	 *
	 * @return \StripeWPFS\Subscription
	 * @throws \StripeWPFS\Exception\ApiErrorException
	 */
	function retrieveSubscription( $subscriptionID ) {
		return $this->stripe->subscriptions->retrieve( $subscriptionID );
	}

    /**
     * @param $subscriptionId string
     * @param $params array
     *
     * @return \StripeWPFS\Subscription
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function retrieveSubscriptionWithParams($subscriptionId, $params ) {
	    $stripeSubscription = $this->stripe->subscriptions->retrieve( $subscriptionId, $params );

	    return $stripeSubscription;
    }

    /**
     * @param $stripeSubscriptionId
     * @param $newPlanId
     * @param $newQuantity
     */
    protected function fireBeforeSubscriptionUpdateAction($stripeSubscriptionId, $newPlanId, $newQuantity) {
        $params = [
            'stripeSubscriptionId' => $stripeSubscriptionId,
            'planId' => $newPlanId,
            'quantity' => $newQuantity
        ];

        do_action(MM_WPFS::ACTION_NAME_BEFORE_SUBSCRIPTION_UPDATE, $params);
    }

    /**
     * @param $stripeSubscriptionId
     * @param $newPlanId
     * @param $newQuantity
     */
    protected function fireAfterSubscriptionUpdateAction($stripeSubscriptionId, $newPlanId, $newQuantity) {
        $params = [
            'stripeSubscriptionId' => $stripeSubscriptionId,
            'planId' => $newPlanId,
            'quantity' => $newQuantity
        ];

        do_action(MM_WPFS::ACTION_NAME_AFTER_SUBSCRIPTION_UPDATE, $params);
    }

    /**
	 * @param $stripeCustomerId
	 * @param $stripeSubscriptionId
	 * @param $planId
	 * @param $newPlanQuantity
	 *
	 * @return bool
	 * @throws \StripeWPFS\Exception\ApiErrorException
	 */
	public function updateSubscriptionPlanAndQuantity( $stripeCustomerId, $stripeSubscriptionId, $planId, $planQuantity = null ) {
        if ( ! empty( $stripeCustomerId ) && ! empty( $stripeSubscriptionId ) && ! empty( $planId ) ) {
            /* @var $subscription \StripeWPFS\Subscription */
            $subscription = $this->retrieveSubscriptionByCustomer( $stripeCustomerId, $stripeSubscriptionId );

            if ( ! empty( $planQuantity ) && is_numeric( $planQuantity ) ) {
                $newPlanQuantity = intval( $planQuantity );
            } else {
                $newPlanQuantity = $subscription->quantity;
            }

            if ( isset( $subscription ) ) {
                $parameters    = array();
                $performUpdate = false;
                $planUpdated   = false;
                // tnagy update subscription plan
                if ( $subscription->plan != $planId ) {
                    $parameters    = array_merge( $parameters, array( 'plan' => $planId ) );
                    $planUpdated   = true;
                    $performUpdate = true;
                }
                // tnagy update subscription quantity
                $allowMultipleSubscriptions = false;
                if ( isset( $subscription->metadata ) && isset( $subscription->metadata->allow_multiple_subscriptions ) ) {
                    $allowMultipleSubscriptions = boolval( $subscription->metadata->allow_multiple_subscriptions );
                }
                $minimumQuantity = MM_WPFS_Utils::getMinimumPlanQuantityOfSubscription( $subscription );
                $maximumQuantity = MM_WPFS_Utils::getMaximumPlanQuantityOfSubscription( $subscription );
                if ( $allowMultipleSubscriptions ) {
                    if ( $minimumQuantity > 0 && $newPlanQuantity < $minimumQuantity ) {
                        throw new Exception( sprintf(
                        /* translators: Error message displayed when subscriber tries to set a quantity for a subscription which is beyond allowed value */
                            __( "Subscription quantity '%d' is not allowed for this subscription!", 'wp-full-stripe' ), $newPlanQuantity ) );
                    }
                    if ( $maximumQuantity > 0 && $newPlanQuantity > $maximumQuantity ) {
                        throw new Exception( sprintf(
                        /* translators: Error message displayed when subscriber tries to set a quantity for a subscription which is over allowed value */
                            __( "Subscription quantity '%d' is not allowed for this subscription!", 'wp-full-stripe' ), $newPlanQuantity ) );
                    }
                    if ( $subscription->quantity != intval( $newPlanQuantity ) || $planUpdated ) {
                        $parameters    = array_merge( $parameters, array( 'quantity' => $newPlanQuantity ) );
                        $performUpdate = true;
                    }
                } elseif ( $newPlanQuantity > 1 ) {
                    throw new Exception(
                    /* translators: Error message displayed when subscriber tries to set a quantity for a
                     * subscription where quantity other than one is not allowed.
                     */
                        __( 'Quantity update is not allowed for this subscription!', 'wp-full-stripe' ) );
                }
            } else {
                throw new Exception( sprintf(
                /* translators: Error message displayed when a subscription is not found.
                 * p1: Subscription identifier
                 */
                    __( "Subscription '%s' not found!", 'wp-full-stripe' ), $stripeSubscriptionId ) );
            }
            if ( $performUpdate ) {
                $this->fireBeforeSubscriptionUpdateAction( $stripeSubscriptionId, $planId, $newPlanQuantity );
                $this->stripe->subscriptions->update( $stripeSubscriptionId, $parameters );
                $this->fireAfterSubscriptionUpdateAction( $stripeSubscriptionId, $planId, $newPlanQuantity );
            }

            return true;
        } else {
            // This is an internal error, no need to localize it
            throw new Exception( 'Invalid parameters!' );
        }
	}

	function getProducts($associativeArray = false, $productIds = null ) {
		$products = array();
		try {

			$params = array(
				'limit'     => 100,
				'include[]' => 'total_count'
			);
			if ( ! is_null( $productIds ) && count( $productIds ) > 0 ) {
				$params['ids'] = $productIds;
			}
			$params            = array( 'active' => 'false', 'limit' => 100 );
			$productCollection = $this->stripe->products->all( $params );
			foreach ( $productCollection->autoPagingIterator() as $product ) {
				if ( $associativeArray ) {
					$products[ $product->id ] = $product;
				} else {
					array_push( $products, $product );
				}
			}

			// MM_WPFS_Utils::log( 'params=' . print_r( $params, true ) );
			// MM_WPFS_Utils::log( 'productCollection=' . print_r( $productCollection, true ) );

		} catch ( Exception $ex ) {
            $this->logger->error(__FUNCTION__, 'Error while getting products', $ex );

			$products = array();
		}

		return $products;
	}

    /**
     * @param $chargeId
     *
     * @return \StripeWPFS\Charge
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	function captureCharge($chargeId ) {
        $charge = $this->stripe->charges->retrieve( $chargeId );
        if ( $charge instanceof \StripeWPFS\Charge ) {
            /* @var $charge \StripeWPFS\Charge */
			return $charge->capture();
		}

		return $charge;
	}

    /**
     * @param $paymentIntentId
     *
     * @return \StripeWPFS\PaymentIntent
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function capturePaymentIntent( $paymentIntentId ) {
		$paymentIntent = $this->stripe->paymentIntents->retrieve( $paymentIntentId );
		if ( $paymentIntent instanceof \StripeWPFS\PaymentIntent ) {
            /* @var $charge \StripeWPFS\PaymentIntent */
			return $paymentIntent->capture();
		}

		return $paymentIntent;
	}

    /**
     * @param $chargeId
     *
     * @return \StripeWPFS\Refund
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	function refundCharge( $chargeId ) {
        $refund = $this->stripe->refunds->create( [
            'charge' => $chargeId
        ] );

		return $refund;
	}

    /**
     * @param $paymentIntentId
     *
     * @return \StripeWPFS\PaymentIntent|\StripeWPFS\Refund
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function cancelOrRefundPaymentIntent( $paymentIntentId ) {
		$paymentIntent = $this->stripe->paymentIntents->retrieve( $paymentIntentId );
		if ( $paymentIntent instanceof \StripeWPFS\PaymentIntent ) {
		    /* @var $paymentIntent \StripeWPFS\PaymentIntent */
			if (
				\StripeWPFS\PaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD === $paymentIntent->status
				|| \StripeWPFS\PaymentIntent::STATUS_REQUIRES_CAPTURE === $paymentIntent->status
				|| \StripeWPFS\PaymentIntent::STATUS_REQUIRES_CONFIRMATION === $paymentIntent->status
				|| \StripeWPFS\PaymentIntent::STATUS_REQUIRES_ACTION === $paymentIntent->status
			) {
				return $paymentIntent->cancel();
			} elseif (
				\StripeWPFS\PaymentIntent::STATUS_PROCESSING === $paymentIntent->status
				|| \StripeWPFS\PaymentIntent::STATUS_SUCCEEDED === $paymentIntent->status
			) {
				/** @var \StripeWPFS\Charge $lastCharge */
				$lastCharge = $paymentIntent->charges->data[0];

				return $this->refundCharge( $lastCharge->id );
			}
		}

		return $paymentIntent;
	}

	public function updateCustomerBillingAddressByPaymentMethod( $stripeCustomer, $stripePaymentMethod ) {
		if ( $stripeCustomer instanceof \StripeWPFS\Customer && $stripePaymentMethod instanceof \StripeWPFS\PaymentMethod ) {
			$address = $this->fetchBillingAddressFromPaymentMethod( $stripePaymentMethod );
			if ( count( $address ) > 0 ) {
				$this->stripe->customers->update(
					$stripeCustomer->id,
					array(
						'address' => $address
					)
				);
			}
		}
	}

	/**
	 * @param $stripePaymentMethod
	 *
	 * @return array
	 */
	private function fetchBillingAddressFromPaymentMethod( $stripePaymentMethod ) {
		$address = array();
		if (
			isset( $stripePaymentMethod->billing_details )
			&& isset( $stripePaymentMethod->billing_details->address )
			&& $this->isRealBillingAddressInPaymentMethod( $stripePaymentMethod )
		) {
			$billingDetailsAddress = $stripePaymentMethod->billing_details->address;
			if ( isset( $billingDetailsAddress->city ) ) {
				$address['city'] = $billingDetailsAddress->city;

			}
			if ( isset( $billingDetailsAddress->country ) ) {
				$address['country'] = $billingDetailsAddress->country;

			}
			if ( isset( $billingDetailsAddress->line1 ) ) {
				$address['line1'] = $billingDetailsAddress->line1;

			}
			if ( isset( $billingDetailsAddress->line2 ) ) {
				$address['line2'] = $billingDetailsAddress->line2;

			}
			if ( isset( $billingDetailsAddress->postal_code ) ) {
				$address['postal_code'] = $billingDetailsAddress->postal_code;

			}
			if ( isset( $billingDetailsAddress->state ) ) {
				$address['state'] = $billingDetailsAddress->state;

				return $address;

			}

			return $address;
		}

		return $address;
	}

	private function isRealBillingAddressInPaymentMethod( $stripePaymentMethod ) {
		$res = false;

		$billingDetailsAddress = $stripePaymentMethod->billing_details->address;
		if ( ! empty( $billingDetailsAddress->city )
		     && ! empty( $billingDetailsAddress->country )
		     && ! empty( $billingDetailsAddress->line1 )
		) {
			$res = true;
		}

		return $res;
	}

	public function updateCustomerShippingAddressByPaymentMethod( $stripeCustomer, $stripePaymentMethod ) {
		if ( $stripeCustomer instanceof \StripeWPFS\Customer && $stripePaymentMethod instanceof \StripeWPFS\PaymentMethod ) {
			$address = $this->fetchBillingAddressFromPaymentMethod( $stripePaymentMethod );
			if ( count( $address ) > 0 ) {
				$this->stripe->customers->update(
					$stripeCustomer->id,
					array(
						'shipping' => array(
							'address' => $address
						)
					)
				);
			}
		}
	}

    /**
     * @param $parameters array
     * 
     * @return \StripeWPFS\Checkout\Session
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function createCheckoutSession( $parameters ) {
        return $this->stripe->checkout->sessions->create( apply_filters( 'fullstripe_checkout_session_parameters', $parameters ));
    }

	/**
	 * @param string $stripeInvoiceId
	 */
	public function payInvoiceOutOfBand( $stripeInvoiceId ) {
		return $this->stripe->invoices->pay( $stripeInvoiceId, array( 'paid_out_of_band' => true )) ;
	}

    /**
     * @param $stripeInvoice
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
	public function finalizeInvoice( $stripeInvoiceId ) {
        return $this->stripe->invoices->finalizeInvoice( $stripeInvoiceId );
    }

    public function getUpcomingInvoice( $invoiceParams ) {
	    return $this->stripe->invoices->upcoming( $invoiceParams );
    }

    /**
     * @return \StripeWPFS\StripeClient
     */
    public function getStripeClient() {
	    return $this->stripe;
    }

	/**
	 * @param $priceIds array
	 *
	 * @return array
	 */
	public function retrieveProductsByPriceIds( $priceIds ) {
		$products = array();
		foreach ( $priceIds as $priceId ) {
			$price = $this->stripe->prices->retrieve( $priceId, [ 'expand' => [ 'product' ] ] );
			array_push( $products, $price->product );
		}

		return $products;
	}

	/**
	 * @param $priceIds array
	 *
	 * @return array
	 */
	public function retrieveProductIdsByPriceIds( $priceIds ) {
		$products   = $this->retrieveProductsByPriceIds( $priceIds );
		$productIds = array_map(
			function ( $o ) {
				return $o->id;
			},
			$products
		);

		return $productIds;
	}


    /**
     * @param $priceId
     *
     * @return \StripeWPFS\Price
     */
    public function retrievePriceWithProductExpanded( $priceId ) {
       return $this->stripe->prices->retrieve( $priceId, array( "expand" => array( "product" ) ));
    }

    /**
     * @param $taxRateId
     * @return \StripeWPFS\TaxRate
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function retrieveTaxRate( $taxRateId ) {
        return $this->stripe->taxRates->retrieve( $taxRateId );
    }

    /**
     * @param $stripeCustomerId
     * @param $taxIdType
     * @param $taxId
     * @return \StripeWPFS\TaxId
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function createTaxIdForCustomer( $stripeCustomerId, $taxIdType, $taxId ) {
        return $this->stripe->customers->createTaxId(
            $stripeCustomerId,
            [
                'type'  => $taxIdType,
                'value' => $taxId
            ]
        );
    }

    /**
     * @param $stripeCustomerId
     * @return \StripeWPFS\Collection
     * @throws \StripeWPFS\Exception\ApiErrorException
     */
    public function getTaxIdsForCustomer( $stripeCustomerId ) {
        return $this->stripe->customers->allTaxIds(
            $stripeCustomerId,
            []
        );
    }
}