Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add customer payment methods and PPCP chargeback reporting #25

Merged
merged 35 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e953ca1
Add customer payment methods
chrismccluskey Dec 3, 2024
3d72f43
Use singular payment method and remove empty values
chrismccluskey Dec 3, 2024
c7304c7
Return an empty array so that it passes validation
chrismccluskey Dec 3, 2024
cf08f8f
Delint
chrismccluskey Dec 3, 2024
2397009
Allow enumeration
chrismccluskey Dec 3, 2024
58f98f7
Try using get_current_user_id when sourcing user_id
chrismccluskey Dec 3, 2024
c60b05b
Remove payment_method if empty
chrismccluskey Dec 4, 2024
e15f791
Try to source the user id from order if returned user is admin
chrismccluskey Dec 4, 2024
8d46b3e
Use user id from order when is_admin
chrismccluskey Dec 4, 2024
55a00f3
Try using session id in this case
chrismccluskey Dec 4, 2024
c0acb38
Add debugging for woocommerce-payments integration
chrismccluskey Dec 5, 2024
6ca5823
Add more temporary debug statements
chrismccluskey Dec 5, 2024
53d3826
Add more temp debug logging
chrismccluskey Dec 5, 2024
5a304b5
Fix debugging bug
chrismccluskey Dec 5, 2024
8ea0b10
Log events as they are sent to Sift
chrismccluskey Dec 5, 2024
44702cb
Try to find missing event with temp debug logs
chrismccluskey Dec 5, 2024
a3a6777
Use user id from order if it isn't otherwise available
chrismccluskey Dec 5, 2024
35b789d
Add debugging logs for getting customer payment methods
chrismccluskey Dec 5, 2024
93304f5
Use array filter instead of array reduce
chrismccluskey Dec 5, 2024
318240e
Use array_reduce correctly
chrismccluskey Dec 5, 2024
84b2a67
Seed the array_reduce function correctly
chrismccluskey Dec 5, 2024
858a913
Cache customer orders
chrismccluskey Dec 5, 2024
b7ef122
Remove debug logs
chrismccluskey Dec 5, 2024
92869e4
Remove log from woocommerce-payments integration
chrismccluskey Dec 5, 2024
7216251
Promote order cache to singleton
chrismccluskey Dec 5, 2024
8459132
Get payments from purchase-unit in order instead of purchase unit fro…
chrismccluskey Dec 6, 2024
9230b1a
Get name from objects
chrismccluskey Dec 6, 2024
ad62efc
Fix typo
chrismccluskey Dec 6, 2024
9a8843d
Use correct property from object
chrismccluskey Dec 6, 2024
0c51c2e
Remove use of session unique id for user_id
chrismccluskey Dec 6, 2024
8192963
Add chargeback event to PPCP
chrismccluskey Dec 6, 2024
4d89695
Remove unused variable
chrismccluskey Dec 6, 2024
e95aebb
Prevent errors
chrismccluskey Dec 6, 2024
f375dd6
Remove action for creating chargeback with ppcp gateway and address f…
chrismccluskey Dec 9, 2024
a4edc85
Delint
chrismccluskey Dec 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 65 additions & 6 deletions src/inc/payment-gateways/ppcp-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use WooCommerce\PayPalCommerce\PPCP;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use Sift_For_WooCommerce\Sift_Events\Events;

add_filter( 'sift_for_woocommerce_ppcp-gateway_payment_gateway_string', fn() => '$paypal' );

Expand Down Expand Up @@ -44,13 +45,71 @@ function get_from_order( $value, \WC_Order $order ) {
add_filter( 'sift_for_woocommerce_ppcp-gateway_payment_type_string', fn( $value, $ppcp_gateway_payment_type ) => 'ppcp' === $ppcp_gateway_payment_type ? '$third_party_processor' : $value );

add_filter( 'sift_for_woocommerce_ppcp-gateway_card_last4', fn( $value, $ppcp_data ) => $ppcp_data['wc_order']?->get_meta_data( PayPalGateway::FRAUD_RESULT_META_KEY )['card_last_digits'] ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_avs_result_code', fn( $value, $ppcp_data ) => $ppcp_data['purchase-unit']?->payments()?->authorizations()[0]?->fraud_processor_response()->avs_code() ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_cvv_result_code', fn( $value, $ppcp_data ) => $ppcp_data['purchase-unit']?->payments()?->authorizations()[0]?->fraud_processor_response()->cvv_code() ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_verification_status', fn( $value, $ppcp_data ) => $ppcp_data['purchase-unit']?->payments()?->authorizations()[0]?->status() ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_decline_reason_code', fn( $value, $ppcp_data ) => $ppcp_data['purchase-unit']?->payments()?->authorizations()[0]?->to_array()['reason_code'] ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_avs_result_code', fn( $value, $ppcp_data ) => $ppcp_data['order']?->purchase_units()[0]?->payments()?->authorizations()[0]?->fraud_processor_response()->avs_code() ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_cvv_result_code', fn( $value, $ppcp_data ) => $ppcp_data['order']?->purchase_units()[0]?->payments()?->authorizations()[0]?->fraud_processor_response()->cvv_code() ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_verification_status', fn( $value, $ppcp_data ) => $ppcp_data['order']?->purchase_units()[0]?->payments()?->authorizations()[0]?->status()->name() ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_decline_reason_code', fn( $value, $ppcp_data ) => $ppcp_data['order']?->purchase_units()[0]?->payments()?->authorizations()[0]?->to_array()['reason_code'] ?? $value, 10, 2 );

add_filter( 'sift_for_woocommerce_ppcp-gateway_paypal_payer_id', fn( $value, $ppcp_data ) => $ppcp_data['order']?->payer()?->payer_id() ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_paypal_payer_email', fn( $value, $ppcp_data ) => $ppcp_data['order']?->payer()?->email_address() ?? $value, 10, 2 );

add_filter( 'sift_for_woocommerce_ppcp-gateway_paypal_protection_eligibility', fn( $value, $ppcp_data ) => $ppcp_data['purchase-unit']?->payments()?->captures()[0]->seller_protection() ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_paypal_payment_status', fn( $value, $ppcp_data ) => $ppcp_data['purchase-unit']?->payments()?->captures()[0]->status()->name() ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_paypal_protection_eligibility', fn( $value, $ppcp_data ) => $ppcp_data['order']?->purchase_units()[0]?->payments()?->captures()[0]?->seller_protection()?->status ?? $value, 10, 2 );
add_filter( 'sift_for_woocommerce_ppcp-gateway_paypal_payment_status', fn( $value, $ppcp_data ) => $ppcp_data['order']?->purchase_units()[0]?->payments()?->captures()[0]->status()->name() ?? $value, 10, 2 );

/**
* Send a chargeback event to Sift.
*
* @param object $event The Stripe event object.
*
* @return void
*/
function send_chargeback_to_sift( $event ): void {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept these functions in case we ever get a filter to allow us to use them in the future.

$order_id = $event['data']['object']['resource']['disputed_transactions'][0]['seller_transaction_id'] ?? null;
// Log the resolved order ID.
if ( ! $order_id ) {
wc_get_logger()->error( 'Order ID not found in event.' );
return;
}
$order = wc_get_order( $order_id );

if ( ! $order instanceof \WC_Order ) {
wc_get_logger()->error( 'WooCommerce order not found for Order ID: ' . esc_html( $order_id ) );
return;
}

$chargeback_reason = convert_dispute_reason_to_sift_chargeback_reason( $event['data']['object']['resource']['reason'] ?? '' );

Events::chargeback( $order_id, $order, $chargeback_reason );
}

/**
* Convert a dispute reason from a string that PayPal would use to a string that Sift would use.
*
* @param string $dispute_reason A dispute reason string that PayPal would use.
*
* @return string|null A dispute reason string that Sift would use.
*/
function convert_dispute_reason_to_sift_chargeback_reason( string $dispute_reason ): ?string {
switch ( $dispute_reason ) {
case 'MERCHANDISE_OR_SERVICE_NOT_RECEIVED':
return '$product_not_received';
case 'MERCHANDISE_OR_SERVICE_NOT_AS_DESCRIBED':
return '$product_unacceptable';
case 'UNAUTHORIZED':
return '$authorization';
case 'PROBLEM_WITH_REMITTANCE':
return '$processing_errors';
case 'DUPLICATE_TRANSACTION':
return '$duplicate';
case 'INCORRECT_AMOUNT':
case 'CREDIT_NOT_PROCESSED':
case 'PAYMENT_BY_OTHER_MEANS':
return '$customer_disputes';
case 'CANCELED_RECURRING_BILLING':
return '$cancel_subscription';
case 'OTHER':
return '$other';
default:
return null;
}
}
66 changes: 53 additions & 13 deletions src/inc/sift-events/sift-events.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

use Sift_For_WooCommerce\Sift_Order;
use Sift_For_WooCommerce\Sift\SiftEventsValidator;
use Sift_For_WooCommerce\Sift_For_WooCommerce;
use WC_Product;

/**
Expand Down Expand Up @@ -332,6 +333,10 @@ public static function update_account( string $user_id, ?\WP_User $old_user_data
'$time' => intval( 1000 * microtime( true ) ),
);

if ( empty( $properties['$payment_methods'] ) ) {
unset( $properties['$payment_methods'] );
}

try {
SiftEventsValidator::validate_update_account( $properties );
} catch ( \Exception $e ) {
Expand Down Expand Up @@ -555,7 +560,6 @@ public static function create_order( string $order_id, \WC_Order $order ) {
* @return void
*/
public static function update_or_create_order( string $order_id, \WC_Order $order, bool $create_order = false ) {

if ( ! in_array( $order->get_status(), self::SUPPORTED_WOO_ORDER_STATUS_CHANGES, true ) ) {
return;
}
Expand All @@ -566,15 +570,13 @@ public static function update_or_create_order( string $order_id, \WC_Order $orde

// Determine user and session context.
$user_id = wp_get_current_user()->ID ?? null; // Check first for logged-in user.
$is_system = ! $create_order && str_starts_with( sanitize_title( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ), 'WordPress' ); // Check if this is an order update via system action.
$is_admin = 1 === $user_id;

// Figure out if it should use the session ID if no logged-in user exists.
if ( ! $user_id ) {
$user_id = $is_system ? $order->get_user_id() : null; // Use order user ID only for system actions.
if ( ! $user_id || $is_admin ) {
$user_id = $order->get_user_id() ?? null; // Use order user ID if it isn't available otherwise
}

$user = $user_id ? get_userdata( $user_id ) : null;

$physical_or_electronic = '$electronic';
$items = array();
foreach ( $order->get_items( 'line_item' ) as $item ) {
Expand Down Expand Up @@ -628,8 +630,8 @@ public static function update_or_create_order( string $order_id, \WC_Order $orde

// Add the user_id only if a user exists, otherwise, let it remain empty.
// Ref: https://developers.sift.com/docs/php/apis-overview/core-topics/faq/tracking-users
if ( $user ) {
$properties['$user_id'] = self::format_user_id( $user->ID );
if ( $user_id && ! $is_admin ) {
$properties['$user_id'] = self::format_user_id( $user_id );
}

// Add in the address information if it's available.
Expand Down Expand Up @@ -811,7 +813,6 @@ public static function chargeback( string $order_id, \WC_Order $order, string $c
* @return void
*/
public static function add( string $event, array $properties ) {

// Give a chance for the platform to modify the data (and add potentially new custom data)
$properties = apply_filters( 'sift_for_woocommerce_pre_send_event_properties', $properties, $event );

Expand Down Expand Up @@ -1017,11 +1018,40 @@ private static function get_order_address( string $order_id, string $type = 'bil
*
* @param integer $user_id The User / Customer ID.
*
* @return array|null
* @return array
*/
private static function get_customer_payment_methods( int $user_id ) {
$payment_methods = array();

/**
* Allow / disallow customer payment method lookup via looping over all customer orders and extracting the payment method from each order.
*
* If this filter returns false, the sift_for_woocommerce_get_customer_payment_methods filter should be implemented so that some payment methods are returned.
*
* Otherwise, no customer payment methods will be returned.
*
* @param boolean $allow True if this method of payment method lookup should be used, otherwise false.
* @param integer $user_id The User / Customer ID.
*
* @return boolean True if this method of payment method lookup should be used, otherwise false.
*/
if ( apply_filters( 'sift_for_woocommerce_get_customer_payment_methods_via_order_enumeration', true, $user_id ) ) {
$customer_orders = wc_get_orders(
array(
'limit' => -1,
'customer' => $user_id,
'status' => wc_get_is_paid_statuses(),
)
);

$payment_methods = array_map(
function ( $order ) {
return static::get_order_payment_methods( $order )[0] ?? null;
},
$customer_orders
);
}

/**
* Include a filter here for unexpected payment providers to be able to add their results in as well.
*
Expand All @@ -1030,7 +1060,18 @@ private static function get_customer_payment_methods( int $user_id ) {
*/
$payment_methods = apply_filters( 'sift_for_woocommerce_get_customer_payment_methods', $payment_methods, $user_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

return $payment_methods ?? null;
$payment_methods = array_reduce(
$payment_methods,
function ( $payment_methods, $payment_method ) {
if ( ! empty( $payment_method ) && ! in_array( $payment_method, $payment_methods, true ) ) {
$payment_methods[] = $payment_method;
}
return $payment_methods;
},
array()
);

return $payment_methods ?? array();
}

/**
Expand All @@ -1044,8 +1085,7 @@ private static function get_customer_payment_methods( int $user_id ) {
* @return array
*/
private static function get_order_payment_methods( \WC_Order $order ) {
$sift_order = new Sift_Order( $order );
return $sift_order->get_payment_methods();
return Sift_For_WooCommerce::get_instance()->get_sift_order_from_wc_order( $order )->get_payment_methods();
}

/**
Expand Down
17 changes: 17 additions & 0 deletions src/sift-for-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
*/
class Sift_For_WooCommerce {

private static $sift_orders = array();

// region MAGIC METHODS

/**
Expand Down Expand Up @@ -132,5 +134,20 @@ public static function get_api_client() {
return $client;
}

/**
* Get a Sift_Order object from a WC_Order object.
* Allows a small speed up in performance due to caching the orders.
*
* @param \WC_Order $order The WC_Order object.
*
* @return Sift_Order The Sift_Order object.
*/
public static function get_sift_order_from_wc_order( \WC_Order $order ): Sift_Order {
if ( ! array_key_exists( $order->get_order_key(), static::$sift_orders ) ) {
static::$sift_orders[ $order->get_order_key() ] = new Sift_Order( $order );
}
return static::$sift_orders[ $order->get_order_key() ];
}

// endregion
}
Loading