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

feat(connector): [JPMORGAN] add Payment flows for cards #6668

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

bsayak03
Copy link
Contributor

@bsayak03 bsayak03 commented Nov 26, 2024

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

Additional Changes

  • This PR modifies the API contract
  • This PR modifies the database schema
  • This PR modifies application configuration/environment variables

Motivation and Context

https://developer.payments.jpmorgan.com/docs/commerce/online-payments/capabilities/online-payments/how-to/auth-and-capture-payment
https://developer.payments.jpmorgan.com/api/commerce/online-payments/online-payments#/operations/V2PaymentPost

Closes juspay/hyperswitch-cloud#7569

How did you test it?

Cypress Tests

Test Passes for

1. No3DSAutoCapture
No3DSAutoCapture

2. 3DSAutoCapture
3DSAutoCapture

3. No3DSManualCapture
No3DSManualCapture

4. VoidPayment
Void Payment

5. SyncPayment
Sync Payment

6. RefundPayment
Refund

7. SyncRefund
Sync Refund

Test Cases failing for
1. RefundPayment : The connector currently sends the same transaction ID for every request in the sandbox environment, causing refund failures. While the refund flow has been implemented in the code, it has been commented out, and an error message is thrown by default until the connector resolves this issue.
2. SyncRefund : Since refunds are not being processed, the sync refund functionality is expected to fail. To address this, the TRIGGER_FLAG is used in Cypress tests to prevent failures. However, the Sync Refund logic has already been implemented in the code.

Following flows need to be tested for card payments for new connector Jpmorgan:

1. Authorize (Manual)

  • Request
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_VwPwlCTqgrrBU4OPlPTDHZz89LN0GpcHQRx7iV49U090zNugXvDd8gKK8UWl7Ipl' \
--data '{
    "amount": 90,
    "currency": "USD",
    "confirm": true,
    "capture_method":"manual",
    "profile_id": null,
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": { 
        "card": {
            "card_number": "4200000000000000",
            "card_exp_month": "10",
            "card_exp_year": "2026",
            "card_holder_name": "joseph Doe",
            "card_cvc": "123"
        }
    }
}'
  • Response
{
    "payment_id": "pay_xgDIyzRE05vHzo2y2dcj",
    "merchant_id": "merchant_1732504948",
    "status": "requires_capture",
    "amount": 90,
    "net_amount": 90,
    "shipping_cost": null,
    "amount_capturable": 90,
    "amount_received": null,
    "connector": "jpmorgan",
    "client_secret": "pay_xgDIyzRE05vHzo2y2dcj_secret_rXfBN5zk3nyQCTrBvipQ",
    "created": "2024-11-25T03:57:19.367Z",
    "currency": "USD",
    "customer_id": null,
    "customer": null,
    "description": null,
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "manual",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "0000",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "420000",
            "card_extended_bin": null,
            "card_exp_month": "10",
            "card_exp_year": "2026",
            "card_holder_name": null,
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": null,
    "order_details": null,
    "email": null,
    "name": null,
    "phone": null,
    "return_url": null,
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": null,
    "manual_retry_allowed": false,
    "connector_transaction_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "payment_link": null,
    "profile_id": "pro_5RER4wWpKHIE29BnX5Sc",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_MCfRyvSkAZajaTZO5V9g",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2024-11-25T04:12:19.366Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2024-11-25T03:57:22.367Z",
    "charges": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}

2. Capture (Manual)

  • Request
curl --location 'http://localhost:8080/payments/pay_xgDIyzRE05vHzo2y2dcj/capture' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_VwPwlCTqgrrBU4OPlPTDHZz89LN0GpcHQRx7iV49U090zNugXvDd8gKK8UWl7Ipl' \
--data '{
  "amount_to_capture": 90,
  "statement_descriptor_name": "Joseph",
  "statement_descriptor_prefix" :"joseph",
  "statement_descriptor_suffix": "JS"
}'
  • Response
{
    "payment_id": "pay_xgDIyzRE05vHzo2y2dcj",
    "merchant_id": "merchant_1732504948",
    "status": "succeeded",
    "amount": 90,
    "net_amount": 90,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 90,
    "connector": "jpmorgan",
    "client_secret": "pay_xgDIyzRE05vHzo2y2dcj_secret_rXfBN5zk3nyQCTrBvipQ",
    "created": "2024-11-25T03:57:19.367Z",
    "currency": "USD",
    "customer_id": null,
    "customer": null,
    "description": null,
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "manual",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "0000",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "420000",
            "card_extended_bin": null,
            "card_exp_month": "10",
            "card_exp_year": "2026",
            "card_holder_name": null,
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": null,
    "order_details": null,
    "email": null,
    "name": null,
    "phone": null,
    "return_url": null,
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": null,
    "manual_retry_allowed": false,
    "connector_transaction_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "payment_link": null,
    "profile_id": "pro_5RER4wWpKHIE29BnX5Sc",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_MCfRyvSkAZajaTZO5V9g",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2024-11-25T04:12:19.366Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2024-11-25T03:59:41.611Z",
    "charges": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}

3. Authorize + Capture

  • Request
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_VwPwlCTqgrrBU4OPlPTDHZz89LN0GpcHQRx7iV49U090zNugXvDd8gKK8UWl7Ipl' \
--data '{
    "amount": 29,
    "currency": "USD",
    "confirm": true,
    "capture_method":"automatic",
    "profile_id": null,
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": { 
        "card": {
            "card_number": "4200000000000000",
            "card_exp_month": "10",
            "card_exp_year": "2026",
            "card_holder_name": "joseph Doe",
            "card_cvc": "123"
        }
    }
}'
  • Response
{
    "payment_id": "pay_rNoRS0s4cL5sKIpKx9Mc",
    "merchant_id": "merchant_1732504948",
    "status": "succeeded",
    "amount": 29,
    "net_amount": 29,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 29,
    "connector": "jpmorgan",
    "client_secret": "pay_rNoRS0s4cL5sKIpKx9Mc_secret_VdFa9Ka56kCCggZd2JNs",
    "created": "2024-11-25T04:02:26.242Z",
    "currency": "USD",
    "customer_id": null,
    "customer": null,
    "description": null,
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "0000",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "420000",
            "card_extended_bin": null,
            "card_exp_month": "10",
            "card_exp_year": "2026",
            "card_holder_name": null,
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": null,
    "order_details": null,
    "email": null,
    "name": null,
    "phone": null,
    "return_url": null,
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": null,
    "manual_retry_allowed": false,
    "connector_transaction_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "payment_link": null,
    "profile_id": "pro_5RER4wWpKHIE29BnX5Sc",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_MCfRyvSkAZajaTZO5V9g",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2024-11-25T04:17:26.242Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2024-11-25T04:02:27.807Z",
    "charges": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}

4. PSync

  • Request
curl --location 'http://localhost:8080/payments/pay_rNoRS0s4cL5sKIpKx9Mc?force_sync=true&expand_captures=true&expand_attempts=true' \
--header 'Accept: application/json' \
--header 'api-key: dev_VwPwlCTqgrrBU4OPlPTDHZz89LN0GpcHQRx7iV49U090zNugXvDd8gKK8UWl7Ipl' \
--data ''
  • Response
{
    "payment_id": "pay_rNoRS0s4cL5sKIpKx9Mc",
    "merchant_id": "merchant_1732504948",
    "status": "succeeded",
    "amount": 29,
    "net_amount": 29,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 29,
    "connector": "jpmorgan",
    "client_secret": "pay_rNoRS0s4cL5sKIpKx9Mc_secret_VdFa9Ka56kCCggZd2JNs",
    "created": "2024-11-25T04:02:26.242Z",
    "currency": "USD",
    "customer_id": null,
    "customer": null,
    "description": null,
    "refunds": null,
    "disputes": null,
    "attempts": [
        {
            "attempt_id": "pay_rNoRS0s4cL5sKIpKx9Mc_1",
            "status": "charged",
            "amount": 29,
            "currency": "USD",
            "connector": "jpmorgan",
            "error_message": null,
            "payment_method": "card",
            "connector_transaction_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
            "capture_method": "automatic",
            "authentication_type": "no_three_ds",
            "created_at": "2024-11-25T04:02:26.242Z",
            "modified_at": "2024-11-25T04:02:27.806Z",
            "cancellation_reason": null,
            "mandate_id": null,
            "error_code": null,
            "payment_token": null,
            "connector_metadata": null,
            "payment_experience": null,
            "payment_method_type": "credit",
            "reference_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
            "unified_code": null,
            "unified_message": null,
            "client_source": null,
            "client_version": null
        }
    ],
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "0000",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "420000",
            "card_extended_bin": null,
            "card_exp_month": "10",
            "card_exp_year": "2026",
            "card_holder_name": null,
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": null,
    "order_details": null,
    "email": null,
    "name": null,
    "phone": null,
    "return_url": null,
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": null,
    "manual_retry_allowed": false,
    "connector_transaction_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "payment_link": null,
    "profile_id": "pro_5RER4wWpKHIE29BnX5Sc",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_MCfRyvSkAZajaTZO5V9g",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2024-11-25T04:17:26.242Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2024-11-25T04:02:27.807Z",
    "charges": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}

5. Refund (Issue in the refund flow, because the connector sends the same transaction id for every hit on sandbox, hence conflicting)

  • Request
curl --location 'http://localhost:8080/refunds' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_UrUqvKI4VJryn9DBhpNn28eNR8xKKKX6wgUAzBgkZS6JxPTL02KWowsVVBLWKKLB' \
--data '{
    "payment_id": "pay_LSvriAUkv1EQ7H0IiCtj",
    
    
    "reason": "Customer returned product",
    "refund_type": "instant",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    }
}'
  • Response
{
    "refund_id": "ref_pf6FYPvh9OReebKK6lr0",
    "payment_id": "pay_LSvriAUkv1EQ7H0IiCtj",
    "amount": 128,
    "currency": "USD",
    "status": "succeeded",
    "reason": "Customer returned product",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    },
    "error_message": null,
    "error_code": null,
    "created_at": "2024-11-25T04:15:14.312Z",
    "updated_at": "2024-11-25T04:15:14.914Z",
    "connector": "jpmorgan",
    "profile_id": "pro_ReWjg2xQYvoCCp6Aedli",
    "merchant_connector_id": "mca_xn15JFYJguLAd1PB45jr",
    "charges": null
}

6. RSync

  • Request
curl --location 'http://localhost:8080/refunds/ref_pf6FYPvh9OReebKK6lr0' \
--header 'Accept: application/json' \
--header 'api-key: dev_UrUqvKI4VJryn9DBhpNn28eNR8xKKKX6wgUAzBgkZS6JxPTL02KWowsVVBLWKKLB' \
--data ''
  • Response
{
    "refund_id": "ref_pf6FYPvh9OReebKK6lr0",
    "payment_id": "pay_LSvriAUkv1EQ7H0IiCtj",
    "amount": 128,
    "currency": "USD",
    "status": "succeeded",
    "reason": "Customer returned product",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    },
    "error_message": null,
    "error_code": null,
    "created_at": "2024-11-25T04:15:14.312Z",
    "updated_at": "2024-11-25T04:15:14.914Z",
    "connector": "jpmorgan",
    "profile_id": "pro_ReWjg2xQYvoCCp6Aedli",
    "merchant_connector_id": "mca_xn15JFYJguLAd1PB45jr",
    "charges": null
}

7. Cancel/Void

  • Request
curl --location 'http://localhost:8080/payments/pay_Bxl3iUqyNMHkUXfqdL0E/cancel' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_VwPwlCTqgrrBU4OPlPTDHZz89LN0GpcHQRx7iV49U090zNugXvDd8gKK8UWl7Ipl' \
--data '{
  "cancellation_reason": "requested_by_customer"
}'
  • Response
{
    "payment_id": "pay_Bxl3iUqyNMHkUXfqdL0E",
    "merchant_id": "merchant_1732504948",
    "status": "cancelled",
    "amount": 128,
    "net_amount": 128,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": null,
    "connector": "jpmorgan",
    "client_secret": "pay_Bxl3iUqyNMHkUXfqdL0E_secret_6t6Ersg5ctM0YMqHP4IZ",
    "created": "2024-11-25T04:12:12.347Z",
    "currency": "USD",
    "customer_id": null,
    "customer": null,
    "description": null,
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "manual",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "0000",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "420000",
            "card_extended_bin": null,
            "card_exp_month": "10",
            "card_exp_year": "2026",
            "card_holder_name": null,
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": null,
    "order_details": null,
    "email": null,
    "name": null,
    "phone": null,
    "return_url": null,
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": "requested_by_customer",
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": null,
    "manual_retry_allowed": false,
    "connector_transaction_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "cdf62f90-6440-496f-817c-c05dd3b7b01a",
    "payment_link": null,
    "profile_id": "pro_5RER4wWpKHIE29BnX5Sc",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_MCfRyvSkAZajaTZO5V9g",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2024-11-25T04:27:12.347Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2024-11-25T04:12:58.939Z",
    "charges": null,
    "frm_metadata": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null
}

Checklist

  • I formatted the code cargo +nightly fmt --all
  • I addressed lints thrown by cargo clippy
  • I reviewed the submitted code
  • I added unit tests for my changes where possible

@bsayak03 bsayak03 requested review from a team as code owners November 26, 2024 14:13
Copy link

semanticdiff-com bot commented Nov 26, 2024

Review changes with  SemanticDiff

Changed Files
File Status
  crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs  14% smaller
  crates/hyperswitch_connectors/src/connectors/jpmorgan.rs  10% smaller
  api-reference-v2/openapi_spec.json  0% smaller
  api-reference/openapi_spec.json  0% smaller
  config/config.example.toml Unsupported file format
  config/deployments/integration_test.toml Unsupported file format
  config/deployments/production.toml Unsupported file format
  config/deployments/sandbox.toml Unsupported file format
  config/development.toml Unsupported file format
  config/docker_compose.toml Unsupported file format
  crates/api_models/src/connector_enums.rs  0% smaller
  crates/common_enums/src/connector_enums.rs  0% smaller
  crates/connector_configs/src/connector.rs  0% smaller
  crates/connector_configs/toml/development.toml Unsupported file format
  crates/connector_configs/toml/production.toml Unsupported file format
  crates/connector_configs/toml/sandbox.toml Unsupported file format
  crates/hyperswitch_connectors/src/constants.rs  0% smaller
  crates/router/src/core/admin.rs  0% smaller
  crates/router/src/types/api.rs  0% smaller
  crates/router/src/types/transformers.rs  0% smaller
  crates/router/tests/connectors/sample_auth.toml Unsupported file format
  crates/test_utils/src/connector_auth.rs  0% smaller
  cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js  0% smaller
  cypress-tests/cypress/e2e/PaymentUtils/Utils.js  0% smaller
  loadtest/config/development.toml Unsupported file format

@hyperswitch-bot hyperswitch-bot bot added the M-api-contract-changes Metadata: This PR involves API contract changes label Nov 26, 2024
@bsayak03 bsayak03 self-assigned this Nov 26, 2024
@bsayak03 bsayak03 added this to the November 2024 Release milestone Nov 26, 2024
@bsayak03 bsayak03 force-pushed the jpmorgan-cards branch 5 times, most recently from 6d7ae12 to 99e5223 Compare November 29, 2024 11:12
@bsayak03 bsayak03 requested a review from a team as a code owner December 5, 2024 11:00
@bsayak03 bsayak03 force-pushed the jpmorgan-cards branch 2 times, most recently from 808ae0e to 678b508 Compare December 5, 2024 13:27
Copy link
Contributor

Choose a reason for hiding this comment

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

does jpmorgan use same base url in both sbx and prod env?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, it uses different

Copy link
Contributor

Choose a reason for hiding this comment

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

add pm filters in toml files.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

Copy link
Contributor

Choose a reason for hiding this comment

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

check if jpmorgan is needs to be added in temp_locker_enable_config

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we don't need to add that here

@@ -945,6 +945,7 @@ default_imp_for_payouts!(
connector::Gpayments,
connector::Iatapay,
connector::Itaubank,
//connector::Jpmorgan,
Copy link
Contributor

Choose a reason for hiding this comment

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

will we add support for payouts also for jp morgan?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, removing the commented code

Copy link
Contributor

Choose a reason for hiding this comment

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

revert this file change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

Copy link
Contributor

Choose a reason for hiding this comment

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

revert this change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

Copy link
Contributor

Choose a reason for hiding this comment

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

revert

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

package.json Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

revert

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

@@ -1,10 +1,13 @@
pub mod transformers;
use std::convert::TryFrom;
Copy link
Contributor

@mrudulvajpayee4935 mrudulvajpayee4935 Dec 6, 2024

Choose a reason for hiding this comment

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

remove unused imports from the files

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we get warnings for unused imports, isn't it?
i got them and removed those, no more warnings remaining

@bsayak03 bsayak03 force-pushed the jpmorgan-cards branch 2 times, most recently from 3e3ded0 to a818e57 Compare December 6, 2024 09:39
@@ -320,6 +321,10 @@ debit.currency = "USD"
ali_pay.currency = "GBP,CNY"
we_chat_pay.currency = "GBP,CNY"

[pm.filters.jpmorgan]
Copy link
Contributor

Choose a reason for hiding this comment

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

please verify the currencies supported

client_id.peek(),
client_secret.unwrap_or_default().peek()
);
// let creds = format!("{}:{}", client_id.peek(), client_secret.unwrap_or_default().peek());
Copy link
Contributor

Choose a reason for hiding this comment

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

remove unused commented code

) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
let endpoint = self.base_url(connectors);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok(format!("{}/payments", self.base_url(connectors)))

})
}
_ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()),
_ => Err(errors::ConnectorError::NotImplemented(
Copy link
Contributor

Choose a reason for hiding this comment

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

mention all enums exclusively, in case of payment methods

})
}
_ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()),
_ => Err(errors::ConnectorError::NotImplemented(
"Selected payment method through jpmorgan".to_string(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Err(errors::ConnectorError::NotImplemented(
                            get_unimplemented_payment_method_error_message("jpmorgan"),
                        ))?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

let capture_method = if let Some(method) = item.router_data.request.capture_method {
map_capture_method(method)
} else {
String::from("AUTOMATIC")
Copy link
Contributor

Choose a reason for hiding this comment

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

cover this condition in map_capture_method itself

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

let mut transaction_state = item.response.transaction_state.to_string();

if transaction_state == "Closed" {
let cm = item.response.capture_method.clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

let capture_method = item.response.capture_method.clone();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

modifying

let cm = item.response.capture_method.clone();
if cm == Some("NOW".to_string()) {
transaction_state = String::from("Closed");
} else if cm == Some("MANUAL".to_string()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

please make enums instead of string comparison.


let currency = Some(item.router_data.request.currency.to_string());

// let company_name : Option<String> = Some(String::from("JPMC"));
Copy link
Contributor

Choose a reason for hiding this comment

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

remove this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

Comment on lines 324 to 327
[pm.filters.jpmorgan]
debit = { country = "CA, EU, UK, US", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BYN, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, ISK, JMD, JPY, KES, KHR, KMF, KRW, KYD, KZT, LAK, LBP, LKR, LRD, LSL, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SLL, SOS, SRD, STN, SZL, THB, TJS, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW" }
credit = { country = "CA, EU, UK, US", currency = "AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BIF, BMD, BND, BOB, BRL, BSD, BTN, BWP, BYN, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ETB, EUR, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, IDR, ILS, INR, ISK, JMD, JPY, KES, KHR, KMF, KRW, KYD, KZT, LAK, LBP, LKR, LRD, LSL, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRU, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RWF, SAR, SBD, SCR, SEK, SGD, SHP, SLL, SOS, SRD, STN, SZL, THB, TJS, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW" }

Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this added only in config/deployments/production.toml

[[jpmorgan.debit]]
payment_method_type = "Visa"
[jpmorgan.connector_auth.BodyKey]
api_key="Access Token"
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the API key from JPMorgan referred to as an access_token in their documentation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

api_key is client_id and key1 is client_secret

@@ -1,10 +1,13 @@
pub mod transformers;
use std::convert::TryFrom;
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

cool

@@ -56,84 +139,258 @@ impl TryFrom<&JpmorganRouterData<&PaymentsAuthorizeRouterData>> for JpmorganPaym
) -> Result<Self, Self::Error> {
match item.router_data.request.payment_method_data.clone() {
PaymentMethodData::Card(req_card) => {
if item.router_data.is_three_ds() {
return Err(errors::ConnectorError::NotSupported {
message: "Three_ds payments".to_string(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
message: "Three_ds payments".to_string(),
message: "3DS Payments".to_string(),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay

Comment on lines 149 to 153
let capture_method = if let Some(method) = item.router_data.request.capture_method {
map_capture_method(method)
} else {
String::from("AUTOMATIC")
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Handle the option in map_capture_method only


let merchant_software = JpmorganMerchantSoftware {
company_name: String::from("JPMC"),
product_name: String::from("Hyperswitch"), //could be Amazon or something else, subject to change
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this hardcoded?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

RouterData doesn't have this, hence hardcoded

complete: item.router_data.request.is_auto_capture()?,
account_number,
expiry,
is_bill_payment: item.router_data.request.is_auto_capture()?,
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the use of this value is_bill_payment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nothing as such, just a field inside cards. Should i remove this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove it if it is not mandatory

Comment on lines 196 to 213
//in jpm, we get a client id and secret and using these two, we have a curl, we make an api call and we get a access token in res with an expiry time as well
#[derive(Debug)]
pub struct JpmorganAuthType {
pub(super) api_key: Secret<String>,
#[allow(dead_code)]
pub(super) key1: Secret<String>,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we have dead code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

solved

@bsayak03 bsayak03 force-pushed the jpmorgan-cards branch 2 times, most recently from d7378fd to 17d3b31 Compare December 10, 2024 08:40
};
use hyperswitch_interfaces::errors;
use masking::Secret;
use serde::{Deserialize, Serialize};
use strum::Display;
Copy link
Contributor

Choose a reason for hiding this comment

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

Apply match statement on enum

Comment on lines 125 to 133
fn map_capture_method(capture_method: CaptureMethod) -> CapMethod {
match capture_method {
CaptureMethod::Automatic => CapMethod::Now,
CaptureMethod::Manual | CaptureMethod::ManualMultiple => CapMethod::Manual,
CaptureMethod::Scheduled | CaptureMethod::SequentialAutomatic => CapMethod::Delayed,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Only map the capture methods that you actually support

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

complete: item.router_data.request.is_auto_capture()?,
account_number,
expiry,
is_bill_payment: item.router_data.request.is_auto_capture()?,
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove it if it is not mandatory

Comment on lines 327 to 333
pub trait FromTransactionState {
fn from_transaction_state(transaction_state: JpmorganTransactionState) -> Self;
}

pub trait FromResponseStatus {
fn from_response_status(transaction_state: String) -> Self;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to use trait. Directly write down the functions like attempt_status_from_transaction_state and refund_status_from_transaction_state

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

JpmorganResponseStatus::Error => String::from("Error"),
};

let status = common_enums::AttemptStatus::from_response_status(response_status);
Copy link
Contributor

Choose a reason for hiding this comment

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

Directly do status mapping here. No need to define a function for a single time use

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright

req: &RefreshTokenRouterData,
connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
let req = Some(
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok(Some( RequestBuilder::new() .method(Method::Post) .attach_default_headers() .headers(RefreshTokenType::get_headers(self, req, connectors)?) .url(&RefreshTokenType::get_url(self, req, connectors)?) .set_body(RefreshTokenType::get_request_body(self, req, connectors)?) .build(), ))


let connector_response_reference_id = Some(item.response.transaction_id.clone());

let resource_id = ResponseId::ConnectorTransactionId(item.response.transaction_id);
Copy link
Contributor

Choose a reason for hiding this comment

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

these values can be directly assigned while returning

@@ -223,6 +223,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1"
inespay.base_url = "https://apiflow.inespay.com/san/v21"
itaubank.base_url = "https://sandbox.devportal.itau.com.br/"
jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2"
jpmorgan.secondary_base_url= "https://id.payments.jpmorgan.com/am/oauth2/alpha/access_token"
Copy link
Member

Choose a reason for hiding this comment

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

Why are we including the entire URL here instead of just the base URL?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the secondary base url is different from the base url, it is actually used for retrieving the access token, it is a separate api call

Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we be including only the base URL here instead of the entire URL?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

so where should we include the access token flow's url?

Comment on lines +209 to +210
pub(super) _api_key: Secret<String>,
pub(super) _key1: Secret<String>,
Copy link
Member

Choose a reason for hiding this comment

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

Can we remove the underscore prefix if we are using this struct?

If we are not using this struct, why are we not using it?

Copy link
Contributor Author

@bsayak03 bsayak03 Dec 19, 2024

Choose a reason for hiding this comment

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

no we can't remove that struct, because if we do then the Payments Connector Create flow will be successfully even if im using HeaderKey, which shouldn't be the case. I should get a success response only when using the BodyKey

Copy link
Contributor

Choose a reason for hiding this comment

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

These keys are unused because JPMorgan only requires an access token. The access token is obtained during the token flow using the JPMorgan API keys, which are provided through the AccessTokenRequestData.

expiry: Option<ExpiryResponse>,
card_type: Option<String>,
card_type_name: Option<String>,
masked_account_number: Option<String>,
Copy link
Member

Choose a reason for hiding this comment

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

Should we consider wrapping this within a Secret?

Comment on lines 315 to 316
month: Option<i32>,
year: Option<i32>,
Copy link
Member

Choose a reason for hiding this comment

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

Can we wrap this within a Secret? This would otherwise log these values in plaintext...


#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExpiryCapReq {
Copy link
Member

Choose a reason for hiding this comment

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

Wrap fields carrying sensitive information within Secret.


#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SoftMerchantCapReq {
Copy link
Member

Choose a reason for hiding this comment

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

Wrap fields carrying sensitive information within Secret.

let capture_method =
map_capture_method(item.router_data.request.capture_method.unwrap_or_default());
Ok(Self {
capture_method: Some(capture_method?),
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Can you add the ? on the map_capture_method() call instead of here?

pub struct CardCapRes {
pub card_type: Option<String>,
pub card_type_name: Option<String>,
unmasked_account_number: Option<String>,
Copy link
Member

Choose a reason for hiding this comment

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

Wrap this within Secret.

pub struct TransactionData {
payment_type: Option<String>,
status_code: i32,
txn_secret: Option<String>,
Copy link
Member

Choose a reason for hiding this comment

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

Should this field be wrapped within a Secret?

@bsayak03 bsayak03 force-pushed the jpmorgan-cards branch 3 times, most recently from dc135fe to 1666b1a Compare December 19, 2024 07:15
Comment on lines 315 to 316
impl From<JpmorganTransactionStatus> for common_enums::AttemptStatus {
fn from(item: JpmorganTransactionStatus) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this function in code if it is not being used anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

Comment on lines 437 to 447
impl From<JpmorganTransactionState> for common_enums::AttemptStatus {
fn from(item: JpmorganTransactionState) -> Self {
match item {
JpmorganTransactionState::Authorized => Self::Authorized,
JpmorganTransactionState::Closed => Self::Charged,
JpmorganTransactionState::Declined | JpmorganTransactionState::Error => Self::Failure,
JpmorganTransactionState::Pending => Self::Pending,
JpmorganTransactionState::Voided => Self::Voided,
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this function in code if it is not being used anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

@bsayak03 bsayak03 force-pushed the jpmorgan-cards branch 2 times, most recently from 0713c41 to 5d10e37 Compare December 19, 2024 13:52
@@ -120,6 +120,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1"
inespay.base_url = "https://apiflow.inespay.com/san/v21"
itaubank.base_url = "https://sandbox.devportal.itau.com.br/"
jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2"
jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com/am/oauth2/alpha/access_token"
Copy link
Member

Choose a reason for hiding this comment

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

Nit:

Suggested change
jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com/am/oauth2/alpha/access_token"
jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-connector-integration Area: Connector integration C-feature Category: Feature request or enhancement M-api-contract-changes Metadata: This PR involves API contract changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants