Skip to content

Commit

Permalink
Merge branch 'develop' into fix/198-mccy-fedex-conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelzaleski authored Dec 18, 2024
2 parents a6a0793 + efc8b58 commit 6b0ff25
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 25 deletions.
4 changes: 4 additions & 0 deletions changelog/fix-method-title-availability
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: fix

Set payment method title once title is known.
5 changes: 5 additions & 0 deletions changelog/fix-tokenized-cart-multiple-variations
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: fix: tokenized cart & multiple variations.


5 changes: 5 additions & 0 deletions changelog/fix-tokenized-cart-subscription-signup-fee
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: fix: tokenized cart subscription signup fee price


Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: update

Update error messages for payment authorization actions to provide more specific and user-friendly feedback.
102 changes: 86 additions & 16 deletions client/data/authorizations/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,51 @@ import {
import { STORE_NAME } from '../constants';
import { ApiError } from 'wcpay/types/errors';

const getErrorMessage = ( apiError: {
code?: string;
message?: string;
} ): string => {
// Map specific error codes to user-friendly messages
const errorMessages: Record< string, string > = {
wcpay_missing_order: __(
'The order could not be found.',
'woocommerce-payments'
),
wcpay_refunded_order_uncapturable: __(
'Payment cannot be processed for partially or fully refunded orders.',
'woocommerce-payments'
),
wcpay_intent_order_mismatch: __(
'The payment cannot be processed due to a mismatch with order details.',
'woocommerce-payments'
),
wcpay_payment_uncapturable: __(
'This payment cannot be processed in its current state.',
'woocommerce-payments'
),
wcpay_capture_error: __(
'The payment capture failed to complete.',
'woocommerce-payments'
),
wcpay_cancel_error: __(
'The payment cancellation failed to complete.',
'woocommerce-payments'
),
wcpay_server_error: __(
'An unexpected error occurred. Please try again later.',
'woocommerce-payments'
),
};

return (
errorMessages[ apiError.code ?? '' ] ??
__(
'Unable to process the payment. Please try again later.',
'woocommerce-payments'
)
);
};

export function updateAuthorizations(
query: Query,
data: Authorization[]
Expand Down Expand Up @@ -165,17 +210,29 @@ export function* submitCaptureAuthorization(
)
);
} catch ( error ) {
const baseErrorMessage = sprintf(
// translators: %s Order id
__(
'There has been an error capturing the payment for order #%s.',
'woocommerce-payments'
),
orderId
);

const apiError = error as {
code?: string;
message?: string;
data?: {
status?: number;
};
};

const errorDetails = getErrorMessage( apiError );

yield controls.dispatch(
'core/notices',
'createErrorNotice',
sprintf(
// translators: %s Order id
__(
'There has been an error capturing the payment for order #%s. Please try again later.',
'woocommerce-payments'
),
orderId
)
`${ baseErrorMessage } ${ errorDetails }`
);
} finally {
yield controls.dispatch(
Expand All @@ -184,6 +241,7 @@ export function* submitCaptureAuthorization(
'getAuthorization',
[ paymentIntentId ]
);

yield controls.dispatch(
STORE_NAME,
'setIsRequestingAuthorization',
Expand Down Expand Up @@ -278,17 +336,29 @@ export function* submitCancelAuthorization(
)
);
} catch ( error ) {
const baseErrorMessage = sprintf(
// translators: %s Order id
__(
'There has been an error canceling the payment for order #%s.',
'woocommerce-payments'
),
orderId
);

const apiError = error as {
code?: string;
message?: string;
data?: {
status?: number;
};
};

const errorDetails = getErrorMessage( apiError );

yield controls.dispatch(
'core/notices',
'createErrorNotice',
sprintf(
// translators: %s Order id
__(
'There has been an error canceling the payment for order #%s. Please try again later.',
'woocommerce-payments'
),
orderId
)
`${ baseErrorMessage } ${ errorDetails }`
);
} finally {
yield controls.dispatch(
Expand Down
159 changes: 157 additions & 2 deletions client/data/authorizations/test/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
updateAuthorization,
} from '../actions';
import authorizationsFixture from './authorizations.fixture.json';
import { STORE_NAME } from 'wcpay/data/constants';

describe( 'Authorizations actions', () => {
describe( 'submitCaptureAuthorization', () => {
Expand Down Expand Up @@ -153,10 +154,117 @@ describe( 'Authorizations actions', () => {
controls.dispatch(
'core/notices',
'createErrorNotice',
'There has been an error capturing the payment for order #42. Please try again later.'
'There has been an error capturing the payment for order #42. Unable to process the payment. Please try again later.'
)
);
} );

describe( 'error handling', () => {
it( 'should create error notice with API error message', () => {
const generator = submitCaptureAuthorization( 'pi_123', 123 );

// Mock the start of the capture process
expect( generator.next().value ).toEqual(
controls.dispatch(
STORE_NAME,
'startResolution',
'getAuthorization',
[ 'pi_123' ]
)
);

expect( generator.next().value ).toEqual(
controls.dispatch(
STORE_NAME,
'setIsRequestingAuthorization',
true
)
);

// Mock API error response
const apiError = {
code: 'wcpay_refunded_order_uncapturable',
message:
'Payment cannot be captured for partially or fully refunded orders.',
data: { status: 400 },
};

// Simulate API error
expect( generator.throw( apiError ).value ).toEqual(
controls.dispatch(
'core/notices',
'createErrorNotice',
'There has been an error capturing the payment for order #123. Payment cannot be processed for partially or fully refunded orders.'
)
);

// Verify cleanup in finally block
expect( generator.next().value ).toEqual(
controls.dispatch(
STORE_NAME,
'finishResolution',
'getAuthorization',
[ 'pi_123' ]
)
);

expect( generator.next().value ).toEqual(
controls.dispatch(
STORE_NAME,
'setIsRequestingAuthorization',
false
)
);
} );

it( 'should create error notice with fallback message when API error has no message', () => {
const generator = submitCaptureAuthorization( 'pi_123', 123 );

// Skip initial dispatch calls
generator.next();
generator.next();

// Mock API error without message
const apiError = {
code: 'unknown_error',
data: { status: 500 },
};

expect( generator.throw( apiError ).value ).toEqual(
controls.dispatch(
'core/notices',
'createErrorNotice',
'There has been an error capturing the payment for order #123. Unable to process the payment. Please try again later.'
)
);
} );

it( 'should show default error notice for unknown error code', () => {
const generator = submitCaptureAuthorization(
'pi_unknown',
999
);

// Start the generator to the point where it would throw an error
generator.next();
generator.next();

// Mock an API error with an unknown error code
const apiError = {
code: 'unknown_error_code',
data: { status: 500 },
};

// Expect the default error message to be dispatched
expect( generator.throw( apiError ).value ).toEqual(
controls.dispatch(
'core/notices',
'createErrorNotice',
'There has been an error capturing the payment for order #999. Unable to process the payment. Please try again later.'
)
);
} );
} );
} );

describe( 'submitCancelAuthorization', () => {
Expand Down Expand Up @@ -294,9 +402,56 @@ describe( 'Authorizations actions', () => {
controls.dispatch(
'core/notices',
'createErrorNotice',
'There has been an error canceling the payment for order #42. Please try again later.'
'There has been an error canceling the payment for order #42. Unable to process the payment. Please try again later.'
)
);
} );

describe( 'error handling', () => {
it( 'should create error notice with API error message', () => {
const generator = submitCancelAuthorization( 'pi_123', 123 );

// Skip initial dispatch calls
generator.next();
generator.next();

// Mock API error response
const apiError = {
code: 'wcpay_payment_uncapturable',
message: 'The payment cannot be canceled at this time.',
data: { status: 400 },
};

expect( generator.throw( apiError ).value ).toEqual(
controls.dispatch(
'core/notices',
'createErrorNotice',
'There has been an error canceling the payment for order #123. This payment cannot be processed in its current state.'
)
);
} );

it( 'should create error notice with fallback message when API error has no message', () => {
const generator = submitCancelAuthorization( 'pi_123', 123 );

// Skip initial dispatch calls
generator.next();
generator.next();

// Mock API error without message
const apiError = {
code: 'unknown_error',
data: { status: 500 },
};

expect( generator.throw( apiError ).value ).toEqual(
controls.dispatch(
'core/notices',
'createErrorNotice',
'There has been an error canceling the payment for order #123. Unable to process the payment. Please try again later.'
)
);
} );
} );
} );
} );
17 changes: 16 additions & 1 deletion client/tokenized-express-checkout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,21 @@ jQuery( ( $ ) => {
'wcpay.express-checkout.update-button-data',
'automattic/wcpay/express-checkout',
async () => {
// if the product cannot be added to cart (because of missing variation selection, etc),
// don't try to add it to the cart to get new data - the call will likely fail.
if (
getExpressCheckoutData( 'button_context' ) === 'product'
) {
const addToCartButton = $(
'.single_add_to_cart_button'
);

// First check if product can be added to cart.
if ( addToCartButton.is( '.disabled' ) ) {
return;
}
}

try {
expressCheckoutButtonUi.blockButton();

Expand Down Expand Up @@ -375,7 +390,7 @@ jQuery( ( $ ) => {

expressCheckoutButtonUi.unblockButton();
} catch ( e ) {
expressCheckoutButtonUi.hide();
expressCheckoutButtonUi.hideContainer();
}
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export const transformPrice = ( price, priceObject ) => {
export const transformCartDataForDisplayItems = ( cartData ) => {
const displayItems = cartData.items.map( ( item ) => ( {
amount: transformPrice(
parseInt( item.prices.price, 10 ),
item.prices
parseInt( item.totals?.line_subtotal || item.prices.price, 10 ),
item.totals || item.prices
),
name: [
item.name,
Expand Down Expand Up @@ -96,7 +96,7 @@ export const transformCartDataForDisplayItems = ( cartData ) => {
* @return {{id: string, label: string, amount: integer, deliveryEstimate: string}} `shippingRates` for Stripe.
*/
export const transformCartDataForShippingRates = ( cartData ) =>
cartData.shipping_rates?.[ 0 ].shipping_rates
cartData.shipping_rates?.[ 0 ]?.shipping_rates
.sort( ( rateA, rateB ) => {
if ( rateA.selected === rateB.selected ) {
return 0; // Keep relative order if both have the same value for 'selected'
Expand Down
Loading

0 comments on commit 6b0ff25

Please sign in to comment.