paypal / PayPal-PHP-SDK

PHP SDK for PayPal RESTful APIs
https://developer.paypal.com/docs/api/
Other
27 stars 100 forks source link

How to check Billing Agreement payments at each cycle? #1203

Open toshex opened 6 years ago

toshex commented 6 years ago

General information

Issue description

I am mostly using the sample codes. I have created a plan, activated it, created an agreement and activated that. I have the Agreement ID from the callback token and have the agreement object.

However I have 2 questions/concerns with the API:

  1. Since this is a subscription based system, how do I check the user was billed at the next cycle and continue to provide services to them? Do I do it by the failed count, by the last payment date or other paramter? Is there a function in any of the Classses to do such a check or to get these attributes?
  1. When will the 2.0 API go live, and should I be concerned with it - i.e. should I implement 1.13.0 API and not worry or should I try to do everything with 2.0 BETA? (this is a live system so beta is not very desireable)

Thanks.

prakash-gangadharan commented 4 years ago

Hi @toshex, Refer these example code to check Billing Agreement payments details.

Sebbo94BY commented 4 years ago

Related and useful information: Clarify the lifecycle of Billing Agreement and associated webhooks

I also have one question regarding this work flow:

After point 6: BILLING.SUBSCRIPTION.CREATED webhook is sent.

How and when exactly do I handle payments? When the BILLING.SUBSCRIPTION.CREATED webhook is sent? When the PAYMENT.SALE.COMPLETED webhook is sent?

Additional question: Does any webhook expect a specific response (eg. HTTP code 200 and text 'success' or 'failure') or can it be any text and any HTTP code 2xx?


Currently, I'm doing the steps 1 - 5 exactly like in your samples.

At the end the user gets redirected to my shop (after approving the subscription) and there I'll show the user, that the subscription has been successfully enabled and the first payment will be executed in the next few minutes.

Before this message is shown, I execute the agreement as mentioned in step 5: $agreement = $agreement->execute($request->token, $paypal->apiContext);

After that I get the webhook event BILLING.SUBSCRIPTION.CREATED with the state "Pending".

And here I'm a little bit confused. Does this state ever change while the subscription plan is active? Or does it only change when the last cycle finished? When and with which webhook event reports the change? Does the PAYMENT.SALE.COMPLETED webhook contain the updated state or is this a different one? (Usually completed.)

Another question: I saw somewhere the attribute next_billing_date. Which webhook event sents this?

prakash-gangadharan commented 4 years ago

Use billing plans and billing agreements to create an agreement for a recurring PayPal payment for goods or services.

Create Billing Plans

Create Billing Agreements

Sebbo94BY commented 4 years ago

Ok, I already do all the points. Regarding the last point: Is this logic for handling the webhooks then fine?


    public function webhook(Request $request)
    {
        // Get request details
        $request_body = $request->getContent();
        $headers = array_change_key_case($request->headers->all(), CASE_UPPER);

        // Verify webhook signature
        $signature_verification = new \PayPal\Api\VerifyWebhookSignature();
        $signature_verification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO'][0]);
        $signature_verification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID'][0]);
        $signature_verification->setCertUrl($headers['PAYPAL-CERT-URL'][0]);
        $signature_verification->setWebhookId(config('payment-methods.supportedPaymentMethods.paypal.webhook_id'));
        $signature_verification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG'][0]);
        $signature_verification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME'][0]);
        $signature_verification->setRequestBody($request_body);

        try {
            $output = $signature_verification->post($this->apiContext);
        } catch (\Exception $ex) {
            Log::critical($ex->getMessage());
            return response('Error: Could not verify signature.', 500)->header('Content-Type', 'text/plain');
        }

        $status = $output->getVerificationStatus(); // 'SUCCESS' or 'FAILURE'

        switch(strtoupper($status)) {
            case "SUCCESS":
                $json = json_decode($request_body, 1);
            default:
                return response('Forbidden: Invalid signature.', 403)->header('Content-Type', 'text/plain');
        }

        $reference_id = (isset($json['resource']['parent_payment'])) ? $json['resource']['parent_payment'] : null;

        switch($json['event_type']) {
            case "BILLING.PLAN.CREATED":
                // A billing plan (subscription) authorization is created, approved, updated, executed, or a future payment authorization is created.
                try {
                    $subscription = PaypalSubscriptions::where('plan_id', '=', $json['resource']['id'])->firstOrFail();
                } catch (ModelNotFoundException $ex) {
                    return response('Error: Could not find the subscription plan with the id '.$json['resource']['id'].'.', 404)->header('Content-Type', 'text/plain');
                }

                try {
                    $subscription->update(
                        [
                            'payment_definition_id' => $json['resource']['payment_definitions'][0]['id'],
                            'payment_definition_frequency' => $json['resource']['payment_definitions'][0]['frequency'],
                            'payment_definition_amount_currency' => $json['resource']['payment_definitions'][0]['amount']['currency'],
                            'payment_definition_amount_value' => $json['resource']['payment_definitions'][0]['amount']['value'],
                            'payment_definition_cycles' => $json['resource']['payment_definitions'][0]['cycles'],
                            'plan_state' => $json['resource']['state']
                        ]
                    );
                } catch (Exception $ex) {
                    return response('Error: Subscription plan database entry could not be updated by '.$json['event_type'].' (state: '.$json['resource']['state'].').', 500)->header('Content-Type', 'text/plain');
                }

                if (isset($json['resource']['agreement_details']['next_billing_date'])) {
                        // Save transaction to database
                }

                return response($status, 200)->header('Content-Type', 'text/plain');
            case "BILLING.PLAN.UPDATED":
                // A billing plan (subscription) authorization is created, approved, updated, executed, or a future payment authorization is created.
                try {
                    $subscription = PaypalSubscriptions::where('plan_id', '=', $json['resource']['id'])->firstOrFail();
                } catch (ModelNotFoundException $ex) {
                    return response('Error: Could not find the subscription plan with the id '.$json['resource']['id'].'.', 404)->header('Content-Type', 'text/plain');
                }

                try {
                    $subscription->update(
                        [
                            'payment_definition_id' => $json['resource']['payment_definitions'][0]['id'],
                            'payment_definition_frequency' => $json['resource']['payment_definitions'][0]['frequency'],
                            'payment_definition_amount_currency' => $json['resource']['payment_definitions'][0]['amount']['currency'],
                            'payment_definition_amount_value' => $json['resource']['payment_definitions'][0]['amount']['value'],
                            'payment_definition_cycles' => $json['resource']['payment_definitions'][0]['cycles'],
                            'plan_state' => $json['resource']['state']
                        ]
                    );
                } catch (Exception $ex) {
                    return response('Error: Subscription plan database entry could not be updated by '.$json['event_type'].' (state: '.$json['resource']['state'].').', 500)->header('Content-Type', 'text/plain');
                }

                return response($status, 200)->header('Content-Type', 'text/plain');
            case "PAYMENT.SALE.COMPLETED":
                // A sale (PayPal Billing Agreement) completed.
                if(strtoupper($json['resource']['state']) != "COMPLETED") {
                    return response('Forbidden: Payment is not completed yet. '.$json['event_type'].' (state: '.$json['resource']['state'].').', 403)->header('Content-Type', 'text/plain');                    
                }

                try {
                    $subscription = PaypalSubscriptions::where('subscription_id', '=', $json['resource']['billing_agreement_id'])->firstOrFail();
                } catch (ModelNotFoundException $ex) {
                    return response('Error: Could not find related PayPal subscription with ID '.$json['resource']['billing_agreement_id'].'.', 500)->header('Content-Type', 'text/plain');
                }

                // Save transaction to database

                return response($status, 200)->header('Content-Type', 'text/plain');
            default:
                Log::info("PayPal Webhook event '" . $json['event_type'] . "' not handled. Ignoring... (Data: " . json_encode($json) . ")");
        }

        return response('Error: Invalid webhook.', 500)->header('Content-Type', 'text/plain');
    }
Sebbo94BY commented 4 years ago

@prakash-gangadharan is my above logic fine for handling these webhooks?

Sebbo94BY commented 4 years ago

@prakash-gangadharan your feedback is required. I would like to finish the implementation. :)

stell commented 4 years ago

Hey, I'm working on a similar thing and have the same questions you did. Did you get this working? Are the Webhooks triggering after every recurring payment or did you end up using the Subscription API from PayPal Plus?