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

The payment provider paypal is of type PaymentProcessor. PaymentProcessors cannot update payment session data. #4166

Closed
davide-guariento-spot opened this issue May 24, 2023 · 12 comments

Comments

@davide-guariento-spot
Copy link

davide-guariento-spot commented May 24, 2023

Bug report

Describe the bug

I am following the Checkout Flow with the PayPal plugin but I get the following JSON when I reach the Update Payment Session step.

Code

    await medusaClient.carts.updatePaymentSession($cart.id, "paypal", {
      data: {
        data: {
          ...authorization,
        },
      },
    });

Request
http://localhost:9000/store/carts/cart_01H16S35WZ6AJ8F76KF2B78CYE/payment-sessions/paypal

Response

{
    "type": "not_allowed",
    "message": "The payment provider paypal is of type PaymentProcessor. PaymentProcessors cannot update payment session data."
}

System information

Medusa version (including plugins):

Backend

"@babel/preset-typescript": "^7.21.4",
"@medusajs/admin": "^5.0.0",
"@medusajs/cache-inmemory": "^1.8.6",
"@medusajs/cache-redis": "^1.8.6",
"@medusajs/event-bus-local": "^1.9.3",
"@medusajs/event-bus-redis": "^1.8.6",
"@medusajs/medusa": "^1.11.0",
"@medusajs/medusa-cli": "^1.3.14",
"babel-preset-medusa-package": "^1.1.13",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"express": "^4.17.2",
"medusa-fulfillment-manual": "^1.1.37",
"medusa-interfaces": "^1.3.7",
"medusa-payment-manual": "^1.0.23",
"medusa-payment-paypal": "^5.0.0",
"medusa-payment-stripe": "^5.0.0",
"typeorm": "^0.3.16"

Frontend

"@medusajs/medusa-js": "^5.0.0",
"@paypal/paypal-js": "^5.1.6",
"@paypal/react-paypal-js": "^7.8.3",

Node.js version: v18.15.0
Database: PostgreSQL (v15.3)
Operating system: macOs Monterey (Version 12.5)
Browser (if relevant): Google Chrome (Version 113.0.5672.126)

Steps to reproduce the behavior

  1. Add Shipping Address;
  2. Add Shipping Option;
  3. Create Payment Session;
  4. Select Payment Session;
  5. Update Payment Session.

Expected behavior

It should update the Payment Session inside the Cart object without errors.

Code snippets

Checkout code

  const checkoutFlow = async () => {
    await setAddressAsync();

    await setShippingOptionAsync();

    await createPaymentSessionAsync();
  };

  const setAddressAsync = async (): Promise<void> => {
    // todo validation

    const address = shippingAddress as Address;

    if ($cart) {
      await medusaClient.carts.update($cart.id, {
        shipping_address: {
          ...address,
          country_code: "it",
        },
        email,
      });
    }
  };

  const setShippingOptionAsync = async (): Promise<void> => {
    // todo validation

    const shippingOption = selectedShippingOption as ShippingOption;

    if ($cart) {
      await medusaClient.carts.addShippingMethod($cart.id, {
        option_id: shippingOption.id,
      });
    }
  };

  const createPaymentSessionAsync = async (): Promise<void> => {
    // todo validation

    if ($cart) {
      await medusaClient.carts.createPaymentSessions($cart.id);
    }
  };

PayPal.tsx

import { useState } from "react";

import { useStore } from "@nanostores/react";
import { PayPalButtons, PayPalScriptProvider } from "@paypal/react-paypal-js";

import { medusaClient } from "../scripts/medusa";
import { cart } from "../store/cartStore";

import type {
  CreateOrderActions,
  CreateOrderData,
  OnApproveActions,
  OnApproveData,
  OrderResponseBody,
} from "@paypal/paypal-js";

type PaypalProps = {
  clientId: string;
};

const Paypal = ({ clientId }: PaypalProps) => {
  const payPalProviderConfig = {
    "client-id": clientId,
    currency: "EUR",
    intent: "authorize",
  };

  const [errorMessage, setErrorMessage] = useState("");
  const [processing, setProcessing] = useState(false);
  const $cart = useStore(cart);

  const handlePayment = async (
    _: OnApproveData,
    actions: OnApproveActions
  ): Promise<void> => {
    if (!$cart) {
      return;
    }

    const authorization: OrderResponseBody | undefined =
      await actions.order?.authorize();

    if (authorization?.status !== "COMPLETED") {
      setErrorMessage(`An error occurred, status: ${authorization?.status}`);
      setProcessing(false);
      return;
    }

    const response = await medusaClient.carts.setPaymentSession($cart.id, {
      provider_id: "paypal",
    });

    if (!response.cart) {
      setProcessing(false);
      return;
    }

    await medusaClient.carts.updatePaymentSession($cart.id, "paypal", {
      data: {
        data: {
          ...authorization,
        },
      },
    });

    const result = await medusaClient.carts.complete($cart.id);

    if (!result.data || result.type !== "order") {
      setProcessing(false);
      return;
    }

    // order successful
    alert("success");
  };

  const createOrder = async (
    data: CreateOrderData,
    actions: CreateOrderActions
  ): Promise<string> => {
    return actions.order.create({
      purchase_units: [
        {
          amount: {
            value: "79.99",
          },
        },
      ],
    });
  };

  return (
    <div style={{ marginTop: "10px", marginLeft: "10px" }}>
      {cart !== undefined && (
        <PayPalScriptProvider options={payPalProviderConfig}>
          {errorMessage && (
            <span className="text-rose-500 mt-4">{errorMessage}</span>
          )}
          <PayPalButtons
            style={{ layout: "horizontal" }}
            createOrder={createOrder}
            onApprove={handlePayment}
            disabled={processing}
          />
        </PayPalScriptProvider>
      )}
    </div>
  );
};

export default Paypal;

Context

I correctly loaded the PayPal plugin inside the Region through the Admin Panel.

@quiloos39
Copy link

quiloos39 commented May 24, 2023

I wrote custom payment provider called StripeExtended which i am having same problem.

/services/stripe-extended.ts

import {
  CartService,
  CustomerService,
  PaymentProcessorContext,
  PaymentProcessorError,
  PaymentProcessorSessionResponse,
  PaymentSessionStatus,
} from "@medusajs/medusa";
import { AbstractPaymentProcessor } from "@medusajs/medusa/dist/interfaces/payment-processor";
import Stripe from "stripe";

class StripeExtended extends AbstractPaymentProcessor {
  static identifier: string = "StripeExtended";
  public stripe_: Stripe;
  protected customerService_: CustomerService;
  protected cartService_: CartService;
  protected stripeOptions: {
    secret_key: string;
    success_url: string;
    options: Record<string, unknown>;
  };

  constructor(container, _options) {
    super(container);
    // ...
  }
  // ...

POST /stripe/hook API

          // ...
          await cartServiceTx.setPaymentSessions(cart.id);
          await cartServiceTx.setPaymentSession(cart.id, "StripeExtended");
          await cartServiceTx.updatePaymentSession(cart.id, {
            ...checkout,
          });
          // ...
Error: The payment provider StripeExtended is of type PaymentProcessor. PaymentProcessors cannot update payment session data

also could be related with #4124

@lrocher91
Copy link

lrocher91 commented Jun 6, 2023

Can we have an update on this please ? 🙏

Seems linked to #3722 and #4076

Maybe it's just unclear in the documentation why it is required for some PaymentProcessor and not for others.

And if it's not needed for Paypal, it should be removed from the example in the documentation

PS: In the NextJs storefront it seems that the updatePaymentSession call is omitted so i guess it's not necessary. I think it should be explained here

@adrien2p
Copy link
Member

adrien2p commented Jun 7, 2023

What payment session are you trying to update? I would like to understand a bit more about what needs to be updated in the payment flow. As far as I can see, everything should be up to date in the payment provider no? Also, if by any chance the customer goes back to the store to update his cart, the payment will be updated with the updated amount

@lrocher91
Copy link

Hi @adrien2p ,

This piece of code come from the Medusa Paypal documentation

const handlePayment = (data, actions) => {
    actions.order.authorize().then(async (authorization) => {
      if (authorization.status !== "COMPLETED") {
        setErrorMessage(
          `An error occurred, status: ${authorization.status}`
        )
        setProcessing(false)
        return
      }

      const response = await client
        .carts
        .setPaymentSession(cart.id, {
          "processor_id": "paypal",
        })

      if (!response.cart) {
        setProcessing(false)
        return
      }

      // here we updatePaymentSession with paypal authorization details
      // to update the payment session status with paypal result but it throws the error above
      await client
        .carts
        .updatePaymentSession(cart.id, "paypal", {
        data: {
          data: {
            ...authorization,
          },
        },
      })

      const { data } = await client.carts.complete(cart.id)

      if (!data || data.object !== "order") {
        setProcessing(false)
        return
      }
      
      // order successful
      alert("success")
    })
  }

Maybe i missunderstood but for me at this moment, the payment is done, so the cart can be deleted and the order created.

@CervarlCG
Copy link

Is there any update here.

On my checkout flow basically we send the CC to our payment processor and it returns a token that we must to send to our backend to process the payment in the backend but i'm having this issue when trying to set the token in the session

@quiloos39
Copy link

I didn't checked once i found workaround but what i did was i wrote middleware which injects context to request which is then accessible inside payment processor as context argument

@ripvannwinkler
Copy link

ripvannwinkler commented Jun 30, 2023

What payment session are you trying to update? I would like to understand a bit more about what needs to be updated in the payment flow. As far as I can see, everything should be up to date in the payment provider no? Also, if by any chance the customer goes back to the store to update his cart, the payment will be updated with the updated amount

I'm writing a payment provider that uses hosted checkout. When the call to completeCart returns REQUIRES_MORE, I use the hosted checkout URL stored in the payment session to redirect the user to the payment provider's hosted checkout page. When the user comes back, I need to update the payment session to reflect the authorized status from the external checkout process. How do you see that working if updatePaymentSession is not the supported use case here?

Further, looking at this code:

  async updateSessionData(
    paymentSession: PaymentSession,
    data: Record<string, unknown>
  ): Promise<PaymentSession> {
    return await this.atomicPhase_(async (transactionManager) => {
      const session = await this.retrieveSession(paymentSession.id)

      const provider = this.retrieveProvider(paymentSession.provider_id)

      if (provider instanceof AbstractPaymentProcessor) {
        throw new MedusaError(
          MedusaError.Types.NOT_ALLOWED,
          `The payment provider ${paymentSession.provider_id} is of type PaymentProcessor. PaymentProcessors cannot update payment session data.`
        )
      } else {
        session.data = await provider
          .withTransaction(transactionManager)
          .updatePaymentData(paymentSession.data, data)
        session.status = paymentSession.status
      }

      const sessionRepo = transactionManager.withRepository(
        this.paymentSessionRepository_
      )
      return await sessionRepo.save(session)
    })
  }

Is there ever a scenario where provider would not be expected to be an instance of AbstractPaymentProcessor? I'm trying to understand the purpose of this check and how to work around it.

@adrien2p
Copy link
Member

adrien2p commented Jul 3, 2023

working on it #4442

@ripvannwinkler
Copy link

Awesome, thanks! For what it's worth, I ended up dropping into low level code using the paymentSessionRepository to update the record directly. I know this probably isn't recommended, but I didn't have any other way to complete the task at the time.

In my web hook subscriber:

	private async getPaymentSession(checkoutSessionId: string): Promise<PaymentSession> {
		return await this.paymentSessionRepository
			.createQueryBuilder('payment_session')
			.where(`payment_session.data->>'checkoutSessionId' = :checkoutSessionId`, {
				checkoutSessionId,
			})
			.getOneOrFail();
	}

	private async handleAuthorized(data: PaymentWebHook): Promise<void> {
		const session = await this.getPaymentSession(data.checkoutSessionId);

		await this.paymentSessionRepository.save({
			...session,
			data: {
				...session.data,
				status: 'authorized',
			} as CheckoutSessionData as any,
		});
	}

@olivermrbl
Copy link
Contributor

If we publish a snapshot with the changes in #4442, can one of you confirm that everything works as expected on your side?

cc @ripvannwinkler @davide-guariento-spot

Copy link
Contributor

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 3 days.

@github-actions github-actions bot added the Stale label Dec 27, 2024
Copy link
Contributor

github-actions bot commented Jan 7, 2025

This issue was closed because it has been stalled for 3 days with no activity.

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jan 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants