Files
shuffle_and_skirmish_website/wp-content/plugins/woocommerce-payments/includes/class-wc-payments-customer-service.php
2025-11-24 21:33:55 +00:00

581 lines
20 KiB
PHP

<?php
/**
* Class WC_Payments_Customer
*
* @package WooCommerce\Payments
*/
use WCPay\Database_Cache;
use WCPay\Exceptions\API_Exception;
use WCPay\Logger;
use WCPay\Constants\Payment_Method;
defined( 'ABSPATH' ) || exit;
/**
* Class handling any customer functionality
*/
class WC_Payments_Customer_Service {
/**
* Deprecated Stripe customer ID option.
*
* This option was used to store the customer_id in a WC_User options before we decoupled live and test customers.
*/
const DEPRECATED_WCPAY_CUSTOMER_ID_OPTION = '_wcpay_customer_id';
/**
* Live Stripe customer ID option.
*
* This option is used to store new live mode customers in a WC_User options. Customers stored in the deprecated
* option are migrated to this one.
*/
const WCPAY_LIVE_CUSTOMER_ID_OPTION = '_wcpay_customer_id_live';
/**
* Test Stripe customer ID option.
*
* This option is used to store new test mode customer IDs in a WC_User options.
*/
const WCPAY_TEST_CUSTOMER_ID_OPTION = '_wcpay_customer_id_test';
/**
* Key used to store customer id for non logged in users in WooCommerce Session.
*/
const CUSTOMER_ID_SESSION_KEY = 'wcpay_customer_id';
/**
* Client for making requests to the WooCommerce Payments API
*
* @var WC_Payments_API_Client
*/
private $payments_api_client;
/**
* WC_Payments_Account instance to get information about the account
*
* @var WC_Payments_Account
*/
private $account;
/**
* Database_Cache instance to get information about the account
*
* @var Database_Cache
*/
private $database_cache;
/**
* WC_Payments_Session_Service instance for working with session information
*
* @var WC_Payments_Session_Service
*/
private $session_service;
/**
* WC_Payments_Order_Service instance
*
* @var WC_Payments_Order_Service
*/
private $order_service;
/**
* Class constructor
*
* @param WC_Payments_API_Client $payments_api_client Payments API client.
* @param WC_Payments_Account $account WC_Payments_Account instance.
* @param Database_Cache $database_cache Database_Cache instance.
* @param WC_Payments_Session_Service $session_service Session Service class instance.
* @param WC_Payments_Order_Service $order_service Order Service class instance.
*/
public function __construct(
WC_Payments_API_Client $payments_api_client,
WC_Payments_Account $account,
Database_Cache $database_cache,
WC_Payments_Session_Service $session_service,
WC_Payments_Order_Service $order_service
) {
$this->payments_api_client = $payments_api_client;
$this->account = $account;
$this->database_cache = $database_cache;
$this->session_service = $session_service;
$this->order_service = $order_service;
}
/**
* Initialize hooks
*/
public function init_hooks() {
/*
* Adds the WooCommerce Payments customer ID found in the user session
* to the WordPress user as metadata.
*
* This is helpful in scenarios where the shopper begins the checkout flow
* logged out (i.e. guest checkout) and a user account is created for them
* during checkout.
*
* This occurs when a user account is necessary for checkout, e.g. when the shopper
* purchases a subscription product.
*/
add_action( 'woocommerce_created_customer', [ $this, 'add_customer_id_to_user' ] );
}
/**
* Get WCPay customer ID for the given WordPress user ID
*
* @param int|null $user_id The user ID to look for a customer ID with.
*
* @return string|null WCPay customer ID or null if not found.
*/
public function get_customer_id_by_user_id( $user_id ) {
// User ID might be 0 if fetched from a WP_User instance for a user who isn't logged in.
if ( null === $user_id || 0 === $user_id ) {
// Try to retrieve the customer id from the session if stored previously.
$customer_id = WC()->session ? WC()->session->get( self::CUSTOMER_ID_SESSION_KEY ) : null;
return is_string( $customer_id ) ? $customer_id : null;
}
$customer_id = get_user_option( $this->get_customer_id_option(), $user_id );
// If customer_id is false it could mean that it hasn't been migrated from the deprecated key.
if ( false === $customer_id ) {
$this->maybe_migrate_deprecated_customer( $user_id );
// Customer might've been migrated in maybe_migrate_deprecated_customer, so we need to fetch it again.
$customer_id = get_user_option( $this->get_customer_id_option(), $user_id );
}
return $customer_id ? $customer_id : null;
}
/**
* Create a customer and associate it with a WordPress user.
*
* @param WP_User|null $user User to create a customer for.
* @param array $customer_data Customer data.
*
* @return string The created customer's ID
*
* @throws API_Exception Error creating customer.
*/
public function create_customer_for_user( ?WP_User $user, array $customer_data = [] ): string {
// Include the session ID for the user.
$customer_data['session_id'] = $this->session_service->get_sift_session_id() ?? null;
// Create a customer on the WCPay server.
$customer_id = $this->payments_api_client->create_customer( $customer_data );
if ( $user instanceof WP_User && $user->ID > 0 ) {
$this->update_user_customer_id( $user->ID, $customer_id );
}
if ( isset( WC()->session ) ) {
// Save the customer id in the session for non logged in users to reuse it in payments.
WC()->session->set( self::CUSTOMER_ID_SESSION_KEY, $customer_id );
}
return $customer_id;
}
/**
* Manages customer details held on WCPay server for WordPress user associated with an order.
*
* @param int|null $user_id ID of the WP user to associate with the customer.
* @param WC_Order $order Woo Order.
*
* @return string WooPayments customer ID.
* @throws API_Exception Throws when server API request fails.
*/
public function get_or_create_customer_id_from_order( ?int $user_id, WC_Order $order ): string {
// Determine the customer making the payment, create one if we don't have one already.
$customer_id = $this->get_customer_id_by_user_id( $user_id );
$customer_data = self::map_customer_data( $order, new WC_Customer( $user_id ?? 0 ) );
$user = null === $user_id ? null : get_user_by( 'id', $user_id );
if ( null !== $customer_id ) {
$this->update_customer_for_user( $customer_id, $user, $customer_data );
return $customer_id;
}
return $this->create_customer_for_user( $user, $customer_data );
}
/**
* Update the customer details held on the WCPay server associated with the given WordPress user.
*
* @param string $customer_id WCPay customer ID.
* @param WP_User|null $user WordPress user.
* @param array $customer_data Customer data.
*
* @return string The updated customer's ID. Can be different to the ID parameter if the customer was re-created.
*
* @throws API_Exception Error updating the customer.
*/
public function update_customer_for_user( string $customer_id, ?WP_User $user, array $customer_data ): string {
try {
// Update the customer on the WCPay server.
$this->payments_api_client->update_customer(
$customer_id,
$customer_data
);
// We successfully updated the existing customer, so return the passed in ID unchanged.
return $customer_id;
} catch ( API_Exception $e ) {
// If we failed to find the customer we wanted to update, then create a new customer and associate it to the
// current user instead. This might happen if the customer was deleted from the server, the linked WCPay
// account was changed, or if users were imported from another site.
if ( $e->get_error_code() === 'resource_missing' ) {
// Create a new customer to associate with this user. We'll return the new customer ID.
return $this->recreate_customer( $user, $customer_data );
}
// For any other type of exception, just re-throw.
throw $e;
}
}
/**
* Sets a payment method as default for a customer.
*
* @param string $customer_id The customer ID.
* @param string $payment_method_id The payment method ID.
*/
public function set_default_payment_method_for_customer( $customer_id, $payment_method_id ) {
$this->payments_api_client->update_customer(
$customer_id,
[
'invoice_settings' => [
'default_payment_method' => $payment_method_id,
],
]
);
}
/**
* Gets all payment methods for a customer.
*
* @param string $customer_id The customer ID.
* @param string $type Type of payment methods to fetch.
*
* @throws API_Exception We only handle 'resource_missing' code types and rethrow anything else.
*/
public function get_payment_methods_for_customer( $customer_id, $type = 'card' ) {
if ( ! $customer_id ) {
return [];
}
$cache_payment_methods = ! WC_Payments::is_network_saved_cards_enabled();
$cache_key = Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_' . $type;
if ( $cache_payment_methods ) {
$payment_methods = $this->database_cache->get( $cache_key );
if ( is_array( $payment_methods ) ) {
return $payment_methods;
}
}
try {
$payment_methods = $this->payments_api_client->get_payment_methods( $customer_id, $type )['data'];
if ( $cache_payment_methods ) {
$this->database_cache->add( $cache_key, $payment_methods );
}
return $payment_methods;
} catch ( API_Exception $e ) {
// If we failed to find the payment methods, we can simply return empty payment methods as this customer
// will be recreated when the user successfully adds a payment method.
if ( $e->get_error_code() === 'resource_missing' ) {
return [];
}
// Rethrow for error codes we don't care about in this function.
throw $e;
}
}
/**
* Updates a customer payment method.
*
* @param string $payment_method_id The payment method ID.
* @param WC_Order $order Order to be used on the update.
*/
public function update_payment_method_with_billing_details_from_order( $payment_method_id, $order ) {
$billing_details = $this->order_service->get_billing_data_from_order( $order );
if ( ! empty( $billing_details ) ) {
$this->payments_api_client->update_payment_method(
$payment_method_id,
[
'billing_details' => $billing_details,
]
);
}
}
/**
* Clear payment methods cache for a user.
*
* @param int $user_id WC user ID.
*/
public function clear_cached_payment_methods_for_user( $user_id ) {
if ( WC_Payments::is_network_saved_cards_enabled() ) {
return; // No need to do anything, payment methods will never be cached in this case.
}
$retrievable_payment_method_types = [ Payment_Method::CARD, Payment_Method::LINK, Payment_Method::SEPA ];
$customer_id = $this->get_customer_id_by_user_id( $user_id );
foreach ( $retrievable_payment_method_types as $type ) {
$this->database_cache->delete( Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_' . $type );
}
}
/**
* Given a WC_Order or WC_Customer, returns an array representing a Stripe customer object.
* At least one parameter has to not be null.
*
* @param WC_Order $wc_order The Woo order to parse.
* @param WC_Customer $wc_customer The Woo customer to parse.
*
* @return array Customer data.
*/
public static function map_customer_data( ?WC_Order $wc_order = null, ?WC_Customer $wc_customer = null ): array {
if ( null === $wc_customer && null === $wc_order ) {
return [];
}
// Where available, the order data takes precedence over the customer.
$object_to_parse = $wc_order ?? $wc_customer;
$name = $object_to_parse->get_billing_first_name() . ' ' . $object_to_parse->get_billing_last_name();
$description = '';
if ( null !== $wc_customer && ! empty( $wc_customer->get_username() ) ) {
// We have a logged in user, so add their username to the customer description.
// translators: %1$s Name, %2$s Username.
$description = sprintf( __( 'Name: %1$s, Username: %2$s', 'woocommerce-payments' ), $name, $wc_customer->get_username() );
} else {
// Current user is not logged in.
// translators: %1$s Name.
$description = sprintf( __( 'Name: %1$s, Guest', 'woocommerce-payments' ), $name );
}
$data = [
'name' => $name,
'description' => $description,
'email' => $object_to_parse->get_billing_email(),
'phone' => $object_to_parse->get_billing_phone(),
'address' => [
'line1' => $object_to_parse->get_billing_address_1(),
'line2' => $object_to_parse->get_billing_address_2(),
'postal_code' => $object_to_parse->get_billing_postcode(),
'city' => $object_to_parse->get_billing_city(),
'state' => $object_to_parse->get_billing_state(),
'country' => $object_to_parse->get_billing_country(),
],
];
if ( ! empty( $object_to_parse->get_shipping_postcode() ) ) {
$data['shipping'] = [
'name' => $object_to_parse->get_shipping_first_name() . ' ' . $object_to_parse->get_shipping_last_name(),
'address' => [
'line1' => $object_to_parse->get_shipping_address_1(),
'line2' => $object_to_parse->get_shipping_address_2(),
'postal_code' => $object_to_parse->get_shipping_postcode(),
'city' => $object_to_parse->get_shipping_city(),
'state' => $object_to_parse->get_shipping_state(),
'country' => $object_to_parse->get_shipping_country(),
],
];
}
return $data;
}
/**
* Delete all saved payment methods that are stored inside the database cache driver.
*
* @return void
*/
public function delete_cached_payment_methods() {
$this->database_cache->delete_by_prefix( Database_Cache::PAYMENT_METHODS_KEY_PREFIX );
}
/**
* Recreates the customer for this user.
*
* @param WP_User|null $user User to recreate a customer for.
* @param array $customer_data Customer data.
*
* @return string The newly created customer's ID
*
* @throws API_Exception Error creating customer.
*/
private function recreate_customer( ?WP_User $user, array $customer_data ): string {
if ( $user instanceof WP_User && $user->ID > 0 ) {
$result = delete_user_option( $user->ID, $this->get_customer_id_option() );
if ( ! $result ) {
// Log the error, but continue since we'll be trying to update this option in create_customer.
Logger::error( 'Failed to delete old customer ID for user ' . $user->ID );
}
}
return $this->create_customer_for_user( $user, $customer_data );
}
/**
* Returns the name of the customer option meta, taking test mode into account.
*
* @return string The customer ID option name.
*/
private function get_customer_id_option(): string {
return WC_Payments::mode()->is_test()
? self::WCPAY_TEST_CUSTOMER_ID_OPTION
: self::WCPAY_LIVE_CUSTOMER_ID_OPTION;
}
/**
* Migrate any customer ID that might be in the DEPRECATED_WCPAY_CUSTOMER_ID_OPTION.
*
* @param int $user_id The user ID to look for a customer ID with.
*/
private function maybe_migrate_deprecated_customer( $user_id ) {
$customer_id = get_user_option( self::DEPRECATED_WCPAY_CUSTOMER_ID_OPTION, $user_id );
if ( false !== $customer_id ) {
// A customer was found in the deprecated key. Migrate it to the appropriate one and delete the old meta.
// If an account is live mode, we optimistically assume that the customer is live mode, to avoid losing
// live mode customer data. If the account is not live mode, it can only have test mode objects, so we
// can safely migrate them to the test key.
// If is_live cannot be determined, default it to true to avoid considering a live account as test.
$account_is_live = null === $this->account->get_is_live() || $this->account->get_is_live();
$customer_option_id = $account_is_live
? self::WCPAY_LIVE_CUSTOMER_ID_OPTION
: self::WCPAY_TEST_CUSTOMER_ID_OPTION;
if ( update_user_option( $user_id, $customer_option_id, $customer_id ) ) {
delete_user_option( $user_id, self::DEPRECATED_WCPAY_CUSTOMER_ID_OPTION );
} else {
Logger::error( 'Failed to store new customer ID for user ' . $user_id . '; legacy customer was kept.' );
}
}
}
/**
* Get the WCPay customer ID associated with an order, or create one if none found.
*
* @param WC_Order $order WC Order object.
*
* @return string|null WCPay customer ID.
* @throws API_Exception If there's an error creating customer.
*/
public function get_customer_id_for_order( $order ) {
$customer_id = null;
$user = $order->get_user();
if ( false !== $user ) {
// Determine the customer making the payment, create one if we don't have one already.
$customer_id = $this->get_customer_id_by_user_id( $user->ID );
if ( null === $customer_id ) {
$customer_data = self::map_customer_data( $order, new WC_Customer( $user->ID ) );
$customer_id = $this->create_customer_for_user( $user, $customer_data );
}
}
return $customer_id;
}
/**
* Updates the given user with the given WooCommerce Payments
* customer ID.
*
* @param int $user_id The WordPress user ID.
* @param string $customer_id The WooCommerce Payments customer ID.
*/
public function update_user_customer_id( int $user_id, string $customer_id ) {
$global = WC_Payments::is_network_saved_cards_enabled();
$result = update_user_option( $user_id, $this->get_customer_id_option(), $customer_id, $global );
if ( ! $result ) {
Logger::error( 'Failed to update customer ID for user ' . $user_id );
}
}
/**
* Adds the WooCommerce Payments customer ID found in the user session
* to the WordPress user as metadata.
*
* @param int $user_id The WordPress user ID.
*/
public function add_customer_id_to_user( $user_id ) {
// Not processing a checkout, bail.
if (
! ( defined( 'WOOCOMMERCE_CHECKOUT' ) && WOOCOMMERCE_CHECKOUT ) &&
! ( function_exists( 'wcs_is_checkout_blocks_api_request' ) && wcs_is_checkout_blocks_api_request( 'v1/checkout' ) )
) {
return;
}
// Retrieve the WooCommerce Payments customer ID from the user session.
$customer_id = WC()->session ? WC()->session->get( self::CUSTOMER_ID_SESSION_KEY ) : null;
if ( ! $customer_id ) {
return;
}
$this->update_user_customer_id( $user_id, $customer_id );
}
/**
* Prepares customer data to be used on 'Pay for Order' or 'Add Payment Method' pages.
* Customer data is retrieved from order when on Pay for Order.
* Customer data is retrieved from customer when on 'Add Payment Method'.
*
* @return array|null An array with customer data or nothing.
*/
public function get_prepared_customer_data() {
if ( ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return null;
}
global $wp;
$user_email = '';
$firstname = '';
$lastname = '';
$billing_country = '';
$address = null;
if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_id = absint( $wp->query_vars['order-pay'] );
$order = wc_get_order( $order_id );
if ( is_a( $order, 'WC_Order' ) ) {
$firstname = $order->get_billing_first_name();
$lastname = $order->get_billing_last_name();
$user_email = $order->get_billing_email();
$billing_country = $order->get_billing_country();
$address = [
'city' => $order->get_billing_city(),
'country' => $order->get_billing_country(),
'line1' => $order->get_billing_address_1(),
'line2' => $order->get_billing_address_2(),
'postal_code' => $order->get_billing_postcode(),
'state' => $order->get_billing_state(),
];
}
}
if ( is_add_payment_method_page() ) {
$user = wp_get_current_user();
if ( $user->ID ) {
$firstname = $user->user_firstname;
$lastname = $user->user_lastname;
$user_email = get_user_meta( $user->ID, 'billing_email', true );
$user_email = ! empty( $user_email ) ? $user_email : $user->user_email;
$billing_country = get_user_meta( $user->ID, 'billing_country', true );
}
}
return [
'name' => $firstname . ' ' . $lastname,
'email' => $user_email,
'billing_country' => $billing_country,
'address' => $address,
];
}
}