paypal / PayPal-PHP-SDK

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

How do I delete a plan? #171

Closed MikeHopley closed 9 years ago

MikeHopley commented 9 years ago

The documentation lacks an example of deleting a billing plan.

I have tried using UpdatePlan example, with $patch->setOp('remove'), but this doesn't seem to do anything except return true (upon listing plans, I get the same number).

jziaja commented 9 years ago

Hi @MikeHopley, ideally a Plan.Delete should be performed via an HTTP call using the DELETE verb, but it looks like that's not yet available. I've reached out to the API team to see if this is something they can add support for in the future.

MikeHopley commented 9 years ago

Yes, that would be the best way to do it. Thanks for letting me know; I'll skip over this part for now.

As I carry on working with the API/SDK, I'll continue to post any bugs/quirks I find here. It's great that Paypal is finally giving some love to developers and communicating so well too. :)

jziaja commented 9 years ago

Great to hear, @MikeHopley, and yes, please do continue to post any bugs/quirks you run into. :)

The API team got back to me this morning, and apparently you can delete a billing plan - you came very close with your solution. :) You'll want to do the following with your $patch object:

$patch = new Patch();
$value = new PPModel('{
    "state":"DELETED"
}');
$patch->setOp('replace')
    ->setPath('/')
    ->setValue($value);

The state value assigned matches the documentation on the Plan object, but it's not clear at all in the documentation that this is how you delete a plan. I'll be following up with the docs team to see if we can improve this. I'm also going to follow up with my SDK colleagues to see if we can add a Plan::delete(...) method to each SDK language to reduce the complexity of this feature for developers. Also, we should add this use case to the samples project. (cc @jaypatel512 )

MikeHopley commented 9 years ago

Awesome, thanks!

Great to see the early focus on documentation too. It helps so much. :)

jaypatel512 commented 9 years ago

@MikeHopley I will go ahead, and get the helper method to delete the plan into the Plan.php class as soon as possible.

Till then, you could use the code snippet suggested by @jziaja

MikeHopley commented 9 years ago

Great, thank you @jaypatel512 !

jaypatel512 commented 9 years ago

I will be making the release soon that includes this change. It is already in master. I will update the thread with release number for future references.

jaypatel512 commented 9 years ago

This release fixes this issue

MikeHopley commented 9 years ago

Thanks Jay, quick work!

MikeHopley commented 9 years ago

Hmmm...

I tried deleting a plan, using the new $plan->delete($apiContext), but I got an error:

ErrorException (E_UNKNOWN) ErrorException thrown with message "Missing Accessor: PayPal\Common\PPModel:setState

However, if I change validation.level from strict to log , it seems that the plan will be deleted.

So what did I do wrong? Why is this basic operation throwing an error in strict mode?

jaypatel512 commented 9 years ago

Generally, the error you are seeing is because, when PayPal SDK tries to convert Json string to PPModel object, it not able to find the setState method. However, I am curious, on why would it fail there.

Changing the validation.level to log would just send the error message to log instead of throwing the exception.

I will look into this. However, if you could send me the code snippet, it would help.

MikeHopley commented 9 years ago

Hi Jay,

Here's all my Paypal code. Hope it's not too much, but I figure it's more useful to give you everything. I'm using Laravel, although I don't think that should matter.

Thanks for looking into everything so actively and not just saying, "well, you probably just made a mistake" (which is entirely possible!).

I can put this up on a development site for you to see, if that's useful. I don't think I can easily arrange access to the code, unfortunately.

<?php

use BBapp\providers\payment\paypal\PaypalPlanManager as PlanManager;
use BBapp\providers\payment\SubscriptionPlan;

class PaypalController extends BaseController {

    public function __construct()
    {
        $this->plans = new PlanManager;
    }

    public function getPlan($plan)
    {
        return $this->plans->retrieve($plan)->toJSON();
    }

    public function createPlan()
    {
        $plan = SubscriptionPlan::monthly();
        $response = $this->plans->create($plan);

        return  '<h1>Plan created: ' . $response->getId() . '</h1>';
    }

    public function listPlans()
    {
        $list = $this->plans->listAll();

        echo('<h1>We got ' . count($list) . ' plans</h1><ol>');
        if ( count($list) == 0 ) {return;}
        foreach ( $list as $plan ) { echo '<li>' . $plan->id; }echo('</ol>');
    }

    public function deletePlan($plan_id)
    {
        $this->plans->delete($plan_id);

        return  '<h1>Plan deleted: ' . $plan_id . '</h1>';
    }

    public function deleteAllPlans()
    {
        $this->plans->deleteAll();

        return Redirect::to('/paypal/plans');
    }

}
<?php namespace BBapp\providers\payment\paypal;

use Config;
use URL;

use BBapp\providers\payment\SubscriptionPlan;

use PayPal\Rest\ApiContext;
use PayPal\Auth\OAuthTokenCredential;

class PaypalApi {

    private   $root;
    protected $returnUrl;
    protected $cancelUrl;
    protected $token;

    public function __construct()
    {
        $this->root      = Config::get('paypal.api_root');
        $this->returnUrl = URL::secure('/subscribe/paypal-success');
        $this->cancelUrl = URL::secure('/subscribe/paypal-cancel');
        $this->token     = $this->getToken();
    }

    private function getToken()
    {
        $user = Config::get('paypal.client_id');
        $pass = Config::get('paypal.secret');

        $apiContext = new ApiContext( new OAuthTokenCredential($user, $pass) );

        $apiContext->setConfig([
            'mode'                   => Config::get('paypal.mode'),
            'http.ConnectionTimeOut' => 120,
            'log.LogEnabled'         => false,
            'validation.level'       => 'strict'
        ]);

        return $apiContext;
    }

}
<?php namespace BBapp\providers\payment\paypal;

use Exception;

use BBapp\providers\payment\PlanManagerInterface;
use BBapp\providers\payment\SubscriptionPlan;

use PayPal\Api\Plan;
use PayPal\Api\PaymentDefinition;
use PayPal\Api\MerchantPreferences;
use PayPal\Api\Currency;

class PaypalPlanManager extends PaypalApi implements PlanManagerInterface {

    public function __construct()
    {
        parent::__construct();
    }

    public function create( SubscriptionPlan $sub )
    {
        $plan = new Plan();

        $plan->setName( $sub->name )
            ->setDescription('Badminton Bible videos subscription')
            ->setType('fixed');

        $paymentDefinition = new PaymentDefinition();

        $paymentDefinition->setName('Regular Payments')
            ->setType('regular')
            ->setFrequency( $sub->interval )
            ->setFrequencyInterval( $sub->interval_count )
            ->setCycles('1200')
            ->setAmount(new Currency([
                'value'    => $sub->price->inPounds(),
                'currency' => 'GBP',
            ]));

        $merchantPreferences = new MerchantPreferences();
        $merchantPreferences->setReturnUrl( $this->returnUrl )
            ->setCancelUrl( $this->cancelUrl )
            ->setAutoBillAmount('yes')
            ->setInitialFailAmountAction('continue')
            ->setMaxFailAttempts('0');

        $plan->setPaymentDefinitions( [$paymentDefinition] );
        $plan->setMerchantPreferences( $merchantPreferences );

        return $plan->create( $this->token );
    }

    public function retrieve($plan_id)
    {
        return Plan::get( $plan_id, $this->token );
    }

    public function delete($plan_id)
    {
        $plan = $this->retrieve($plan_id);

        $plan->delete( $this->token );
    }

    public function deleteAll()
    {
        $plans = $this->listAll();

        if ( is_null($plans) )
        {
            throw new Exception('There are no Paypal plans to delete');
        }

        foreach ( $plans as $plan )
        {
            $this->delete($plan->id);
        }
    }

    public function listAll()
    {
        $page  = 0;
        $list  = $this->getPage($page);
        $plans = $list->plans;

        while ( $list->total_items > count($plans) )
        {
            $page   += 1;
            $newPage = $this->getPage($page)->plans;
            $plans   = array_merge($plans, $newPage);
        }
        return $plans;
    }

    private function getPage($page)
    {
        $params = [
            'page'           => (string) $page,
            'page_size'      => '20',
            'total_required' => 'yes',
        ];
        return Plan::all( $params, $this->token );
    }

}
jaypatel512 commented 9 years ago

I think i found the bug. The retrieve method you are making the call to is getting the plan object first and then deletes it. I think the retrieve method is trying to convert that json to model object, and thats when it gets this error. Is it possible for you to get me the log dump, with the json that you receive back when running the delete command. that would help me find this issue too.

Thank you so much for giving me all the information. This really helps. :)

MikeHopley commented 9 years ago

Thanks Jay. Here's my log file, with validation set to "log" so that the operation succeeds but the warning is logged.

[08-12-2014 10:39:19] PayPal\Core\PPHttpConnection: INFO: POST https://api.sandbox.paypal.com/v1/oauth2/token

[08-12-2014 10:39:19] PayPal\Core\PPHttpConnection: FINE: User-Agent: PayPalSDK/PayPal-PHP-SDK 0.15.1 (lang=PHP;v=5.5.11;bit=32;os=Windows_NT_6.2;machine=i586;openssl=1.0.1g;curl=7.36.0)

[08-12-2014 10:39:19] PayPal\Core\PPHttpConnection: FINE: Authorization: Basic **Redacted**

[08-12-2014 10:39:19] PayPal\Core\PPHttpConnection: FINE: Accept: */*

[08-12-2014 10:39:19] PayPal\Core\PPHttpConnection: FINE: Payload : grant_type=client_credentials

[08-12-2014 10:39:19] PayPal\Core\PPHttpConnection: INFO: Invalid or no certificate authority found - Retrying using bundled CA certs file

[08-12-2014 10:39:21] PayPal\Core\PPHttpConnection: FINE: Response : {"scope":"https://uri.paypal.com/services/applications/webhooks openid https://uri.paypal.com/services/subscriptions https://api.paypal.com/v1/payments/.* https://api.paypal.com/v1/vault/credit-card/.* https://api.paypal.com/v1/vault/credit-card","access_token":"A015e-GKzw10p4gWGVJ2RlSxkJHNX2kHYvwMUOwbYUuxxYI","token_type":"Bearer","app_id":"APP-**Redacted**","expires_in":28800}

[08-12-2014 10:39:21] PayPal\Core\PPHttpConnection: INFO: GET https://api.sandbox.paypal.com/v1/payments/billing-plans/P-51Y80969TK305944BFFZOGXI

[08-12-2014 10:39:21] PayPal\Core\PPHttpConnection: FINE: Content-Type: application/json

[08-12-2014 10:39:21] PayPal\Core\PPHttpConnection: FINE: User-Agent: PayPalSDK/PayPal-PHP-SDK 0.15.1 (lang=PHP;v=5.5.11;bit=32;os=Windows_NT_6.2;machine=i586;openssl=1.0.1g;curl=7.36.0)

[08-12-2014 10:39:21] PayPal\Core\PPHttpConnection: FINE: Authorization: Bearer A015e-GKzw10p4gWGVJ2RlSxkJHNX2kHYvwMUOwbYUuxxYI

[08-12-2014 10:39:21] PayPal\Core\PPHttpConnection: FINE: No Request Payload

[08-12-2014 10:39:21] PayPal\Core\PPHttpConnection: INFO: Invalid or no certificate authority found - Retrying using bundled CA certs file

[08-12-2014 10:39:22] PayPal\Core\PPHttpConnection: FINE: Response : {"id":"P-51Y80969TK305944BFFZOGXI","state":"CREATED","name":"videos_monthly_price_300","description":"Badminton Bible videos subscription","type":"FIXED","payment_definitions":[{"id":"PD-6WH18673HC1964723FFZOGXI","name":"Regular Payments","type":"REGULAR","frequency":"Month","amount":{"currency":"GBP","value":"3"},"cycles":"1200","charge_models":[],"frequency_interval":"1"}],"merchant_preferences":{"setup_fee":{"currency":"GBP","value":"0"},"max_fail_attempts":"0","return_url":"https://localhost/subscribe/paypal-success","cancel_url":"https://localhost/subscribe/paypal-cancel","auto_bill_amount":"YES","initial_fail_amount_action":"CONTINUE"},"create_time":"2014-12-08T10:30:02.845Z","update_time":"2014-12-08T10:30:02.845Z","links":[{"href":"https://api.sandbox.paypal.com/v1/payments/billing-plans/P-51Y80969TK305944BFFZOGXI","rel":"self","method":"GET"}]}

[08-12-2014 10:39:22] PayPal\Validation\ModelAccessorValidator: WARNING: Missing Accessor: PayPal\Common\PPModel:setState. Please let us know by creating an issue at https://github.com/paypal/PayPal-PHP-SDK/issues

[08-12-2014 10:39:22] PayPal\Validation\ModelAccessorValidator: WARNING: Missing Accessor: PayPal\Common\PPModel:setState. Please let us know by creating an issue at https://github.com/paypal/PayPal-PHP-SDK/issues

[08-12-2014 10:39:22] PayPal\Core\PPHttpConnection: INFO: PATCH https://api.sandbox.paypal.com/v1/payments/billing-plans/P-51Y80969TK305944BFFZOGXI

[08-12-2014 10:39:22] PayPal\Core\PPHttpConnection: FINE: Content-Type: application/json

[08-12-2014 10:39:22] PayPal\Core\PPHttpConnection: FINE: User-Agent: PayPalSDK/PayPal-PHP-SDK 0.15.1 (lang=PHP;v=5.5.11;bit=32;os=Windows_NT_6.2;machine=i586;openssl=1.0.1g;curl=7.36.0)

[08-12-2014 10:39:22] PayPal\Core\PPHttpConnection: FINE: Authorization: Bearer A015e-GKzw10p4gWGVJ2RlSxkJHNX2kHYvwMUOwbYUuxxYI

[08-12-2014 10:39:22] PayPal\Core\PPHttpConnection: FINE: Payload : [{"op":"replace","path":"/","value":{"state":"DELETED"}}]

[08-12-2014 10:39:23] PayPal\Core\PPHttpConnection: INFO: Invalid or no certificate authority found - Retrying using bundled CA certs file

[08-12-2014 10:39:24] PayPal\Core\PPHttpConnection: FINE: No Response Body
jaypatel512 commented 9 years ago

Hey @MikeHopley ! I have made the fix for the issue you were facing. The issue was, when you create a PPModel generic instance, the validation is trying to see if you have proper access modifiers. In this case, as it is generic method, it would be using magic methods to set those values.

Anyway, you could either make this change in your code yourself, and wont have to worry, as you would be getting it fixed in the next release. There are few more fixes coming along that I need to get in before making the release.

Here is the commit that you could directly apply on your code, or you could use master branch by defining the rest-api-sdk-php to "dev-master" in your composer.json.

Thank you so much for helping us on fixing these issues. We really appreciate your contribution :)

MikeHopley commented 9 years ago

Fantastic -- I'm really impressed with how fast you're ploughing through these issues. No doubt you have a hundred other things to juggle at the same time. :)