FLUX-SE / PayumStripe

Payum Stripe gateways (with SCA support)
MIT License
28 stars 15 forks source link

Cancel the payment during the checkout status not correct #36

Closed oulfr closed 1 year ago

oulfr commented 1 year ago

When I cancel the payment during the checkout of a Stripe Checkout session by hiting the return link, the status of my payment model does not get set to 'cancel.' I have reviewed the details column of payment table, and the status is being updated correctly by the hook, but it is not reflected in the status attribute of the payment table.

The problem is the status is considered as new when the user click the return link ? for me this is a cancelled status.

I have resolved the issue by calling : $gateway->execute(new GetHumanStatus($payment)); in done script

Prometee commented 1 year ago

Hello @oulfr !

Can you give more information about your issue, especially I will need you to confirm that you are using the last version of this Payum library as well as the versions of the bundle and the Sylius plugin (if you are using it).

If you are using the Sylius plugin this behaviour is covered by this BeHat test : https://github.com/FLUX-SE/SyliusPayumStripePlugin/blob/master/features/shop/paying_with_stripe_session_checkout_during_checkout.feature#L29 When a payment is canceled Sylius is duplicating the Payment and set the status to new for the customer to be able to make another attempt. This behaviour can explain what you get, you can check all Payment entities related to your order and see that their is a canceled one.

Also make sure you are using Stripe webhook, to double confirm the Payment status, when you come back from the Stripe portal.

oulfr commented 1 year ago

Hello @Prometee, thank you for your replay. I use this package not with Syluis but with a Laravel project, and it works great. The problem come when the user cancel the link in the Stripe checkout page by clicking the return link. I have checked my payment database table, and the status in the detail table is marked as "cancelled," but the status column in the same table is not correct; it still shows "new." This status is calculated with an extension similar to the Syluis one. To correct this issue, I need to modify my "done" script as follows:

    public function done($payumToken, callable $closure)
        {
            return $this->receive($payumToken, function ($gateway, $token, $httpRequestVerifier) use ($closure) {
                $gateway->execute($status = new GetHumanStatus($token));
                $payment = $status->getFirstModel();
                $gateway->execute($status = new GetHumanStatus($payment));// this is the line that correct the issue and sync the status correctlly between detail and status columns
                return $closure($status, $payment, $gateway, $token, $httpRequestVerifier);
            });
        }

I dont know if it's a bug or just where the status is calculated must be in the done script. Just one question when I run the $gateway->execute($status = new GetHumanStatus($payment)); all actions that support GetHumanStatus is runned again ?

Prometee commented 1 year ago

Normally this action : https://github.com/Payum/Payum/blob/master/src/Payum/Core/Action/ExecuteSameRequestWithModelDetailsAction.php is doing exactly what you've done. If it's not the case, it will depends on how your Laravel app is handling Payum and if it implements the interfaces mentioned in the supports method. For example, here is how Sylius is handling it : https://github.com/Sylius/Sylius/blob/1.13/src/Sylius/Bundle/PayumBundle/Action/ExecuteSameRequestWithPaymentDetailsAction.php

To be able to reproduce your issue, can you paste the json details array (don't forget to remove sensitive info) ? Normally the details should comply with those tests : https://github.com/FLUX-SE/PayumStripe/blob/master/tests/Action/StatusPaymentIntentActionTest.php#L145-L181

oulfr commented 1 year ago

This is the order of execusion execution #:

                              [2023-06-12 12:37:41] local.DEBUG: [Payum] 1# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(GetHumanStatus{model: PayumToken})  
                [2023-06-12 12:37:41] local.DEBUG: [Payum] 2# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(GetHumanStatus{model: PayumPayment})  
                [2023-06-12 12:37:41] local.DEBUG: [Payum] 3# FluxSE\PayumStripe\Action\StatusPaymentIntentAction::execute(GetHumanStatus{model: ArrayObject})  
                [2023-06-12 12:37:41] local.DEBUG: [Payum] 4# FluxSE\PayumStripe\Action\SyncAction::execute(Sync{model: ArrayObject})  
                [2023-06-12 12:37:41] local.DEBUG: [Payum] 5# FluxSE\PayumStripe\Action\Api\Resource\RetrievePaymentIntentAction::execute(RetrievePaymentIntent{model: string})  
                [2023-06-12 12:37:41] local.DEBUG: [Payum] 2# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(GetHumanStatus{model: PayumPayment})  
                [2023-06-12 12:37:41] local.DEBUG: [Payum] 3# FluxSE\PayumStripe\Action\StatusPaymentIntentAction::execute(GetHumanStatus{model: ArrayObject})  
                [2023-06-12 12:37:41] local.DEBUG: [Payum] 4# FluxSE\PayumStripe\Action\SyncAction::execute(Sync{model: ArrayObject})  
                [2023-06-12 12:37:41] local.DEBUG: [Payum] 5# FluxSE\PayumStripe\Action\Api\Resource\RetrievePaymentIntentAction::execute(RetrievePaymentIntent{model: string})  
                [2023-06-12 12:37:42] local.DEBUG: [Payum] 2# Payum\Core\Bridge\PlainPhp\Action\GetHttpRequestAction::execute(GetHttpRequest)  
                [2023-06-12 12:37:42] local.DEBUG: [Payum] 2# FluxSE\PayumStripe\Action\Api\Resource\AllSessionAction::execute(AllSession{model: ArrayObject})  
                [2023-06-12 12:37:42] local.DEBUG: [Payum] 2# FluxSE\PayumStripe\Action\Api\Resource\ExpireSessionAction::execute(ExpireSession{model: string})  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 1# FluxSE\PayumStripe\Action\NotifyAction::execute(Notify{model: NULL})  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 2# FluxSE\PayumStripe\Action\Api\ResolveWebhookEventAction::execute(ResolveWebhookEvent)  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 3# Payum\Core\Bridge\PlainPhp\Action\GetHttpRequestAction::execute(GetHttpRequest)  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 3# FluxSE\PayumStripe\Action\Api\ConstructEventAction::execute(ConstructEvent{model: string})  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 2# FluxSE\PayumStripe\Action\Api\WebhookEvent\PaymentIntentCanceledAction::execute(WebhookEvent{model: EventWrapper})  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 3# Payum\Core\Action\GetTokenAction::execute(GetToken)  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 3# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(Notify{model: PayumToken})  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 4# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(Notify{model: PayumPayment})  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 5# FluxSE\PayumStripe\Action\NotifyAction::execute(Notify{model: ArrayObject})  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 6# FluxSE\PayumStripe\Action\SyncAction::execute(Sync{model: ArrayObject})  
                [2023-06-12 12:37:43] local.DEBUG: [Payum] 7# FluxSE\PayumStripe\Action\Api\Resource\RetrievePaymentIntentAction::execute(RetrievePaymentIntent{model: string})  

And this is the detail json:

  {
    "id": "pi_3NI9ZGCLXE6pdYwb1J78Xu36",
    "object": "payment_intent",
    "amount": 2000,
    "amount_capturable": 0,
    "amount_details": {
      "tip": []
    },
    "amount_received": 0,
    "application": null,
    "application_fee_amount": null,
    "automatic_payment_methods": null,
    "canceled_at": 1686571799,
    "cancellation_reason": "automatic",
    "capture_method": "automatic",
    "charges": {
      "object": "list",
      "data": [],
      "has_more": false,
      "total_count": 0,
      "url": "/v1/charges?payment_intent=pi_3NI9ZGCLXE6pdYwb1J78Xu36"
    },
    "client_secret": "pi_XXX",
    "confirmation_method": "automatic",
    "created": 1686571794,
    "currency": "eur",
    "customer": null,
    "description": null,
    "invoice": null,
    "last_payment_error": null,
    "latest_charge": null,
    "livemode": false,
    "metadata": {
      "token_hash": "4dDkTXwxOLux-Is6r6RDnuK6YuiVleL2dVJbC2vu5qo"
    },
    "next_action": null,
    "on_behalf_of": null,
    "payment_method": null,
    "payment_method_options": {
      "alipay": [],
      "bancontact": {
        "preferred_language": "en"
      },
      "card": {
        "installments": null,
        "mandate_options": null,
        "network": null,
        "request_three_d_secure": "automatic"
      },
      "eps": [],
      "giropay": [],
      "ideal": [],
      "klarna": {
        "preferred_locale": null
      },
      "link": {
        "persistent_token": null
      },
      "p24": [],
      "paypal": {
        "preferred_locale": null,
        "reference": null
      },
      "sepa_debit": [],
      "wechat_pay": {
        "app_id": null,
        "client": "web"
      }
    },
    "payment_method_types": [
      "card",
      "bancontact",
      "eps",
      "giropay",
      "ideal",
      "p24",
      "sepa_debit",
      "alipay",
      "klarna",
      "wechat_pay",
      "link",
      "paypal"
    ],
    "processing": null,
    "receipt_email": null,
    "review": null,
    "setup_future_usage": null,
    "shipping": null,
    "source": null,
    "statement_descriptor": null,
    "statement_descriptor_suffix": null,
    "status": "canceled",
    "transfer_data": null,
    "transfer_group": null
  }
oulfr commented 1 year ago

I think the issue is after FluxSE\PayumStripe\Action\StatusPaymentIntentAction there is no GetHumanStatus executed

Prometee commented 1 year ago

This action is having its supports method from AbstractStatusAction. Like I already say, a test is covering what you have into your done method : https://github.com/FLUX-SE/PayumStripe/blob/master/tests/Action/StatusPaymentIntentActionTest.php#L154

Prometee commented 1 year ago

About the call logs you sent, there is two different things here. The first one is the call of:

$gateway->execute($status = new GetHumanStatus($token));

Formated logs :

1# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(GetHumanStatus{model: PayumToken})
  2# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(GetHumanStatus{model: PayumPayment})
    3# FluxSE\PayumStripe\Action\StatusPaymentIntentAction::execute(GetHumanStatus{model: ArrayObject})
      4# FluxSE\PayumStripe\Action\SyncAction::execute(Sync{model: ArrayObject})
        5# FluxSE\PayumStripe\Action\Api\Resource\RetrievePaymentIntentAction::execute(RetrievePaymentIntent{model: string})
  2# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(GetHumanStatus{model: PayumPayment})
    3# FluxSE\PayumStripe\Action\StatusPaymentIntentAction::execute(GetHumanStatus{model: ArrayObject})
      4# FluxSE\PayumStripe\Action\SyncAction::execute(Sync{model: ArrayObject})
        5# FluxSE\PayumStripe\Action\Api\Resource\RetrievePaymentIntentAction::execute(RetrievePaymentIntent{model: string})
  2# Payum\Core\Bridge\PlainPhp\Action\GetHttpRequestAction::execute(GetHttpRequest)
  2# FluxSE\PayumStripe\Action\Api\Resource\AllSessionAction::execute(AllSession{model: ArrayObject})
  2# FluxSE\PayumStripe\Action\Api\Resource\ExpireSessionAction::execute(ExpireSession{model: string})

1# No action will be supported that why the request is forwarded to ExecuteSameRequestWithModelDetailsAction 2# the token is explored to get the underlying Payment and then GetHumanStatus is executed again with the Payment 3# This time an action support this request StatusPaymentIntentAction 4# Like every Sync*Action in this lib a Sync request is made to get fresh data from Stripe API 5# That why here we have a call to retrieve the PaymentIntent data from its ID. 2# This line I don't get why there is 2 execution of the same request here.

What I can see here is that your payment was not canceled during the first GetHumanStatus meaning it did not pass the if statements present in the Payum extension which expires the CheckoutSession : https://github.com/FLUX-SE/PayumStripe/blob/master/src/Extension/StripeCheckoutSession/AbstractCancelUrlExtension.php#L27 At this point your PaiementIntent seems to be ok and it does not require to be canceled. Then during the second GetHumanStatus (the one with the Payment model) the if statements are passed allowing the extension to expire the related CheckoutSession making the PaymentIntent being canceled.

The second call graph is the Webhook call, this call is expected because ExpireSessionAction has been executed.

1# FluxSE\PayumStripe\Action\NotifyAction::execute(Notify{model: NULL})
  2# FluxSE\PayumStripe\Action\Api\ResolveWebhookEventAction::execute(ResolveWebhookEvent)
    3# Payum\Core\Bridge\PlainPhp\Action\GetHttpRequestAction::execute(GetHttpRequest)
    3# FluxSE\PayumStripe\Action\Api\ConstructEventAction::execute(ConstructEvent{model: string})
  2# FluxSE\PayumStripe\Action\Api\WebhookEvent\PaymentIntentCanceledAction::execute(WebhookEvent{model: EventWrapper})
    3# Payum\Core\Action\GetTokenAction::execute(GetToken)
    3# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(Notify{model: PayumToken})
      4# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(Notify{model: PayumPayment})
        5# FluxSE\PayumStripe\Action\NotifyAction::execute(Notify{model: ArrayObject})
          6# FluxSE\PayumStripe\Action\SyncAction::execute(Sync{model: ArrayObject})
            7# FluxSE\PayumStripe\Action\Api\Resource\RetrievePaymentIntentAction::execute(RetrievePaymentIntent{model: string})
oulfr commented 1 year ago

Thank you, @Prometee, for your explanation. It has helped me to better understand the flow of execution. However, regarding my issue, where you suggested calculating the column status to be synced with the detail one, I have created an extension. Unfortunately, it is not working because it does not receive the fresh data when the execution is finished. Additionally, the context in the case of the hook is empty. Here is the log with data in both cases: the first one is in the same user context, and the second one is called by the hook. As you can see, the data in the context of the hook is empty. What is strange is that the detail column is correctly updated, but not my custom status.

          [2023-06-12 15:10:36] local.DEBUG: [Payum] 1# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(GetHumanStatus{model: PayumToken})  
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 2# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(GetHumanStatus{model: PayumPayment})  
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 3# FluxSE\PayumStripe\Action\StatusPaymentIntentAction::execute(GetHumanStatus{model: ArrayObject})  
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 4# FluxSE\PayumStripe\Action\SyncAction::execute(Sync{model: ArrayObject})  
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 5# FluxSE\PayumStripe\Action\Api\Resource\RetrievePaymentIntentAction::execute(RetrievePaymentIntent{model: string})  
          [2023-06-12 15:10:36] local.DEBUG: Extension ********   
          [2023-06-12 15:10:36] local.DEBUG: Payum\Core\Request\GetHumanStatus::__set_state(array(
             'model' => 
            Payum\Core\Bridge\Spl\ArrayObject::__set_state(array(
               'id' => 'pi_3NICNyCLXE6pdYwb0kfQVs81',
               'object' => 'payment_intent',

               [...]

               'status' => 'requires_payment_method',
               'transfer_data' => NULL,
               'transfer_group' => NULL,
            )),
             'firstModel' => 
            Selliofy\PaymentGateway\Models\PayumPayment::__set_state(array(
               'connection' => 'tenant',
               'table' => 'payum_payments',
               'primaryKey' => 'id',
               'keyType' => 'int',
               'incrementing' => true,
               'with' => 
              array (
              ),
               'withCount' => 
              array (
              ),
               'preventsLazyLoading' => false,
               'perPage' => 15,
               'exists' => true,
               'wasRecentlyCreated' => false,
               'escapeWhenCastingToString' => false,
               'attributes' => 
              array (
                'id' => 1,
                'number' => '6487356162e6f',
                'description' => 'Charge for new customer',
                'clientId' => 'anId',
                'clientEmail' => 'foo@example.com',
                'totalAmount' => '2000',
                'currencyCode' => 'EUR',
                'status' => NULL,
                'details' => '{"id":"pi_3NICNyCLXE6pdYwb0kfQVs81","object":"payment_intent","status":"requires_payment_method"}',
                'created_at' => '2023-06-12 15:10:25',
                'updated_at' => '2023-06-12 15:10:27',
              ),
               'original' => 
              array (
                'id' => 1,
                'number' => '6487356162e6f',
                'description' => 'Charge for new customer',
                'clientId' => 'anId',
                'clientEmail' => 'foo@example.com',
                'totalAmount' => '2000',
                'currencyCode' => 'EUR',
                'status' => NULL,
                'details' => '{"id":"pi_3NICNyCLXE6pdYwb0kfQVs81","object":"payment_intent","status":"requires_payment_method"}',
                'created_at' => '2023-06-12 15:10:25',
                'updated_at' => '2023-06-12 15:10:27',
              ),
               'changes' => 
              array (
              ),
               'casts' => 
              array (
              ),
               'classCastCache' => 
              array (
              ),
               'attributeCastCache' => 
              array (
              ),
               'dateFormat' => NULL,
               'appends' => 
              array (
              ),
               'dispatchesEvents' => 
              array (
              ),
               'observables' => 
              array (
              ),
               'relations' => 
              array (
              ),
               'touches' => 
              array (
              ),
               'timestamps' => true,
               'usesUniqueIds' => false,
               'hidden' => 
              array (
              ),
               'visible' => 
              array (
              ),
               'fillable' => 
              array (
              ),
               'guarded' => 
              array (
                0 => '*',
              ),
               'creditCard' => NULL,
               'bankAccount' => NULL,
            )),
             'token' => 
            Selliofy\PaymentGateway\Models\PayumToken::__set_state(array(
               'connection' => 'tenant',
               'table' => 'payum_tokens',
               'primaryKey' => 'hash',
               'keyType' => 'int',
               'incrementing' => false,
               'with' => 
              array (
              ),
               'withCount' => 
              array (
              ),
               'preventsLazyLoading' => false,
               'perPage' => 15,
               'exists' => true,
               'wasRecentlyCreated' => false,
               'escapeWhenCastingToString' => false,
               'attributes' => 
              array (
                'id' => 1,
                'hash' => 'X8dZQ5ufGKBVCiryDhcPa30z4VQjhXeVx4SGf63kDSI',
                'targetUrl' => 'http://root.test.com/payment/done/X8dZQ5ufGKBVCiryDhcPa30z4VQjhXeVx4SGf63kDSI',
                'afterUrl' => NULL,
                'gatewayName' => 'stripe_checkout_session',
                'details' => 'O:25:"Payum\\Core\\Model\\Identity":2:{i:0;i:1;i:1;s:43:"Selliofy\\PaymentGateway\\Models\\PayumPayment";}',
                'created_at' => '2023-06-12 15:10:25',
                'updated_at' => '2023-06-12 15:10:25',
              ),
               'original' => 
              array (
                'id' => 1,
                'hash' => 'X8dZQ5ufGKBVCiryDhcPa30z4VQjhXeVx4SGf63kDSI',
                'targetUrl' => 'http://root.test.com/payment/done/X8dZQ5ufGKBVCiryDhcPa30z4VQjhXeVx4SGf63kDSI',
                'afterUrl' => NULL,
                'gatewayName' => 'stripe_checkout_session',
                'details' => 'O:25:"Payum\\Core\\Model\\Identity":2:{i:0;i:1;i:1;s:43:"Selliofy\\PaymentGateway\\Models\\PayumPayment";}',
                'created_at' => '2023-06-12 15:10:25',
                'updated_at' => '2023-06-12 15:10:25',
              ),
               'changes' => 
              array (
              ),
               'casts' => 
              array (
              ),
               'classCastCache' => 
              array (
              ),
               'attributeCastCache' => 
              array (
              ),
               'dateFormat' => NULL,
               'appends' => 
              array (
              ),
               'dispatchesEvents' => 
              array (
              ),
               'observables' => 
              array (
              ),
               'relations' => 
              array (
              ),
               'touches' => 
              array (
              ),
               'timestamps' => true,
               'usesUniqueIds' => false,
               'hidden' => 
              array (
              ),
               'visible' => 
              array (
              ),
               'fillable' => 
              array (
              ),
               'guarded' => 
              array (
                0 => '*',
              ),
            )),
             'status' => 'new',
          ))  
          [2023-06-12 15:10:36] local.DEBUG: Extension ********   
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 2# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(GetHumanStatus{model: PayumPayment})  
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 3# FluxSE\PayumStripe\Action\StatusPaymentIntentAction::execute(GetHumanStatus{model: ArrayObject})  
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 4# FluxSE\PayumStripe\Action\SyncAction::execute(Sync{model: ArrayObject})  
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 5# FluxSE\PayumStripe\Action\Api\Resource\RetrievePaymentIntentAction::execute(RetrievePaymentIntent{model: string})  
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 2# Payum\Core\Bridge\PlainPhp\Action\GetHttpRequestAction::execute(GetHttpRequest)  
          [2023-06-12 15:10:36] local.DEBUG: [Payum] 2# FluxSE\PayumStripe\Action\Api\Resource\AllSessionAction::execute(AllSession{model: ArrayObject})  
          [2023-06-12 15:10:37] local.DEBUG: [Payum] 2# FluxSE\PayumStripe\Action\Api\Resource\ExpireSessionAction::execute(ExpireSession{model: string})  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 1# FluxSE\PayumStripe\Action\NotifyAction::execute(Notify{model: NULL})  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 2# FluxSE\PayumStripe\Action\Api\ResolveWebhookEventAction::execute(ResolveWebhookEvent)  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 3# Payum\Core\Bridge\PlainPhp\Action\GetHttpRequestAction::execute(GetHttpRequest)  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 3# FluxSE\PayumStripe\Action\Api\ConstructEventAction::execute(ConstructEvent{model: string})  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 2# FluxSE\PayumStripe\Action\Api\WebhookEvent\PaymentIntentCanceledAction::execute(WebhookEvent{model: EventWrapper})  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 3# Payum\Core\Action\GetTokenAction::execute(GetToken)  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 3# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(Notify{model: PayumToken})  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 4# Payum\Core\Action\ExecuteSameRequestWithModelDetailsAction::execute(Notify{model: PayumPayment})  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 5# FluxSE\PayumStripe\Action\NotifyAction::execute(Notify{model: ArrayObject})  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 6# FluxSE\PayumStripe\Action\SyncAction::execute(Sync{model: ArrayObject})  
          [2023-06-12 15:10:38] local.DEBUG: [Payum] 7# FluxSE\PayumStripe\Action\Api\Resource\RetrievePaymentIntentAction::execute(RetrievePaymentIntent{model: string})  
          [2023-06-12 15:10:38] local.DEBUG: Extension ********   
          [2023-06-12 15:10:38] local.DEBUG: Payum\Core\Request\Notify::__set_state(array(
             'model' => NULL,
             'firstModel' => NULL,
             'token' => NULL,
          ))  
          [2023-06-12 15:10:38] local.DEBUG: Extension ********   
oulfr commented 1 year ago

and this is the extension, in case of hook it will not do any thing because the context is empty

https://github.com/Sylius/SyliusPayumBundle/blob/master-old/Extension/UpdatePaymentStateExtension.php

And The status column is synced with the detail one if i change the Core ExecuteSameRequestWithModelDetailsAction like that:

          use Illuminate\Support\Facades\Log;
          use Payum\Core\Bridge\Spl\ArrayObject;
          use Payum\Core\Exception\RequestNotSupportedException;
          use Payum\Core\GatewayAwareInterface;
          use Payum\Core\GatewayAwareTrait;
          use Payum\Core\Model\DetailsAggregateInterface;
          use Payum\Core\Model\DetailsAwareInterface;
          use Payum\Core\Model\ModelAggregateInterface;
          use Payum\Core\Model\ModelAwareInterface;
          use Payum\Core\Model\PaymentInterface;

          class ExecuteSameRequestWithModelDetailsAction implements ActionInterface, GatewayAwareInterface
          {
              use GatewayAwareTrait;

              /**
               * {@inheritDoc}
               *
               * @param ModelAggregateInterface|ModelAwareInterface $request
               */
              public function execute($request)
              {
                  RequestNotSupportedException::assertSupports($this, $request);

                  /** @var DetailsAggregateInterface $model */
                  $model = $request->getModel();
                  $details = $model->getDetails();

                  if (is_array($details)) {
                      $details = ArrayObject::ensureArrayObject($details);
                  }

                  $request->setModel($details);
                  try {
                      $this->gateway->execute($request);
                  } finally {
                      if ($model instanceof DetailsAwareInterface) {
                          $model->setDetails($details);
                      }
                      if ($model instanceof PaymentInterface && isset($details['status'])) {
                          $model->setStatus($details['status']);
                      }
                  }
              }

              /**
               * {@inheritDoc}
               */
              public function supports($request)
              {
                  return
                      $request instanceof ModelAggregateInterface &&
                      $request instanceof ModelAwareInterface &&
                      $request->getModel() instanceof DetailsAggregateInterface;
              }
          }
oulfr commented 1 year ago

I think this is not an issue i can close it since the details column is correct for the status column it's easy to compute the value from details without changing the core, thanks

oulfr commented 1 year ago

@Prometee: I have found another extension that you have created for Sylius, and it works great for me. The extension I'm referring to is UpdatePaymentStateExtension.

Currently, the cancel status is being set by the hook. However, when I disable the hook, the status is not being set correctly. In the redirection to the done script, I have the last version of the payment intent, and it shows as cancelled. But the extension doesn't set it correctly. Do you have any ideas on how to resolve this issue?

Prometee commented 1 year ago

Hello @oulfr !

This extension is here to reproduce the Payum Core StorageExtension behaviour for a Sylius payments because Sylius is not using a Payum Payment class with all the related interfaces (Sylius is having some more stricter typed interfaces).

It will help saving the model to the database after each top level Payum request : https://github.com/FLUX-SE/SyliusPayumStripePlugin/blob/master/src/Extension/UpdatePaymentStateExtension.php#L96 (meaning this extension will run after each #1 ... line of your logs).

oulfr commented 1 year ago

@Prometee: Thank you for your replay.

Please help with this following scenario: I need to add a "status" column to my payment table. When a user clicks the return button on the Stripe Checkout session, I want the status to be automatically set to "cancel" without configuring a hook.

can you try this senario, you will see that the status isnot correctlly updated

Prometee commented 1 year ago

It's already covered like I said, read this test : https://github.com/FLUX-SE/PayumStripe/blob/master/tests/Action/StatusPaymentIntentActionTest.php#L145C12-L181

The first one is when a customer ask to cancel the payment and the second is when the customer is going back from the Stripe Checkout portal without filling any info.

I also re-test this behaviour in a raw app using only Payum as a requirement with no issue, I really think that the Payum implementation is missing something in your Laravel project. Are you able to link me the code used by Laravel to use payum/payum or payum/core ?

oulfr commented 1 year ago

this is the project i'm using :

https://github.com/oulfr/laravel-payum

and i use only this dependency:

"require": { "php-http/guzzle7-adapter": "^1.0", "php-http/message-factory": "^1.1", "flux-se/payum-stripe": "^2.0" },

Prometee commented 1 year ago

Ok I see what happens here, you want to cancel a payment when the customer go back from the Stripe Checkout portal, but the customer can normally go back again to the Stripe Portal and finalize his payment ( that's the purpose of this test https://github.com/FLUX-SE/PayumStripe/blob/master/tests/Action/StatusPaymentIntentActionTest.php#L180). What you wanted is instead to cancel the payment, because maybe you need this workflow.

To do so, you can simply create a new action for your project and build it like this :

<?php

declare(strict_types=1);

namespace App\Payum\StripeCheckout\Action;

use FluxSE\PayumStripe\Action\StatusPaymentIntentAction as BaseStatusPaymentIntentAction;
use Stripe\PaymentIntent;

class StatusPaymentIntentAction extends BaseStatusPaymentIntentAction
{

    /**
     * @see https://stripe.com/docs/payments/intents#payment-intent
     */
    protected function isCanceledStatus(string $status): bool
    {
        return in_array($status, [
            PaymentIntent::STATUS_CANCELED,
            PaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD, // Customer use the "cancel_url"
        ], true);
    }

    protected function isNewStatus(string $status): bool
    {
        return in_array($status, [
            PaymentIntent::STATUS_REQUIRES_CONFIRMATION,
            PaymentIntent::STATUS_REQUIRES_ACTION,
        ], true);
    }
}