├── src └── Dinkbit │ └── ConektaCashier │ ├── work │ └── .gitkeep │ ├── PlanInterface.php │ ├── BillableRepositoryInterface.php │ ├── CashierServiceProvider.php │ ├── EloquentBillableRepository.php │ ├── stubs │ └── migration.stub │ ├── CashierTableCommand.php │ ├── WebhookController.php │ ├── Customer.php │ ├── LineItem.php │ ├── Contracts │ └── Billable.php │ ├── Billable.php │ └── ConektaGateway.php ├── .gitignore ├── .travis.yml ├── tests ├── LineItemTest.php ├── WebhookControllerTest.php ├── BillableTraitTest.php └── ConektaGatewayTest.php ├── phpunit.xml ├── LICENSE ├── composer.json └── readme.md /src/Dinkbit/ConektaCashier/work/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | Thumbs.db 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | - 7.1 9 | 10 | sudo: false 11 | 12 | install: travis_retry composer install --no-interaction --prefer-source 13 | 14 | script: vendor/bin/phpunit --verbose 15 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/PlanInterface.php: -------------------------------------------------------------------------------- 1 | 10000]); 16 | $billable->shouldReceive('formatCurrency')->andReturn(100.00); 17 | $this->assertEquals(100.00, $line->total()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/CashierServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('Dinkbit\ConektaCashier\BillableRepositoryInterface', function () { 27 | return new EloquentBillableRepository(); 28 | }); 29 | 30 | $this->app->singleton('command.conekta.cashier.table', function ($app) { 31 | return new CashierTableCommand(); 32 | }); 33 | 34 | $this->commands('command.conekta.cashier.table'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 dinkbit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/EloquentBillableRepository.php: -------------------------------------------------------------------------------- 1 | createCashierModel(Config::get('services.conekta.model')); 20 | 21 | return $model->where($model->getConektaIdName(), $conektaId)->first(); 22 | } 23 | 24 | /** 25 | * Create a new instance of the Auth model. 26 | * 27 | * @param string $model 28 | * 29 | * @return \Dinkbit\ConektaCashier\BillableInterface 30 | */ 31 | protected function createCashierModel($class) 32 | { 33 | $model = new $class(); 34 | 35 | if (!$model instanceof BillableContract) { 36 | throw new \InvalidArgumentException('Model does not implement Billable.'); 37 | } 38 | 39 | return $model; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/stubs/migration.stub: -------------------------------------------------------------------------------- 1 | tinyInteger('conekta_active')->default(0); 17 | $table->string('conekta_id')->nullable(); 18 | $table->string('conekta_subscription')->nullable(); 19 | $table->string('conekta_plan', 35)->nullable(); 20 | $table->string('card_type', 30)->nullable(); 21 | $table->string('last_four', 4)->nullable(); 22 | $table->timestamp('trial_ends_at')->nullable(); 23 | $table->timestamp('subscription_ends_at')->nullable(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::table('conekta_cashier_table', function(Blueprint $table) { 35 | $table->dropColumn( 36 | 'conekta_active', 'conekta_id', 'conekta_subscription', 'conekta_plan', 'card_type', 'last_four', 'trial_ends_at', 'subscription_ends_at' 37 | ); 38 | }); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /tests/WebhookControllerTest.php: -------------------------------------------------------------------------------- 1 | andReturn(json_encode(['type' => 'charge.succeeded', 'id' => 'event-id'])); 16 | $controller = new WebhookControllerTestStub(); 17 | $controller->handleWebhook(); 18 | 19 | $this->assertTrue($_SERVER['__received']); 20 | } 21 | 22 | public function testNormalResponseIsReturnedIfMethodIsMissing() 23 | { 24 | Request::shouldReceive('getContent')->andReturn(json_encode(['type' => 'foo.bar', 'id' => 'event-id'])); 25 | $controller = new WebhookControllerTestStub(); 26 | $response = $controller->handleWebhook(); 27 | $this->assertEquals(200, $response->getStatusCode()); 28 | } 29 | } 30 | 31 | class WebhookControllerTestStub extends Dinkbit\ConektaCashier\WebhookController 32 | { 33 | public function handleChargeSucceeded() 34 | { 35 | $_SERVER['__received'] = true; 36 | } 37 | 38 | /** 39 | * Verify with Conekta that the event is genuine. 40 | * 41 | * @param string $id 42 | * 43 | * @return bool 44 | */ 45 | protected function eventExistsOnConekta($id) 46 | { 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dinkbit/conekta-cashier", 3 | "description": "Dinkbit Cashier nos da una interface para cobrar subscripciones con Conketa en Laravel.", 4 | "keywords": ["dinkbit", "conekta", "laravel", "billing"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "dinkbit", 9 | "email": "developers@dinkbit.com", 10 | "homepage": "https://github.com/dinkbit/conekta-cashier" 11 | }, 12 | { 13 | "name": "Rafael Masri", 14 | "email": "rafael.masri@dinkbit.com", 15 | "role": "Developer" 16 | }, 17 | { 18 | "name": "Joseph Cohen", 19 | "email": "joseph.cohen@dinkbit.com", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": ">=5.4.0", 25 | "conekta/conekta-php": "~2.0.4", 26 | "illuminate/filesystem": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", 27 | "illuminate/support": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", 28 | "nesbot/carbon": "~1.0", 29 | "symfony/http-kernel": "~2.6|~3.0" 30 | }, 31 | "require-dev": { 32 | "illuminate/routing": "~5.0", 33 | "illuminate/view": "~5.0", 34 | "mockery/mockery": "0.9.*", 35 | "phpunit/phpunit": "4.0.*" 36 | }, 37 | "autoload": { 38 | "psr-0": { 39 | "Dinkbit\\ConektaCashier\\": "src/" 40 | } 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "2.0-dev" 45 | } 46 | }, 47 | "minimum-stability": "dev" 48 | } 49 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/CashierTableCommand.php: -------------------------------------------------------------------------------- 1 | createBaseMigration(); 32 | 33 | file_put_contents($fullPath, $this->getMigrationStub()); 34 | 35 | $this->info('Migration created successfully!'); 36 | 37 | $this->laravel['composer']->dumpAutoloads(); 38 | } 39 | 40 | /** 41 | * Create a base migration file for the reminders. 42 | * 43 | * @return string 44 | */ 45 | protected function createBaseMigration() 46 | { 47 | $name = 'add_conekta_cashier_columns'; 48 | 49 | $path = $this->laravel['path.database'].'/migrations'; 50 | 51 | return $this->laravel['migration.creator']->create($name, $path); 52 | } 53 | 54 | /** 55 | * Get the contents of the reminder migration stub. 56 | * 57 | * @return string 58 | */ 59 | protected function getMigrationStub() 60 | { 61 | $stub = file_get_contents(__DIR__.'/stubs/migration.stub'); 62 | 63 | return str_replace('conekta_cashier_table', $this->argument('table'), $stub); 64 | } 65 | 66 | /** 67 | * Get the console command arguments. 68 | * 69 | * @return array 70 | */ 71 | protected function getArguments() 72 | { 73 | return [ 74 | ['table', InputArgument::REQUIRED, 'The name of your billable table.'], 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/WebhookController.php: -------------------------------------------------------------------------------- 1 | getJsonPayload(); 24 | 25 | if (!$this->eventExistsOnConekta($payload['id'])) { 26 | return; 27 | } 28 | 29 | $method = 'handle'.studly_case(str_replace('.', '_', $payload['type'])); 30 | 31 | if (method_exists($this, $method)) { 32 | return $this->{$method}($payload); 33 | } else { 34 | return $this->missingMethod(); 35 | } 36 | } 37 | 38 | /** 39 | * Verify with Stripe that the event is genuine. 40 | * 41 | * @param string $id 42 | * 43 | * @return bool 44 | */ 45 | protected function eventExistsOnConekta($id) 46 | { 47 | try { 48 | Conekta::setApiKey(Config::get('services.conekta.secret')); 49 | 50 | return !is_null(Conekta_Event::where(['id' => $id])); 51 | } catch (Exception $e) { 52 | return false; 53 | } 54 | } 55 | 56 | /** 57 | * Handle a failed payment from a Conekta subscription. 58 | * 59 | * @param array $payload 60 | * 61 | * @return \Symfony\Component\HttpFoundation\Response 62 | */ 63 | protected function handleSubscriptionPaymentFailed(array $payload) 64 | { 65 | $billable = $this->getBillable($payload['data']['object']['customer_id']); 66 | 67 | if ($billable) { 68 | $billable->subscription()->cancel(); 69 | } 70 | 71 | return new Response('Webhook Handled', 200); 72 | } 73 | 74 | /** 75 | * Get the billable entity instance by Conekta ID. 76 | * 77 | * @param string $conektaId 78 | * 79 | * @return \Dinkbit\ConektaCashier\BillableInterface 80 | */ 81 | protected function getBillable($conektaId) 82 | { 83 | return App::make('Dinkbit\ConektaCashier\BillableRepositoryInterface')->find($conektaId); 84 | } 85 | 86 | /** 87 | * Get the JSON payload for the request. 88 | * 89 | * @return array 90 | */ 91 | protected function getJsonPayload() 92 | { 93 | return (array) json_decode(Request::getContent(), true); 94 | } 95 | 96 | /** 97 | * Handle calls to missing methods on the controller. 98 | * 99 | * @param array $parameters 100 | * 101 | * @return mixed 102 | */ 103 | public function missingMethod($parameters = []) 104 | { 105 | return new Response(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/Customer.php: -------------------------------------------------------------------------------- 1 | subscription ? $this->subscription->id : null; 32 | } 33 | 34 | /** 35 | * Find a subscription by ID. 36 | * 37 | * @param string $id 38 | * 39 | * @return \Conekta_Subscription|null 40 | */ 41 | public function findSubscription($id) 42 | { 43 | if ($this->subscription->id == $id) { 44 | return $this->subscription; 45 | } 46 | } 47 | 48 | /** 49 | * Create the current subscription with the given data. 50 | * 51 | * @param array $params 52 | * 53 | * @return void 54 | */ 55 | protected function _createSubscription(array $params) 56 | { 57 | return $this->subscription = $this->createSubscription($params); 58 | } 59 | 60 | /** 61 | * Update the current subscription with the given data. 62 | * 63 | * @param array $params 64 | * 65 | * @return \Conekta_Subscription 66 | */ 67 | public function updateSubscription($params = null) 68 | { 69 | if (is_null($this->subscription) || $this->subscription->status == 'canceled') { 70 | return $this->_createSubscription($params); 71 | } 72 | 73 | return $this->_updateSubscription($params); 74 | } 75 | 76 | /** 77 | * Update the current subscription with the given data. 78 | * 79 | * @param $params 80 | * 81 | * @return void 82 | */ 83 | public function _updateSubscription($params = null) 84 | { 85 | return $this->subscription = $this->subscription->update($params); 86 | } 87 | 88 | /** 89 | * Cancel the current subscription. 90 | * 91 | * @param array $params 92 | * 93 | * @return void 94 | */ 95 | public function cancelSubscription($params = null) 96 | { 97 | return $this->subscription->cancel($params); 98 | } 99 | 100 | /** 101 | * Pause the current subscription. 102 | * 103 | * @return void 104 | */ 105 | public function pauseSubscription() 106 | { 107 | return $this->subscription = $this->subscription->pause(); 108 | } 109 | 110 | /** 111 | * Resume the current subscription. 112 | * 113 | * @return void 114 | */ 115 | public function resumeSubscription() 116 | { 117 | return $this->subscription = $this->subscription->resume(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/LineItem.php: -------------------------------------------------------------------------------- 1 | billable = $billable; 34 | $this->conektaLine = $conektaLine; 35 | } 36 | 37 | /** 38 | * Get the total amount for the line item in dollars. 39 | * 40 | * @param string $symbol The Symbol you want to show 41 | * 42 | * @return string 43 | */ 44 | public function dollars() 45 | { 46 | return $this->totalWithCurrency(); 47 | } 48 | 49 | /** 50 | * Get the total amount for the line item with the currency symbol. 51 | * 52 | * @return string 53 | */ 54 | public function totalWithCurrency() 55 | { 56 | if (starts_with($total = $this->total(), '-')) { 57 | return '-'.$this->billable->addCurrencySymbol(ltrim($total, '-')); 58 | } else { 59 | return $this->billable->addCurrencySymbol($total); 60 | } 61 | } 62 | 63 | /** 64 | * Get the total for the line item. 65 | * 66 | * @return float 67 | */ 68 | public function total() 69 | { 70 | return $this->billable->formatCurrency($this->amount); 71 | } 72 | 73 | /** 74 | * Get a human readable date for the start date. 75 | * 76 | * @return string 77 | */ 78 | public function startDateString() 79 | { 80 | if ($this->isSubscription()) { 81 | return date('M j, Y', $this->period->start); 82 | } 83 | } 84 | 85 | /** 86 | * Get a human readable date for the end date. 87 | * 88 | * @return string 89 | */ 90 | public function endDateString() 91 | { 92 | if ($this->isSubscription()) { 93 | return date('M j, Y', $this->period->end); 94 | } 95 | } 96 | 97 | /** 98 | * Determine if the line item is for a subscription. 99 | * 100 | * @return bool 101 | */ 102 | public function isSubscription() 103 | { 104 | return $this->type == 'subscription'; 105 | } 106 | 107 | /** 108 | * Get the Conekta line item instance. 109 | * 110 | * @return object 111 | */ 112 | public function getStripeLine() 113 | { 114 | return $this->conektaLine; 115 | } 116 | 117 | /** 118 | * Dynamically access the Conekta line item instance. 119 | * 120 | * @param string $key 121 | * 122 | * @return mixed 123 | */ 124 | public function __get($key) 125 | { 126 | return $this->conektaLine->{$key}; 127 | } 128 | 129 | /** 130 | * Dynamically set values on the Conekta line item instance. 131 | * 132 | * @param string $key 133 | * @param mixed $value 134 | * 135 | * @return mixed 136 | */ 137 | public function __set($key, $value) 138 | { 139 | $this->conektaLine->{$key} = $value; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/BillableTraitTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getTrialEndDate')->andReturn(Carbon\Carbon::now()->addDays(5)); 17 | 18 | $this->assertTrue($billable->onTrial()); 19 | } 20 | 21 | public function testOnTrialMethodReturnsFalseIfTrialDateLessThanCurrentDate() 22 | { 23 | $billable = m::mock('BillableTraitTestStub[getTrialEndDate]'); 24 | $billable->shouldReceive('getTrialEndDate')->andReturn(Carbon\Carbon::now()->subDays(5)); 25 | 26 | $this->assertFalse($billable->onTrial()); 27 | } 28 | 29 | public function testSubscribedChecksStripeIsActiveIfCardRequiredUpFront() 30 | { 31 | $billable = new BillableTraitCardUpFrontTestStub(); 32 | $billable->conekta_active = true; 33 | $billable->subscription_ends_at = null; 34 | $this->assertTrue($billable->subscribed()); 35 | 36 | $billable = new BillableTraitCardUpFrontTestStub(); 37 | $billable->conekta_active = false; 38 | $billable->subscription_ends_at = null; 39 | $this->assertFalse($billable->subscribed()); 40 | 41 | $billable = new BillableTraitCardUpFrontTestStub(); 42 | $billable->conekta_active = false; 43 | $billable->subscription_ends_at = Carbon\Carbon::now()->addDays(5); 44 | $this->assertTrue($billable->subscribed()); 45 | 46 | $billable = new BillableTraitCardUpFrontTestStub(); 47 | $billable->conekta_active = false; 48 | $billable->subscription_ends_at = Carbon\Carbon::now()->subDays(5); 49 | $this->assertFalse($billable->subscribed()); 50 | } 51 | 52 | public function testSubscribedHandlesNoCardUpFront() 53 | { 54 | $billable = new BillableTraitTestStub(); 55 | $billable->trial_ends_at = null; 56 | $billable->conekta_active = null; 57 | $billable->subscription_ends_at = null; 58 | $this->assertFalse($billable->subscribed()); 59 | 60 | $billable = new BillableTraitTestStub(); 61 | $billable->conekta_active = 0; 62 | $billable->trial_ends_at = Carbon\Carbon::now()->addDays(5); 63 | $this->assertTrue($billable->subscribed()); 64 | 65 | $billable = new BillableTraitTestStub(); 66 | $billable->conekta_active = true; 67 | $billable->trial_ends_at = Carbon\Carbon::now()->subDays(5); 68 | $this->assertTrue($billable->subscribed()); 69 | 70 | $billable = new BillableTraitTestStub(); 71 | $billable->conekta_active = false; 72 | $billable->trial_ends_at = Carbon\Carbon::now()->subDays(5); 73 | $billable->subscription_ends_at = null; 74 | $this->assertFalse($billable->subscribed()); 75 | 76 | $billable = new BillableTraitTestStub(); 77 | $billable->trial_ends_at = null; 78 | $billable->conekta_active = null; 79 | $billable->subscription_ends_at = Carbon\Carbon::now()->addDays(5); 80 | $this->assertTrue($billable->subscribed()); 81 | 82 | $billable = new BillableTraitTestStub(); 83 | $billable->trial_ends_at = null; 84 | $billable->conekta_active = null; 85 | $billable->subscription_ends_at = Carbon\Carbon::now()->subDays(5); 86 | $this->assertFalse($billable->subscribed()); 87 | } 88 | 89 | public function testReadyForBillingChecksStripeReadiness() 90 | { 91 | $billable = new BillableTraitTestStub(); 92 | $billable->conekta_id = null; 93 | $this->assertFalse($billable->readyForBilling()); 94 | 95 | $billable = new BillableTraitTestStub(); 96 | $billable->conekta_id = 1; 97 | $this->assertTrue($billable->readyForBilling()); 98 | } 99 | 100 | public function testGettingStripeKey() 101 | { 102 | Config::shouldReceive('get')->once()->with('services.conekta.secret')->andReturn('foo'); 103 | $this->assertEquals('foo', BillableTraitTestStub::getConektaKey()); 104 | } 105 | } 106 | 107 | class BillableTraitTestStub implements Dinkbit\ConektaCashier\Contracts\Billable 108 | { 109 | use Dinkbit\ConektaCashier\Billable; 110 | public $cardUpFront = false; 111 | 112 | public function save() 113 | { 114 | } 115 | } 116 | 117 | class BillableTraitCardUpFrontTestStub implements Dinkbit\ConektaCashier\Contracts\Billable 118 | { 119 | use Dinkbit\ConektaCashier\Billable; 120 | public $cardUpFront = true; 121 | 122 | public function save() 123 | { 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/Contracts/Billable.php: -------------------------------------------------------------------------------- 1 | mockBillableInterface(); 17 | $billable->shouldReceive('getCurrency')->andReturn('mxn'); 18 | $gateway = m::mock('Dinkbit\ConektaCashier\ConektaGateway[getConektaCustomer,createConektaCustomer,updateLocalConektaData]', [$billable, 'plan']); 19 | $gateway->shouldReceive('createConektaCustomer')->andReturn($customer = m::mock('StdClass')); 20 | $customer->shouldReceive('updateSubscription')->once()->with([ 21 | 'plan' => 'plan', 22 | ])->andReturn((object) ['id' => 'sub_id']); 23 | $customer->id = 'foo'; 24 | $billable->shouldReceive('setConektaSubscription')->once()->with('sub_id'); 25 | $gateway->shouldReceive('getConektaCustomer')->once()->with('foo'); 26 | $gateway->shouldReceive('updateLocalConektaData')->once(); 27 | 28 | $gateway->create('token', []); 29 | } 30 | 31 | public function testCreatePassesProperOptionsToCustomerForTrialEnd() 32 | { 33 | $billable = $this->mockBillableInterface(); 34 | $billable->shouldReceive('getCurrency')->andReturn('mxn'); 35 | $gateway = m::mock('Dinkbit\ConektaCashier\ConektaGateway[getConektaCustomer,createConektaCustomer,updateLocalConektaData]', [$billable, 'plan']); 36 | $gateway->shouldReceive('createConektaCustomer')->andReturn($customer = m::mock('StdClass')); 37 | $customer->shouldReceive('updateSubscription')->once()->with([ 38 | 'plan' => 'plan', 39 | 'trial_end' => Carbon::now()->toIso8601String(), 40 | ])->andReturn((object) ['id' => 'sub_id']); 41 | $customer->id = 'foo'; 42 | $billable->shouldReceive('setConektaSubscription')->once()->with('sub_id'); 43 | $gateway->shouldReceive('getConektaCustomer')->once()->with('foo'); 44 | $gateway->shouldReceive('updateLocalConektaData')->once(); 45 | 46 | $gateway->skipTrial(); 47 | $gateway->create('token', []); 48 | } 49 | 50 | public function testCreateUtilizesGivenCustomerIfApplicable() 51 | { 52 | $billable = $this->mockBillableInterface(); 53 | $billable->shouldReceive('getCurrency')->andReturn('mxn'); 54 | $gateway = m::mock('Dinkbit\ConektaCashier\ConektaGateway[getConektaCustomer,createConektaCustomer,updateLocalConektaData,updateCard]', [$billable, 'plan']); 55 | $gateway->shouldReceive('createConektaCustomer')->never(); 56 | $customer = m::mock('StdClass'); 57 | $customer->shouldReceive('updateSubscription')->once()->andReturn($sub = (object) ['id' => 'sub_id']); 58 | $billable->shouldReceive('setConektaSubscription')->with('sub_id'); 59 | $customer->id = 'foo'; 60 | $gateway->shouldReceive('getConektaCustomer')->once()->with('foo'); 61 | $gateway->shouldReceive('updateCard')->once(); 62 | $gateway->shouldReceive('updateLocalConektaData')->once(); 63 | 64 | $gateway->create('token', [], $customer); 65 | } 66 | 67 | public function testSwapCallsCreateWithProperArguments() 68 | { 69 | $billable = $this->mockBillableInterface(); 70 | $gateway = m::mock('Dinkbit\ConektaCashier\ConektaGateway[create,getConektaCustomer,maintainTrial]', [$billable, 'plan']); 71 | $gateway->shouldReceive('getConektaCustomer')->once()->andReturn($customer = m::mock('StdClass')); 72 | $gateway->shouldReceive('maintainTrial')->once(); 73 | $gateway->shouldReceive('create')->once()->with(null, null, $customer); 74 | 75 | $gateway->swap(); 76 | } 77 | 78 | public function testCancellingOfSubscriptions() 79 | { 80 | $billable = $this->mockBillableInterface(); 81 | $gateway = m::mock('Dinkbit\ConektaCashier\ConektaGateway[getConektaCustomer]', [$billable, 'plan']); 82 | $gateway->shouldReceive('getConektaCustomer')->andReturn($customer = m::mock('StdClass')); 83 | $customer->subscription = (object) ['billing_cycle_end' => $time = time(), 'trial_end' => null]; 84 | $billable->shouldReceive('setSubscriptionEndDate')->once()->with(m::type('Carbon\Carbon'))->andReturnUsing(function ($value) use ($time) { 85 | $this->assertEquals($time, $value->getTimestamp()); 86 | 87 | return $value; 88 | }); 89 | $customer->shouldReceive('cancelSubscription')->once(); 90 | $billable->shouldReceive('setConektaIsActive')->once()->with(false)->andReturn($billable); 91 | $billable->shouldReceive('saveBillableInstance')->once(); 92 | 93 | $gateway->cancel(); 94 | } 95 | 96 | public function testCancellingOfSubscriptionsWithTrials() 97 | { 98 | $billable = $this->mockBillableInterface(); 99 | $gateway = m::mock('Dinkbit\ConektaCashier\ConektaGateway[getConektaCustomer]', [$billable, 'plan']); 100 | $gateway->shouldReceive('getConektaCustomer')->andReturn($customer = m::mock('StdClass')); 101 | $customer->subscription = (object) ['billing_cycle_end' => $trialTime = time() + 50, 'trial_end' => time()]; 102 | $billable->shouldReceive('setSubscriptionEndDate')->once()->with(m::type('Carbon\Carbon'))->andReturnUsing(function ($value) use ($trialTime) { 103 | $this->assertEquals($trialTime, $value->getTimestamp()); 104 | 105 | return $value; 106 | }); 107 | $customer->shouldReceive('cancelSubscription')->once(); 108 | $billable->shouldReceive('setConektaIsActive')->once()->with(false)->andReturn($billable); 109 | $billable->shouldReceive('saveBillableInstance')->once(); 110 | 111 | $gateway->cancel(); 112 | } 113 | 114 | public function testUpdatingCreditCardData() 115 | { 116 | $billable = $this->mockBillableInterface(); 117 | $gateway = m::mock('Dinkbit\ConektaCashier\ConektaGateway[getConektaCustomer,getLastFourCardDigits,getCardType]', [$billable, 'plan']); 118 | $gateway->shouldAllowMockingProtectedMethods(); 119 | $gateway->shouldReceive('getConektaCustomer')->andReturn($customer = m::mock('StdClass')); 120 | $gateway->shouldReceive('getLastFourCardDigits')->once()->andReturn('1111'); 121 | $gateway->shouldReceive('getCardType')->once()->andReturn('brand'); 122 | $customer->subscription = (object) ['plan' => (object) ['id' => 1]]; 123 | $customer->shouldReceive('createCard')->once()->with(['token' => 'token'])->andReturn($card = m::mock('StdClass')); 124 | $card->id = 'card_id'; 125 | $customer->shouldReceive('updateSubscription')->once()->with([ 126 | 'card' => $card->id, 127 | ])->andReturn((object) ['id' => 'sub_id']); 128 | $customer->shouldReceive('update')->once()->with(['default_card_id' => 'card_id']); 129 | 130 | $billable->shouldReceive('setLastFourCardDigits')->once()->with('1111')->andReturn($billable); 131 | $billable->shouldReceive('setCardType')->once()->with('brand')->andReturn($billable); 132 | $billable->shouldReceive('saveBillableInstance')->once(); 133 | 134 | $gateway->updateCard('token'); 135 | } 136 | 137 | public function testRetrievingACustomersConektaPlanId() 138 | { 139 | $billable = $this->mockBillableInterface(); 140 | $gateway = m::mock('Dinkbit\ConektaCashier\ConektaGateway[getConektaCustomer]', [$billable, 'plan']); 141 | $gateway->shouldReceive('getConektaCustomer')->andReturn($customer = m::mock('StdClass')); 142 | $customer->subscription = (object) ['plan_id' => 1]; 143 | 144 | $this->assertEquals(1, $gateway->planId()); 145 | } 146 | 147 | public function testUpdatingLocalConektaData() 148 | { 149 | $billable = $this->mockBillableInterface(); 150 | $gateway = new ConektaGateway($billable, 'plan'); 151 | $billable->shouldReceive('setConektaId')->once()->with('id')->andReturn($billable); 152 | $billable->shouldReceive('setConektaPlan')->once()->with('plan')->andReturn($billable); 153 | $billable->shouldReceive('setLastFourCardDigits')->once()->with('last-four')->andReturn($billable); 154 | $billable->shouldReceive('setCardType')->once()->with('brand-type')->andReturn($billable); 155 | $billable->shouldReceive('setConektaIsActive')->once()->with(true)->andReturn($billable); 156 | $billable->shouldReceive('setSubscriptionEndDate')->once()->with(null)->andReturn($billable); 157 | $billable->shouldReceive('saveBillableInstance')->once()->andReturn($billable); 158 | $customer = m::mock('StdClass'); 159 | $customer->cards[0] = (object) ['id' => 'id', 'last4' => 'last-four', 'brand' => 'brand-type']; 160 | $customer->id = 'id'; 161 | $customer->default_card_id = 'id'; 162 | $customer->shouldReceive('getSubscriptionId')->andReturn('sub_id'); 163 | 164 | $gateway->updateLocalConektaData($customer); 165 | } 166 | 167 | public function testGettingTheTrialEndDateForACustomer() 168 | { 169 | $time = time(); 170 | $customer = (object) ['subscription' => (object) ['trial_end' => $time, 'status' => 'in_trial']]; 171 | $gateway = new ConektaGateway($this->mockBillableInterface(), 'plan'); 172 | 173 | $this->assertInstanceOf('Carbon\Carbon', $gateway->getTrialEndForCustomer($customer)); 174 | $this->assertEquals($time, $gateway->getTrialEndForCustomer($customer)->getTimestamp()); 175 | } 176 | 177 | protected function mockBillableInterface() 178 | { 179 | $billable = m::mock('Dinkbit\ConektaCashier\Contracts\Billable'); 180 | $billable->shouldReceive('getConektaKey')->andReturn('key'); 181 | 182 | return $billable; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Conekta Cashier 2 | 3 | [![Build Status](https://img.shields.io/travis/dinkbit/conekta-cashier.svg?style=flat-square)](https://travis-ci.org/dinkbit/conekta-cashier) 4 | [![StyleCI](https://styleci.io/repos/22849643/shield)](https://styleci.io/repos/22849643) 5 | 6 | [![image](https://s3.amazonaws.com/dinkbit/img/firmas/firma_dinkbit.png)]() 7 | 8 | Port of Stripe [Laravel Cashier](https://github.com/laravel/cashier) to Conekta 9 | 10 | Please note the latest version of Laravel Cashier supports Laravel 5+, if you are looking for the Laravel 4 implementation see the [1.0](https://github.com/dinkbit/conekta-cashier/tree/1.0) branch. 11 | 12 | ___ 13 | 14 | # Laravel Cashier 15 | 16 | - [Introduction](#introduction) 17 | - [Configuration](#configuration) 18 | - [Subscribing To A Plan](#subscribing-to-a-plan) 19 | - [Single Charges](#single-charges) 20 | - [Swapping Subscriptions](#swapping-subscriptions) 21 | - [Cancelling A Subscription](#cancelling-a-subscription) 22 | - [Resuming A Subscription](#resuming-a-subscription) 23 | - [Checking Subscription Status](#checking-subscription-status) 24 | - [Handling Failed Payments](#handling-failed-payments) 25 | - [Handling Other Conekta Webhooks](#handling-other-conekta-webhooks) 26 | 27 | 28 | ## Introduction 29 | 30 | Laravel Cashier provides an expressive, fluent interface to [Conekta's](https://conekta.com) subscription billing services. It handles almost all of the boilerplate subscription billing code you are dreading writing. In addition to basic subscription management, Cashier can handle coupons, swapping subscription, subscription "quantities", cancellation grace periods, and even generate invoice PDFs. 31 | 32 | 33 | ## Configuration 34 | 35 | #### Composer 36 | 37 | First, add the Cashier package to your `composer.json` file: 38 | 39 | "dinkbit/conekta-cashier": "~2.0" (For Conekta 1.0.0 PHP-SDK 2.0) 40 | 41 | #### Service Provider 42 | 43 | Next, register the `Dinkbit\ConektaCashier\CashierServiceProvider` in your `app` configuration file. 44 | 45 | #### Migration 46 | 47 | Before using Cashier, we'll need to add several columns to your database. Don't worry, you can use the `conekta-cashier:table` Artisan command to create a migration to add the necessary column. For example, to add the column to the users table use `php artisan conekta-cashier:table users`. Once the migration has been created, simply run the `migrate` command. 48 | 49 | #### Model Setup 50 | 51 | Next, add the `Billable` trait and appropriate date mutators to your model definition: 52 | 53 | use Dinkbit\ConektaCashier\Billable; 54 | use Dinkbit\ConektaCashier\Contracts\Billable as BillableContract; 55 | 56 | class User extends Eloquent implements BillableContract { 57 | 58 | use Billable; 59 | 60 | protected $dates = ['trial_ends_at', 'subscription_ends_at']; 61 | 62 | } 63 | 64 | #### Conekta Key 65 | 66 | Finally, set your Conekta key in your `services.php` config file: 67 | 68 | 'conekta' => [ 69 | 'model' => 'User', 70 | 'secret' => env('CONEKTA_API_SECRET'), 71 | ], 72 | 73 | Alternatively you can store it in one of your bootstrap files or service providers, such as the `AppServiceProvider`: 74 | 75 | User::setConektaKey('conekta-key'); 76 | 77 | ## Subscribing To A Plan 78 | 79 | Once you have a model instance, you can easily subscribe that user to a given Conekta plan: 80 | 81 | $user = User::find(1); 82 | 83 | $user->subscription('monthly')->create($creditCardToken); 84 | 85 | You can also extend a subscription trial. 86 | 87 | $subscription = $user->subscription('monthly')->create($creditCardToken); 88 | 89 | $user->extendTrial(Carbon::now()->addMonth()); 90 | 91 | The `subscription` method will automatically create the Conekta subscription, as well as update your database with Conekta customer ID and other relevant billing information. If your plan has a trial configured in Conekta, the trial end date will also automatically be set on the user record. 92 | 93 | If your plan has a trial period that is **not** configured in Conekta, you must set the trial end date manually after subscribing: 94 | 95 | $user->trial_ends_at = Carbon::now()->addDays(14); 96 | 97 | $user->save(); 98 | 99 | ### Specifying Additional User Details 100 | 101 | If you would like to specify additional customer details, you may do so by passing them as second argument to the `create` method: 102 | 103 | $user->subscription('monthly')->create($creditCardToken, [ 104 | 'email' => $email, 'name' => 'Joe Doe' 105 | ]); 106 | 107 | To learn more about the additional fields supported by Conekta, check out Conekta's [documentation on customer creation](https://www.conekta.io/es/docs/api#crear-cliente). 108 | 109 | ## Single Charges 110 | 111 | If you would like to make a "one off" charge against a subscribed customer's credit card, you may use the `charge` method: 112 | 113 | $user->charge(100); 114 | 115 | The `charge` method accepts the amount you would like to charge in the **lowest denominator of the currency**. So, for example, the example above will charge 100 cents, or $1.00, against the user's credit card. 116 | 117 | The `charge` method accepts an array as its second argument, allowing you to pass any options you wish to the underlying Conekta charge creation: 118 | 119 | $user->charge(100, [ 120 | 'card' => $token, 121 | ]); 122 | 123 | The `charge` method will return `false` if the charge fails. This typically indicates the charge was denied: 124 | 125 | if ( ! $user->charge(100)) 126 | { 127 | // The charge was denied... 128 | } 129 | 130 | If the charge is successful, the full Conekta response will be returned from the method. 131 | 132 | ## Swapping Subscriptions 133 | 134 | To swap a user to a new subscription, use the `swap` method: 135 | 136 | $user->subscription('premium')->swap(); 137 | 138 | If the user is on trial, the trial will be maintained as normal. Also, if a "quantity" exists for the subscription, that quantity will also be maintained. 139 | 140 | ## Cancelling A Subscription 141 | 142 | Cancelling a subscription is a walk in the park: 143 | 144 | $user->subscription()->cancel(); 145 | 146 | When a subscription is cancelled, Cashier will automatically set the `subscription_ends_at` column on your database. This column is used to know when the `subscribed` method should begin returning `false`. For example, if a customer cancels a subscription on March 1st, but the subscription was not scheduled to end until March 5th, the `subscribed` method will continue to return `true` until March 5th. 147 | 148 | ## Resuming A Subscription 149 | 150 | If a user has cancelled their subscription and you wish to resume it, use the `resume` method: 151 | 152 | $user->subscription('monthly')->resume($creditCardToken); 153 | 154 | If the user cancels a subscription and then resumes that subscription before the subscription has fully expired, they will not be billed immediately. Their subscription will simply be re-activated, and they will be billed on the original billing cycle. 155 | 156 | ## Checking Subscription Status 157 | 158 | To verify that a user is subscribed to your application, use the `subscribed` command: 159 | 160 | if ($user->subscribed()) 161 | { 162 | // 163 | } 164 | 165 | The `subscribed` method makes a great candidate for a [route middleware](/docs/5.0/middleware): 166 | 167 | public function handle($request, Closure $next) 168 | { 169 | if ($request->user() && ! $request->user()->subscribed()) 170 | { 171 | return redirect('billing'); 172 | } 173 | 174 | return $next($request); 175 | } 176 | 177 | You may also determine if the user is still within their trial period (if applicable) using the `onTrial` method: 178 | 179 | if ($user->onTrial()) 180 | { 181 | // 182 | } 183 | 184 | To determine if the user was once an active subscriber, but has cancelled their subscription, you may use the `cancelled` method: 185 | 186 | if ($user->cancelled()) 187 | { 188 | // 189 | } 190 | 191 | You may also determine if a user has cancelled their subscription, but are still on their "grace period" until the subscription fully expires. For example, if a user cancels a subscription on March 5th that was scheduled to end on March 10th, the user is on their "grace period" until March 10th. Note that the `subscribed` method still returns `true` during this time. 192 | 193 | if ($user->onGracePeriod()) 194 | { 195 | // 196 | } 197 | 198 | The `everSubscribed` method may be used to determine if the user has ever subscribed to a plan in your application: 199 | 200 | if ($user->everSubscribed()) 201 | { 202 | // 203 | } 204 | 205 | The `onPlan` method may be used to determine if the user is subscribed to a given plan based on its ID: 206 | 207 | if ($user->onPlan('monthly')) 208 | { 209 | // 210 | } 211 | 212 | ## Handling Failed Payments 213 | 214 | What if a customer's credit card expires? No worries - Cashier includes a Webhook controller that can easily cancel the customer's subscription for you. Just point a route to the controller: 215 | 216 | Route::post('conekta/webhook', 'Dinkbit\ConektaCashier\WebhookController@handleWebhook'); 217 | 218 | That's it! Failed payments will be captured and handled by the controller. The controller will cancel the customer's subscription after three failed payment attempts. The `conekta/webhook` URI in this example is just for example. You will need to configure the URI in your Conekta settings. 219 | 220 | 221 | ## Handling Other Conekta Webhooks 222 | 223 | If you have additional Conekta webhook events you would like to handle, simply extend the Webhook controller. Your method names should correspond to Cashier's expected convention, specifically, methods should be prefixed with `handle` and the name of the Conekta webhook you wish to handle. For example, if you wish to handle the `invoice.payment_succeeded` webhook, you should add a `handleInvoicePaymentSucceeded` method to the controller. 224 | 225 | class WebhookController extends Dinkbit\ConektaCashier\WebhookController { 226 | 227 | public function handleInvoicePaymentSucceeded($payload) 228 | { 229 | // Handle The Event 230 | } 231 | 232 | } 233 | 234 | > **Note:** In addition to updating the subscription information in your database, the Webhook controller will also cancel the subscription via the Conekta API. 235 | 236 | ### Todo 237 | 238 | - [ ] Add Invoices support when Conekta has them. 239 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/Billable.php: -------------------------------------------------------------------------------- 1 | email; 25 | } 26 | 27 | /** 28 | * Write the entity to persistent storage. 29 | * 30 | * @return void 31 | */ 32 | public function saveBillableInstance() 33 | { 34 | $this->save(); 35 | } 36 | 37 | /** 38 | * Make a "one off" charge on the customer for the given amount. 39 | * 40 | * @param int $amount 41 | * @param array $options 42 | * 43 | * @return bool|mixed 44 | */ 45 | public function charge($amount, array $options = []) 46 | { 47 | return (new ConektaGateway($this))->charge($amount, $options); 48 | } 49 | 50 | /** 51 | * Get a new billing gateway instance for the given plan. 52 | * 53 | * @param \Dinkbit\ConektaCashier\PlanInterface|string|null $plan 54 | * 55 | * @return \Dinkbit\ConektaCashier\ConektaGateway 56 | */ 57 | public function subscription($plan = null) 58 | { 59 | if ($plan instanceof PlanInterface) { 60 | $plan = $plan->getConektaId(); 61 | } 62 | 63 | return new ConektaGateway($this, $plan); 64 | } 65 | 66 | /** 67 | * Update customer's credit card. 68 | * 69 | * @param string $token 70 | * 71 | * @return void 72 | */ 73 | public function updateCard($token) 74 | { 75 | return $this->subscription()->updateCard($token); 76 | } 77 | 78 | /** 79 | * Determine if the entity is within their trial period. 80 | * 81 | * @return bool 82 | */ 83 | public function onTrial() 84 | { 85 | if (!is_null($this->getTrialEndDate())) { 86 | return Carbon::today()->lt($this->getTrialEndDate()); 87 | } else { 88 | return false; 89 | } 90 | } 91 | 92 | /** 93 | * Determine if the entity is on grace period after cancellation. 94 | * 95 | * @return bool 96 | */ 97 | public function onGracePeriod() 98 | { 99 | if (!is_null($endsAt = $this->getSubscriptionEndDate())) { 100 | return Carbon::today()->lt(Carbon::instance($endsAt)); 101 | } else { 102 | return false; 103 | } 104 | } 105 | 106 | /** 107 | * Determine if the entity has an active subscription. 108 | * 109 | * @return bool 110 | */ 111 | public function subscribed() 112 | { 113 | if ($this->requiresCardUpFront()) { 114 | return $this->conektaIsActive() || $this->onGracePeriod(); 115 | } else { 116 | return $this->conektaIsActive() || $this->onTrial() || $this->onGracePeriod(); 117 | } 118 | } 119 | 120 | /** 121 | * Determine if the entity's trial has expired. 122 | * 123 | * @return bool 124 | */ 125 | public function expired() 126 | { 127 | return !$this->subscribed(); 128 | } 129 | 130 | /** 131 | * Determine if the entity has a Conekta ID but is no longer active. 132 | * 133 | * @return bool 134 | */ 135 | public function cancelled() 136 | { 137 | return $this->readyForBilling() && !$this->conektaIsActive(); 138 | } 139 | 140 | /** 141 | * Deteremine if the user has ever been subscribed. 142 | * 143 | * @return bool 144 | */ 145 | public function everSubscribed() 146 | { 147 | return $this->readyForBilling(); 148 | } 149 | 150 | /** 151 | * Determine if the entity is on the given plan. 152 | * 153 | * @param \Dinkbit\ConektaCashier\PlanInterface|string $plan 154 | * 155 | * @return bool 156 | */ 157 | public function onPlan($plan) 158 | { 159 | if ($plan instanceof PlanInterface) { 160 | $plan = $plan->getConektaId(); 161 | } 162 | 163 | return $this->conektaIsActive() && $this->subscription()->planId() == $plan; 164 | } 165 | 166 | /** 167 | * Determine if billing requires a credit card up front. 168 | * 169 | * @return bool 170 | */ 171 | public function requiresCardUpFront() 172 | { 173 | if (isset($this->cardUpFront)) { 174 | return $this->cardUpFront; 175 | } 176 | 177 | return true; 178 | } 179 | 180 | /** 181 | * Determine if the entity is a Conekta customer. 182 | * 183 | * @return bool 184 | */ 185 | public function readyForBilling() 186 | { 187 | return !is_null($this->getConektaId()); 188 | } 189 | 190 | /** 191 | * Determine if the entity has a current Conekta subscription. 192 | * 193 | * @return bool 194 | */ 195 | public function conektaIsActive() 196 | { 197 | return $this->conekta_active; 198 | } 199 | 200 | /** 201 | * Set whether the entity has a current Conekta subscription. 202 | * 203 | * @param bool $active 204 | * 205 | * @return \Dinkbit\ConektaCashier\Contracts\Billable 206 | */ 207 | public function setConektaIsActive($active = true) 208 | { 209 | $this->conekta_active = $active; 210 | 211 | return $this; 212 | } 213 | 214 | /** 215 | * Set Conekta as inactive on the entity. 216 | * 217 | * @return \Dinkbit\ConektaCashier\Contracts\Billable 218 | */ 219 | public function deactivateConekta() 220 | { 221 | $this->setConektaIsActive(false); 222 | 223 | $this->conekta_subscription = null; 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * Deteremine if the entity has a Conekta customer ID. 230 | * 231 | * @return bool 232 | */ 233 | public function hasConektaId() 234 | { 235 | return !is_null($this->conekta_id); 236 | } 237 | 238 | /** 239 | * Get the Conekta ID for the entity. 240 | * 241 | * @return string 242 | */ 243 | public function getConektaId() 244 | { 245 | return $this->conekta_id; 246 | } 247 | 248 | /** 249 | * Get the name of the Conekta ID database column. 250 | * 251 | * @return string 252 | */ 253 | public function getConektaIdName() 254 | { 255 | return 'conekta_id'; 256 | } 257 | 258 | /** 259 | * Set the Conekta ID for the entity. 260 | * 261 | * @param string $conekta_id 262 | * 263 | * @return \Dinkbit\ConektaCashier\Contracts\Billable 264 | */ 265 | public function setConektaId($conekta_id) 266 | { 267 | $this->conekta_id = $conekta_id; 268 | 269 | return $this; 270 | } 271 | 272 | /** 273 | * Get the current subscription ID. 274 | * 275 | * @return string 276 | */ 277 | public function getConektaSubscription() 278 | { 279 | return $this->conekta_subscription; 280 | } 281 | 282 | /** 283 | * Set the current subscription ID. 284 | * 285 | * @param string $subscription_id 286 | * 287 | * @return \Dinkbit\ConektaCashier\Contracts\Billable 288 | */ 289 | public function setConektaSubscription($subscription_id) 290 | { 291 | $this->conekta_subscription = $subscription_id; 292 | 293 | return $this; 294 | } 295 | 296 | /** 297 | * Get the Conekta plan ID. 298 | * 299 | * @return string 300 | */ 301 | public function getConektaPlan() 302 | { 303 | return $this->conekta_plan; 304 | } 305 | 306 | /** 307 | * Set the Conekta plan ID. 308 | * 309 | * @param string $plan 310 | * 311 | * @return \Dinkbit\ConektaCashier\Contracts\Billable 312 | */ 313 | public function setConektaPlan($plan) 314 | { 315 | $this->conekta_plan = $plan; 316 | 317 | return $this; 318 | } 319 | 320 | /** 321 | * Get the last four digits of the entity's credit card. 322 | * 323 | * @return string 324 | */ 325 | public function getLastFourCardDigits() 326 | { 327 | return $this->last_four; 328 | } 329 | 330 | /** 331 | * Set the last four digits of the entity's credit card. 332 | * 333 | * @return \Dinkbit\ConektaCashier\Contracts\Billable 334 | */ 335 | public function setLastFourCardDigits($digits) 336 | { 337 | $this->last_four = $digits; 338 | 339 | return $this; 340 | } 341 | 342 | /** 343 | * Get the brand of the entity's credit card. 344 | * 345 | * @return string 346 | */ 347 | public function getCardType() 348 | { 349 | return $this->card_type; 350 | } 351 | 352 | /** 353 | * Set the brand of the entity's credit card. 354 | * 355 | * @return \Dinkbit\ConektaCashier\Contracts\Billable 356 | */ 357 | public function setCardType($type) 358 | { 359 | $this->card_type = $type; 360 | 361 | return $this; 362 | } 363 | 364 | /** 365 | * Get the date on which the trial ends. 366 | * 367 | * @return \DateTime 368 | */ 369 | public function getTrialEndDate() 370 | { 371 | return $this->trial_ends_at; 372 | } 373 | 374 | /** 375 | * Set the date on which the trial ends. 376 | * 377 | * @param \DateTime|null $date 378 | * 379 | * @return \Dinkbit\ConektaCashier\Contracts\Billable 380 | */ 381 | public function setTrialEndDate($date) 382 | { 383 | $this->trial_ends_at = $date; 384 | 385 | return $this; 386 | } 387 | 388 | /** 389 | * Get the subscription end date for the entity. 390 | * 391 | * @return \DateTime 392 | */ 393 | public function getSubscriptionEndDate() 394 | { 395 | return $this->subscription_ends_at; 396 | } 397 | 398 | /** 399 | * Set the subscription end date for the entity. 400 | * 401 | * @param \DateTime|null $date 402 | * 403 | * @return \Dinkbit\ConektaCashier\Contracts\Billable 404 | */ 405 | public function setSubscriptionEndDate($date) 406 | { 407 | $this->subscription_ends_at = $date; 408 | 409 | return $this; 410 | } 411 | 412 | /** 413 | * Get the Stripe supported currency used by the entity. 414 | * 415 | * @return string 416 | */ 417 | public function getCurrency() 418 | { 419 | return 'mxn'; 420 | } 421 | 422 | /** 423 | * Get the locale for the currency used by the entity. 424 | * 425 | * @return string 426 | */ 427 | public function getCurrencyLocale() 428 | { 429 | return 'es_MX'; 430 | } 431 | 432 | /** 433 | * Format the given currency for display, without the currency symbol. 434 | * 435 | * @param int $amount 436 | * 437 | * @return mixed 438 | */ 439 | public function formatCurrency($amount) 440 | { 441 | return number_format($amount / 100, 2); 442 | } 443 | 444 | /** 445 | * Add the currency symbol to a given amount. 446 | * 447 | * @param string $amount 448 | * 449 | * @return string 450 | */ 451 | public function addCurrencySymbol($amount) 452 | { 453 | return '$'.$amount; 454 | } 455 | 456 | /** 457 | * Get the Conekta API key. 458 | * 459 | * @return string 460 | */ 461 | public static function getConektaKey() 462 | { 463 | return static::$conektaKey ?: Config::get('services.conekta.secret'); 464 | } 465 | 466 | /** 467 | * Set the Conekta API key. 468 | * 469 | * @param string $key 470 | * 471 | * @return void 472 | */ 473 | public static function setConektaKey($key) 474 | { 475 | static::$conektaKey = $key; 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /src/Dinkbit/ConektaCashier/ConektaGateway.php: -------------------------------------------------------------------------------- 1 | plan = $plan; 54 | $this->billable = $billable; 55 | 56 | Conekta::setApiKey($billable->getConektaKey()); 57 | } 58 | 59 | /** 60 | * Make a "one off" charge on the customer for the given amount. 61 | * 62 | * @param int $amount 63 | * @param array $options 64 | * 65 | * @return bool|mixed 66 | */ 67 | public function charge($amount, array $options = []) 68 | { 69 | $options = array_merge([ 70 | 'currency' => 'mxn', 71 | ], $options); 72 | 73 | $options['amount'] = $amount; 74 | 75 | if (!array_key_exists('card', $options) && $this->billable->hasConektaId()) { 76 | $options['card'] = $this->billable->getConektaId(); 77 | } 78 | 79 | if (!array_key_exists('card', $options)) { 80 | throw new InvalidArgumentException('No payment source provided.'); 81 | } 82 | 83 | try { 84 | $response = Conekta_Charge::create($options); 85 | } catch (Conekta_Error $e) { 86 | return false; 87 | } 88 | 89 | return $response; 90 | } 91 | 92 | /** 93 | * Subscribe to the plan for the first time. 94 | * 95 | * @param string $token 96 | * @param array $properties 97 | * @param object|null $customer 98 | * 99 | * @return void 100 | */ 101 | public function create($token, array $properties = [], $customer = null) 102 | { 103 | $freshCustomer = false; 104 | 105 | if (!$customer) { 106 | $customer = $this->createConektaCustomer($token, $properties); 107 | 108 | $freshCustomer = true; 109 | } elseif (!is_null($token)) { 110 | $this->updateCard($token); 111 | } 112 | 113 | $this->billable->setConektaSubscription( 114 | $customer->updateSubscription($this->buildPayload())->id 115 | ); 116 | 117 | $customer = $this->getConektaCustomer($customer->id); 118 | 119 | if ($freshCustomer && $trialEnd = $this->getTrialEndForCustomer($customer)) { 120 | $this->billable->setTrialEndDate($trialEnd); 121 | } 122 | 123 | $this->updateLocalConektaData($customer); 124 | } 125 | 126 | /** 127 | * Build the payload for a subscription create / update. 128 | * 129 | * @return array 130 | */ 131 | protected function buildPayload() 132 | { 133 | $payload = ['plan' => $this->plan]; 134 | 135 | if ($trialEnd = $this->getTrialEndForUpdate()) { 136 | $payload['trial_end'] = $trialEnd; 137 | } 138 | 139 | return $payload; 140 | } 141 | 142 | /** 143 | * Swap the billable entity to a new plan. 144 | * 145 | * @return void 146 | */ 147 | public function swap() 148 | { 149 | $customer = $this->getConektaCustomer(); 150 | 151 | // If no specific trial end date has been set, the default behavior should be 152 | // to maintain the current trial state, whether that is "active" or to run 153 | // the swap out with the exact number of days left on this current plan. 154 | if (is_null($this->trialEnd)) { 155 | $this->maintainTrial(); 156 | } 157 | 158 | return $this->create(null, [], $customer); 159 | } 160 | 161 | /** 162 | * Resubscribe a customer to a given plan. 163 | * 164 | * @param string $token 165 | * 166 | * @return void 167 | */ 168 | public function resume($token = null) 169 | { 170 | $this->skipTrial()->create($token, [], $this->getConektaCustomer()); 171 | 172 | $this->billable->setTrialEndDate(null)->saveBillableInstance(); 173 | } 174 | 175 | /** 176 | * Cancel the billable entity's subscription. 177 | * 178 | * @return void 179 | */ 180 | public function cancel($atPeriodEnd = true) 181 | { 182 | $customer = $this->getConektaCustomer(); 183 | 184 | if ($customer->subscription) { 185 | if ($atPeriodEnd) { 186 | $this->billable->setSubscriptionEndDate( 187 | Carbon::createFromTimestamp($this->getSubscriptionEndTimestamp($customer)) 188 | ); 189 | } 190 | 191 | $customer->cancelSubscription(['at_period_end' => $atPeriodEnd]); 192 | } 193 | 194 | if ($atPeriodEnd) { 195 | $this->billable->setConektaIsActive(false)->saveBillableInstance(); 196 | } else { 197 | $this->billable->setSubscriptionEndDate(Carbon::now()); 198 | 199 | $this->billable->deactivateConekta()->saveBillableInstance(); 200 | } 201 | } 202 | 203 | /** 204 | * Extend a subscription trial end datetime. 205 | * 206 | * @param \DateTime $trialEnd 207 | * 208 | * @return void 209 | */ 210 | public function extendTrial(\DateTime $trialEnd) 211 | { 212 | $customer = $this->getConektaCustomer(); 213 | 214 | if ($customer->subscription) { 215 | $customer->updateSubscription(['trial_end' => $trialEnd->format(DateTime::ISO8601)]); 216 | 217 | $this->billable->setTrialEndDate($trialEnd)->saveBillableInstance(); 218 | } 219 | } 220 | 221 | /** 222 | * Cancel the billable entity's subscription at the end of the period. 223 | * 224 | * @return void 225 | */ 226 | public function cancelAtEndOfPeriod() 227 | { 228 | return $this->cancel(true); 229 | } 230 | 231 | /** 232 | * Cancel the billable entity's subscription immediately. 233 | * 234 | * @return void 235 | */ 236 | public function cancelNow() 237 | { 238 | return $this->cancel(false); 239 | } 240 | 241 | /** 242 | * Get the subscription end timestamp for the customer. 243 | * 244 | * @param \Conekta_Customer $customer 245 | * 246 | * @return int 247 | */ 248 | protected function getSubscriptionEndTimestamp($customer) 249 | { 250 | if (!is_null($customer->subscription->trial_end) && $customer->subscription->trial_end > time()) { 251 | return $customer->subscription->trial_end; 252 | } else { 253 | return $customer->subscription->billing_cycle_end; 254 | } 255 | } 256 | 257 | /** 258 | * Get the current subscription period's end date. 259 | * 260 | * @return \Carbon\Carbon 261 | */ 262 | public function getSubscriptionEndDate() 263 | { 264 | $customer = $this->getConektaCustomer(); 265 | 266 | return Carbon::createFromTimestamp($this->getSubscriptionEndTimestamp($customer)); 267 | } 268 | 269 | /** 270 | * Update the credit card attached to the entity. 271 | * 272 | * @param string $token 273 | * 274 | * @return void 275 | */ 276 | public function updateCard($token) 277 | { 278 | $customer = $this->getConektaCustomer(); 279 | 280 | $card = $customer->createCard(['token' => $token]); 281 | 282 | $customer->update(['default_card_id' => $card->id]); 283 | 284 | if ($customer->subscription) { 285 | $customer->updateSubscription(['card' => $card->id]); 286 | 287 | $this->billable 288 | ->setLastFourCardDigits($this->getLastFourCardDigits($customer)) 289 | ->setCardType($this->getCardType($customer)) 290 | ->saveBillableInstance(); 291 | } 292 | 293 | return $card; 294 | } 295 | 296 | /** 297 | * Get the plan ID for the billable entity. 298 | * 299 | * @return string 300 | */ 301 | public function planId() 302 | { 303 | $customer = $this->getConektaCustomer(); 304 | 305 | if (isset($customer->subscription)) { 306 | return $customer->subscription->plan_id; 307 | } 308 | } 309 | 310 | /** 311 | * Update the local Conekta data in storage. 312 | * 313 | * @param \Conekta_Customer $customer 314 | * @param string|null $plan 315 | * 316 | * @return void 317 | */ 318 | public function updateLocalConektaData($customer, $plan = null) 319 | { 320 | $this->billable 321 | ->setConektaId($customer->id) 322 | ->setConektaPlan($plan ?: $this->plan) 323 | ->setLastFourCardDigits($this->getLastFourCardDigits($customer)) 324 | ->setCardType($this->getCardType($customer)) 325 | ->setConektaIsActive(true) 326 | ->setSubscriptionEndDate(null) 327 | ->saveBillableInstance(); 328 | } 329 | 330 | /** 331 | * Create a new Conekta customer instance. 332 | * 333 | * @param string $token 334 | * @param array $properties 335 | * 336 | * @return \Conekta_Customer 337 | */ 338 | public function createConektaCustomer($token, array $properties = []) 339 | { 340 | $customer = Conekta_Customer::create( 341 | array_merge(['cards' => [$token]], $properties), $this->getConektaKey() 342 | ); 343 | 344 | return $this->getConektaCustomer($customer->id); 345 | } 346 | 347 | /** 348 | * Get the Conekta customer for entity. 349 | * 350 | * @return \Conekta_Customer 351 | */ 352 | public function getConektaCustomer($id = null) 353 | { 354 | $customer = Customer::retrieve($id ?: $this->billable->getConektaId()); 355 | 356 | return $customer; 357 | } 358 | 359 | /** 360 | * Get the last four credit card digits for a customer. 361 | * 362 | * @param \Conekta_Customer $customer 363 | * 364 | * @return string 365 | */ 366 | protected function getLastFourCardDigits($customer) 367 | { 368 | if (empty($customer->cards[0])) { 369 | return; 370 | } 371 | 372 | if ($customer->default_card_id) { 373 | foreach ($customer->cards as $card) { 374 | if ($card->id == $customer->default_card_id) { 375 | return $card->last4; 376 | } 377 | } 378 | 379 | return; 380 | } 381 | 382 | return $customer->cards[0]->last4; 383 | } 384 | 385 | /** 386 | * Get the last four credit card digits for a customer. 387 | * 388 | * @param \Conekta_Customer $customer 389 | * 390 | * @return string 391 | */ 392 | protected function getCardType($customer) 393 | { 394 | if (empty($customer->cards[0])) { 395 | return; 396 | } 397 | 398 | if ($customer->default_card_id) { 399 | foreach ($customer->cards as $card) { 400 | if ($card->id == $customer->default_card_id) { 401 | return $card->brand; 402 | } 403 | } 404 | 405 | return; 406 | } 407 | 408 | return $customer->cards[0]->brand; 409 | } 410 | 411 | /** 412 | * Indicate that no trial should be enforced on the operation. 413 | * 414 | * @return \Dinkbit\ConektaCashier\ConektaGateway 415 | */ 416 | public function skipTrial() 417 | { 418 | $this->skipTrial = true; 419 | 420 | return $this; 421 | } 422 | 423 | /** 424 | * Specify the ending date of the trial. 425 | * 426 | * @param \DateTime $trialEnd 427 | * 428 | * @return \Dinkbit\ConektaCashier\ConektaGateway 429 | */ 430 | public function trialFor(\DateTime $trialEnd) 431 | { 432 | $this->trialEnd = $trialEnd; 433 | 434 | return $this; 435 | } 436 | 437 | /** 438 | * Get the current trial end date for subscription change. 439 | * 440 | * @return \DateTime 441 | */ 442 | public function getTrialFor() 443 | { 444 | return $this->trialEnd; 445 | } 446 | 447 | /** 448 | * Maintain the days left of the current trial (if applicable). 449 | * 450 | * @return \Dinkbit\ConektaCashier\ConektaGateway 451 | */ 452 | public function maintainTrial() 453 | { 454 | if ($this->billable->readyForBilling()) { 455 | if (!is_null($trialEnd = $this->getTrialEndForCustomer($this->getConektaCustomer()))) { 456 | $this->calculateRemainingTrialDays($trialEnd); 457 | } else { 458 | $this->skipTrial(); 459 | } 460 | } 461 | 462 | return $this; 463 | } 464 | 465 | /** 466 | * Get the trial end timestamp for a Conekta subscription update. 467 | * 468 | * @return int 469 | */ 470 | protected function getTrialEndForUpdate() 471 | { 472 | if ($this->skipTrial) { 473 | return Carbon::now()->toIso8601String(); 474 | } 475 | 476 | return $this->trialEnd ? $this->trialEnd->toIso8601String() : null; 477 | } 478 | 479 | /** 480 | * Get the trial end date for the customer's subscription. 481 | * 482 | * @param object $customer 483 | * 484 | * @return \Carbon\Carbon|null 485 | */ 486 | public function getTrialEndForCustomer($customer) 487 | { 488 | if (isset($customer->subscription) && $customer->subscription->status == 'in_trial' && isset($customer->subscription->trial_end)) { 489 | return Carbon::createFromTimestamp($customer->subscription->trial_end); 490 | } 491 | } 492 | 493 | /** 494 | * Calculate the remaining trial days based on the current trial end. 495 | * 496 | * @param \Carbon\Carbon $trialEnd 497 | * 498 | * @return void 499 | */ 500 | protected function calculateRemainingTrialDays($trialEnd) 501 | { 502 | // If there is still trial left on the current plan, we'll maintain that amount of 503 | // time on the new plan. If there is no time left on the trial we will force it 504 | // to skip any trials on this new plan, as this is the most expected actions. 505 | $diff = Carbon::now()->diffInHours($trialEnd); 506 | 507 | return $diff > 0 ? $this->trialFor(Carbon::now()->addHours($diff)) : $this->skipTrial(); 508 | } 509 | 510 | /** 511 | * Get the Conekta API key for the instance. 512 | * 513 | * @return string 514 | */ 515 | protected function getConektaKey() 516 | { 517 | return $this->billable->getConektaKey(); 518 | } 519 | 520 | /** 521 | * Get the currency for the billable entity. 522 | * 523 | * @return string 524 | */ 525 | protected function getCurrency() 526 | { 527 | return $this->billable->getCurrency(); 528 | } 529 | } 530 | --------------------------------------------------------------------------------