From d947aef0928da2bd25f6b7f6440c1d04fc689aad Mon Sep 17 00:00:00 2001 From: Tyler King Date: Sun, 3 Jun 2018 20:07:24 -0230 Subject: [PATCH 01/18] Just the beginning... setting up initial migrations, adjusting models --- src/ShopifyApp/Models/Shop.php | 10 +++ ..._184733_add_soft_delete_to_shops_table.php | 32 ++++++++ ...2018_06_03_185902_create_charges_table.php | 82 +++++++++++++++++++ tests/Models/ShopModelTest.php | 19 +++++ 4 files changed, 143 insertions(+) create mode 100644 src/ShopifyApp/resources/database/migrations/2018_06_03_184733_add_soft_delete_to_shops_table.php create mode 100644 src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php diff --git a/src/ShopifyApp/Models/Shop.php b/src/ShopifyApp/Models/Shop.php index 0669a917..75d5d143 100644 --- a/src/ShopifyApp/Models/Shop.php +++ b/src/ShopifyApp/Models/Shop.php @@ -3,10 +3,13 @@ namespace OhMyBrew\ShopifyApp\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; use OhMyBrew\ShopifyApp\Facades\ShopifyApp; class Shop extends Model { + use SoftDeletes; + /** * The attributes that are mass assignable. * @@ -18,6 +21,13 @@ class Shop extends Model 'grandfathered', ]; + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['deleted_at']; + /** * The API instance. * diff --git a/src/ShopifyApp/resources/database/migrations/2018_06_03_184733_add_soft_delete_to_shops_table.php b/src/ShopifyApp/resources/database/migrations/2018_06_03_184733_add_soft_delete_to_shops_table.php new file mode 100644 index 00000000..820a6d4d --- /dev/null +++ b/src/ShopifyApp/resources/database/migrations/2018_06_03_184733_add_soft_delete_to_shops_table.php @@ -0,0 +1,32 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('shops', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +} diff --git a/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php b/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php new file mode 100644 index 00000000..457f1c5a --- /dev/null +++ b/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php @@ -0,0 +1,82 @@ +increments('id'); + + // Filled in when the charge is created, provided by shopify, unique makes it indexed + $table->integer('charge_id')->unique(); + + // Test mode or real + $table->boolean('test'); + + $table->string('status')->nullable(); + + // Name of the charge (for recurring or one time charges) + $table->string('name')->nullable(); + + // Terms for the usage charges + $table->string('terms')->nullable(); + + // Integer value reprecenting a recurring, one time, usage, or application_credit. + // This also allows us to store usage based charges not just subscription or one time charges. + // We will be able to do things like create a charge history for a shop if they have multiple charges. + // For instance, usage based or an app that has multiple purchases. + $table->integer('type'); + + // Store the amount of the charge, this helps if you are experimenting with pricing + $table->decimal('price', 8, 2); + + // Store the amount of the charge, this helps if you are experimenting with pricing + $table->decimal('capped_amount', 8, 2); + + // Nullable in case of 0 trial days + $table->integer('trial_days')->nullable(); + + // The recurring application charge must be accepted or the returned value is null + $table->timestamp('billing_on')->nullable(); + + // When activation happened + $table->timestamp('activated_on')->nullable(); + + // Date the trial period ends + $table->timestamp('trial_ends_on')->nullable(); + + // Not supported on Shopify's initial billing screen, but good for future use + $table->timestamp('cancelled_on')->nullable(); + + // Provides created_at && updated_at columns + $table->timestamps(); + + // Allows for soft deleting + $table->softDeletes(); + + // Linking + $table->integer('shop_id')->unsigned(); + $table->foreign('shop_id')->references('id')->on('shops')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('charges'); + } +} diff --git a/tests/Models/ShopModelTest.php b/tests/Models/ShopModelTest.php index 4e01a630..26d99849 100644 --- a/tests/Models/ShopModelTest.php +++ b/tests/Models/ShopModelTest.php @@ -61,4 +61,23 @@ public function testShopShouldConfirmPaidState() $this->assertEquals(false, $shop->isPaid()); $this->assertEquals(true, $shop_2->isPaid()); } + + public function testShopCanSoftDeleteAndBeRestored() + { + $shop = new Shop(); + $shop->shopify_domain = 'hello.myshopify.com'; + $shop->save(); + $shop->delete(); + + // Test soft delete + $this->assertTrue($shop->trashed()); + $this->assertSoftDeleted('shops', [ + 'id' => $shop->id, + 'shopify_domain' => $shop->shopify_domain, + ]); + + // Test restore + $shop->restore(); + $this->assertFalse($shop->trashed()); + } } From 6af30497d54abe5fa4ff6efd787f3219a7f915ed Mon Sep 17 00:00:00 2001 From: Tyler King Date: Sun, 3 Jun 2018 20:10:17 -0230 Subject: [PATCH 02/18] Remove of charge ID from Shop --- ...0233_remove_charge_id_from_shops_table.php | 32 +++++++++++++++++++ tests/Models/ShopModelTest.php | 9 ------ tests/TestCase.php | 1 - 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 src/ShopifyApp/resources/database/migrations/2018_06_03_190233_remove_charge_id_from_shops_table.php diff --git a/src/ShopifyApp/resources/database/migrations/2018_06_03_190233_remove_charge_id_from_shops_table.php b/src/ShopifyApp/resources/database/migrations/2018_06_03_190233_remove_charge_id_from_shops_table.php new file mode 100644 index 00000000..ebc0f531 --- /dev/null +++ b/src/ShopifyApp/resources/database/migrations/2018_06_03_190233_remove_charge_id_from_shops_table.php @@ -0,0 +1,32 @@ +dropColumn(['charge_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('shops', function (Blueprint $table) { + $table->bigInteger('charge_id')->nullable(true)->default(null); + }); + } +} diff --git a/tests/Models/ShopModelTest.php b/tests/Models/ShopModelTest.php index 26d99849..09251336 100644 --- a/tests/Models/ShopModelTest.php +++ b/tests/Models/ShopModelTest.php @@ -53,15 +53,6 @@ public function testShopShouldReturnGrandfatheredState() $this->assertEquals(false, $shop_2->isGrandfathered()); } - public function testShopShouldConfirmPaidState() - { - $shop = Shop::where('shopify_domain', 'grandfathered.myshopify.com')->first(); - $shop_2 = Shop::where('shopify_domain', 'example.myshopify.com')->first(); - - $this->assertEquals(false, $shop->isPaid()); - $this->assertEquals(true, $shop_2->isPaid()); - } - public function testShopCanSoftDeleteAndBeRestored() { $shop = new Shop(); diff --git a/tests/TestCase.php b/tests/TestCase.php index e4df0106..3d127789 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -64,7 +64,6 @@ protected function seedDatabase() $shop = new Shop(); $shop->shopify_domain = 'example.myshopify.com'; $shop->shopify_token = '1234'; - $shop->charge_id = 678298290; $shop->save(); // Non-paid shop, grandfathered From 838a06b4404254f2a35aa7125bc2730407dbe655 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Sun, 3 Jun 2018 23:29:38 -0230 Subject: [PATCH 03/18] Some basics setup --- src/ShopifyApp/Models/Charge.php | 51 +++++++++++++++++++ src/ShopifyApp/Models/Shop.php | 22 +++++--- ...2018_06_03_185902_create_charges_table.php | 2 +- tests/Models/ShopModelTest.php | 9 ++++ tests/TestCase.php | 23 +++++++++ 5 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 src/ShopifyApp/Models/Charge.php diff --git a/src/ShopifyApp/Models/Charge.php b/src/ShopifyApp/Models/Charge.php new file mode 100644 index 00000000..bfad75de --- /dev/null +++ b/src/ShopifyApp/Models/Charge.php @@ -0,0 +1,51 @@ +orderBy('created_at', 'desc')->first(); + } + + /** + * Scope for latest charge by type for a shop. + * + * @param \Illuminate\Database\Eloquent\Builder $query The query builder + * @param integer $type The type of charge + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeLatestByType($query, int $type) + { + return $query->where('type', $type)->orderBy('created_at', 'desc')->first(); + } + + /** + * Gets the shop for the charge. + * + * @return OhMyBrew\ShopifyApp\Models\Shop + */ + public function shop() + { + return $this->belongsTo('OhMyBrew\ShopifyApp\Models\Shop'); + } +} diff --git a/src/ShopifyApp/Models/Shop.php b/src/ShopifyApp/Models/Shop.php index 75d5d143..dba8f1e6 100644 --- a/src/ShopifyApp/Models/Shop.php +++ b/src/ShopifyApp/Models/Shop.php @@ -55,22 +55,32 @@ public function api() } /** - * Checks if a shop has a charge ID. + * Checks is shop is grandfathered in. * * @return bool */ - public function isPaid() + public function isGrandfathered() { - return !is_null($this->charge_id); + return ((bool) $this->grandfathered) === true; } /** - * Checks is shop is grandfathered in. + * Get charges. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function charges() + { + return $this->hasMany('OhMyBrew\ShopifyApp\Models\Charge'); + } + + /** + * Checks if charges have been applied to the shop. * * @return bool */ - public function isGrandfathered() + public function hasCharges() { - return ((bool) $this->grandfathered) === true; + return $this->charges->isNotEmpty(); } } diff --git a/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php b/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php index 457f1c5a..26d7fe3f 100644 --- a/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php +++ b/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php @@ -41,7 +41,7 @@ public function up() $table->decimal('price', 8, 2); // Store the amount of the charge, this helps if you are experimenting with pricing - $table->decimal('capped_amount', 8, 2); + $table->decimal('capped_amount', 8, 2)->nullable(); // Nullable in case of 0 trial days $table->integer('trial_days')->nullable(); diff --git a/tests/Models/ShopModelTest.php b/tests/Models/ShopModelTest.php index 09251336..26d9b15c 100644 --- a/tests/Models/ShopModelTest.php +++ b/tests/Models/ShopModelTest.php @@ -71,4 +71,13 @@ public function testShopCanSoftDeleteAndBeRestored() $shop->restore(); $this->assertFalse($shop->trashed()); } + + public function testShouldReturnBoolForChargesApplied() + { + $shop = Shop::where('shopify_domain', 'grandfathered.myshopify.com')->first(); + $shop_2 = Shop::where('shopify_domain', 'example.myshopify.com')->first(); + + $this->assertEquals(false, $shop->hasCharges()); + $this->assertEquals(true, $shop_2->hasCharges()); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 3d127789..3a26fc11 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,9 +3,11 @@ namespace OhMyBrew\ShopifyApp\Test; use OhMyBrew\ShopifyApp\Models\Shop; +use OhMyBrew\ShopifyApp\Models\Charge; use OhMyBrew\ShopifyApp\ShopifyAppProvider; use Orchestra\Database\ConsoleServiceProvider; use Orchestra\Testbench\TestCase as OrchestraTestCase; +use Carbon\Carbon; abstract class TestCase extends OrchestraTestCase { @@ -60,6 +62,13 @@ protected function setupDatabase($app) protected function seedDatabase() { + $this->createShops(); + $this->createCharges(); + } + + protected function createShops() + { + // Paid shop, not grandfathered $shop = new Shop(); $shop->shopify_domain = 'example.myshopify.com'; @@ -84,4 +93,18 @@ protected function seedDatabase() $shop->shopify_domain = 'no-token-shop.myshopify.com'; $shop->save(); } + + public function createCharges() + {; + $charge = new Charge(); + $charge->charge_id = 98298298; + $charge->test = true; + $charge->name = 'Test Plan'; + $charge->type = 1; + $charge->price = 15.00; + $charge->trial_days = 7; + $charge->trial_ends_on = Carbon::createFromDate(2018, 6, 3, 'UTC')->addWeeks(1); + $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; + $charge->save(); + } } From 0e55c6036f6fe6c2aca2e9ec2f69063b2f554916 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Mon, 11 Jun 2018 00:19:25 -0230 Subject: [PATCH 04/18] Added model helpers for charges, fixed auth to check fo trashed stores, and to restore trashed stores, adjusted billable middleware and shop middleware --- src/ShopifyApp/Middleware/AuthShop.php | 3 +- src/ShopifyApp/Middleware/Billable.php | 13 ++- src/ShopifyApp/Models/Charge.php | 98 +++++++++++++++++++ src/ShopifyApp/ShopifyApp.php | 2 +- src/ShopifyApp/Traits/AuthControllerTrait.php | 8 +- tests/Controllers/AuthControllerTest.php | 22 ++++- tests/Middleware/AuthShopMiddlewareTest.php | 15 +++ tests/Models/ChargeModelTest.php | 86 ++++++++++++++++ tests/TestCase.php | 43 +++++++- 9 files changed, 283 insertions(+), 7 deletions(-) create mode 100644 tests/Models/ChargeModelTest.php diff --git a/src/ShopifyApp/Middleware/AuthShop.php b/src/ShopifyApp/Middleware/AuthShop.php index 5e112061..eec760cc 100644 --- a/src/ShopifyApp/Middleware/AuthShop.php +++ b/src/ShopifyApp/Middleware/AuthShop.php @@ -26,7 +26,8 @@ public function handle(Request $request, Closure $next) if ( $shop === null || ($shopParam && $shopParam !== $shop->shopify_domain) === true || - $shop->shopify_token === null + $shop->shopify_token === null || + $shop->trashed() ) { // Either no shop session or shops do not match session()->forget('shopify_domain'); diff --git a/src/ShopifyApp/Middleware/Billable.php b/src/ShopifyApp/Middleware/Billable.php index 271e48a1..43310c56 100644 --- a/src/ShopifyApp/Middleware/Billable.php +++ b/src/ShopifyApp/Middleware/Billable.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Http\Request; use OhMyBrew\ShopifyApp\Facades\ShopifyApp; +use OhMyBrew\ShopifyApp\Models\Charge; class Billable { @@ -19,9 +20,17 @@ class Billable public function handle(Request $request, Closure $next) { if (config('shopify-app.billing_enabled') === true) { + // Grab the shop and last recurring or one-time charge $shop = ShopifyApp::shop(); - if (!$shop->isPaid() && !$shop->isGrandfathered()) { - // No charge in database and they're not grandfathered in, redirect to billing + $lastCharge = $shop->charges() + ->where(function ($query) { + $query->latestByType(Charge::CHARGE_RECURRING); + })->orWhere(function ($query) { + $query->latestByType(Charge::CHARGE_ONETIME); + })->latest()->first(); + + if (!$shop->isGrandfathered() && (is_null($lastCharge) || $lastCharge->wasDeclined())) { + // They're not grandfathered in, and there is no charge or charge was declined... redirect to billing return redirect()->route('billing'); } } diff --git a/src/ShopifyApp/Models/Charge.php b/src/ShopifyApp/Models/Charge.php index bfad75de..0120d60c 100644 --- a/src/ShopifyApp/Models/Charge.php +++ b/src/ShopifyApp/Models/Charge.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use OhMyBrew\ShopifyApp\Facades\ShopifyApp; +use Carbon\Carbon; class Charge extends Model { @@ -16,6 +17,13 @@ class Charge extends Model const CHARGE_USAGE = 3; const CHARGE_CREDIT = 4; + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['deleted_at']; + /** * Scope for latest charge for a shop. * @@ -48,4 +56,94 @@ public function shop() { return $this->belongsTo('OhMyBrew\ShopifyApp\Models\Shop'); } + + /** + * Checks if the charge is a test. + * + * @return bool + */ + public function isTest() + { + return (bool) $this->test; + } + + /** + * Checks if the charge is a type. + * + * @param integer $type The charge type. + * + * @return bool + */ + public function isType(int $type) + { + return (int) $this->type === $type; + } + + /** + * Checks if the charge is a trial-type charge. + * + * @return bool + */ + public function isTrial() + { + return !is_null($this->trial_ends_on); + } + + /** + * Checks if the charge is currently in trial. + * + * @return bool + */ + public function isActiveTrial() + { + return $this->isTrial() && Carbon::now()->lte(Carbon::parse($this->trial_ends_on)); + } + + /** + * Returns the remaining trial days. + * + * @return integer + */ + public function remainingTrialDays() + { + if (!$this->isTrial()) { + return null; + } + + return $this->isActiveTrial() ? Carbon::now()->diffInDays($this->trial_ends_on) : 0; + } + + /** + * Returns the used trial days. + * + * @return integer|null + */ + public function usedTrialDays() + { + if (!$this->isTrial()) { + return null; + } + + return $this->trial_days - $this->remainingTrialDays(); + } + + /** + * Checks if the charge was accepted (for one-time and reccuring). + * + * @return bool + */ + public function wasAccepted() + { + return $this->status === 'accepted'; + } + + /** + * Checks if the charge was declined (for one-time and reccuring). + * + * @return bool + */ + public function wasDeclined() + { + return $this->status === 'declined'; + } } diff --git a/src/ShopifyApp/ShopifyApp.php b/src/ShopifyApp/ShopifyApp.php index cc4d1798..fd121748 100644 --- a/src/ShopifyApp/ShopifyApp.php +++ b/src/ShopifyApp/ShopifyApp.php @@ -44,7 +44,7 @@ public function shop() if (!$this->shop && $shopifyDomain) { // Grab shop from database here $shopModel = config('shopify-app.shop_model'); - $shop = $shopModel::firstOrCreate(['shopify_domain' => $shopifyDomain]); + $shop = $shopModel::withTrashed()->firstOrCreate(['shopify_domain' => $shopifyDomain]); // Update shop instance $this->shop = $shop; diff --git a/src/ShopifyApp/Traits/AuthControllerTrait.php b/src/ShopifyApp/Traits/AuthControllerTrait.php index 6c874093..0b72cfb9 100644 --- a/src/ShopifyApp/Traits/AuthControllerTrait.php +++ b/src/ShopifyApp/Traits/AuthControllerTrait.php @@ -87,8 +87,14 @@ protected function authenticationWithCode() return redirect()->route('login')->with('error', 'Invalid signature'); } - // Save token to shop + // Grab the shop; restore if need-be $shop = ShopifyApp::shop(); + if ($shop->trashed()) { + $shop->restore(); + $shop->charges()->restore(); + } + + // Save the token to the shop $shop->shopify_token = $api->requestAccessToken(request('code')); $shop->save(); diff --git a/tests/Controllers/AuthControllerTest.php b/tests/Controllers/AuthControllerTest.php index a2addb7a..95daafd7 100644 --- a/tests/Controllers/AuthControllerTest.php +++ b/tests/Controllers/AuthControllerTest.php @@ -19,7 +19,7 @@ public function setUp() { parent::setUp(); - // HMAC + // HMAC for regular tests $this->hmac = 'a7448f7c42c9bc025b077ac8b73e7600b6f8012719d21cbeb88db66e5dbbd163'; $this->hmacParams = [ 'hmac' => $this->hmac, @@ -28,6 +28,15 @@ public function setUp() 'timestamp' => '1337178173', ]; + // HMAC for trashed shop testing + $this->hmacTrashed = '77ec82b8dca7ea606e8b69d3cc50beced069b03631fd6a2f367993eb793f4c45'; + $this->hmacTrashedParams = [ + 'hmac' => $this->hmacTrashed, + 'shop' => 'trashed-shop.myshopify.com', + 'code' => '1234678910', + 'timestamp' => '1337178173', + ]; + // Stub in our API class config(['shopify-app.api_class' => new ApiStub()]); } @@ -65,6 +74,17 @@ public function testAuthAcceptsShopWithCodeAndUpdatesTokenForShop() $this->assertEquals('12345678', $shop->shopify_token); // Previous token was 1234 } + public function testAuthRestoresTrashedShop() + { + $shop = Shop::withTrashed()->where('shopify_domain', 'trashed-shop.myshopify.com')->first(); + $this->assertTrue($shop->trashed()); + + $this->call('get', '/authenticate', $this->hmacTrashedParams); + + $shop = $shop->fresh(); + $this->assertFalse($shop->trashed()); + } + public function testAuthAcceptsShopWithCodeAndRedirectsToHome() { $response = $this->call('get', '/authenticate', $this->hmacParams); diff --git a/tests/Middleware/AuthShopMiddlewareTest.php b/tests/Middleware/AuthShopMiddlewareTest.php index 77b1e8b1..c0e415e9 100644 --- a/tests/Middleware/AuthShopMiddlewareTest.php +++ b/tests/Middleware/AuthShopMiddlewareTest.php @@ -49,6 +49,21 @@ public function testShopWithNoTokenShouldNotPassMiddleware() $this->assertEquals(true, strpos($result, 'Redirecting to http://localhost/authenticate') !== false); } + public function testShopTrashedShouldNotPassMiddleware() + { + // Set a shop + session(['shopify_domain' => 'trashed-shop.myshopify.com']); + + $called = false; + $result = (new AuthShop())->handle(request(), function ($request) use (&$called) { + // Shouldn never be called + $called = true; + }); + + $this->assertFalse($called); + $this->assertEquals(true, strpos($result, 'Redirecting to http://localhost/authenticate') !== false); + } + public function testShopsWhichDoNotMatchShouldKillSessionAndDirectToReAuthenticate() { // Set a shop diff --git a/tests/Models/ChargeModelTest.php b/tests/Models/ChargeModelTest.php new file mode 100644 index 00000000..2d0f3f8e --- /dev/null +++ b/tests/Models/ChargeModelTest.php @@ -0,0 +1,86 @@ +assertInstanceOf( + Shop::class, + Charge::find(1)->shop + ); + } + + public function testChargeImplementsType() + { + $this->assertEquals( + Charge::CHARGE_RECURRING, + Charge::find(1)->type + ); + } + + public function testScopeLatest() + { + $this->assertEquals( + get_class(Charge::latest()), + Charge::class + ); + } + + public function testScopeLatestByType() + { + $this->assertEquals( + get_class(Charge::latestByType(Charge::CHARGE_RECURRING)), + Charge::class + ); + } + + public function testIsTest() + { + $this->assertEquals(true, Charge::find(1)->isTest()); + } + + public function testIsType() + { + $this->assertTrue(Charge::find(1)->isType(Charge::CHARGE_RECURRING)); + } + + public function testIsTrial() + { + $this->assertTrue(Charge::find(1)->isTrial()); + $this->assertFalse(Charge::find(4)->isTrial()); + } + + public function testIsActiveTrial() + { + $this->assertTrue(Charge::find(2)->isActiveTrial()); + $this->assertFalse(Charge::find(4)->isActiveTrial()); + } + + public function testRemainingTrialDays() + { + $this->assertEquals(0, Charge::find(1)->remainingTrialDays()); + $this->assertEquals(2, Charge::find(2)->remainingTrialDays()); + $this->assertEquals(0, Charge::find(3)->remainingTrialDays()); + $this->assertNull(Charge::find(4)->remainingTrialDays()); + } + + public function testUsedTrialDays() + { + $this->assertEquals(7, Charge::find(1)->usedTrialDays()); + $this->assertEquals(5, Charge::find(2)->usedTrialDays()); + $this->assertEquals(7, Charge::find(3)->usedTrialDays()); + $this->assertNull(Charge::find(4)->usedTrialDays()); + } + + public function testAcceptedAndDeclined() + { + $this->assertTrue(Charge::find(1)->wasAccepted()); + $this->assertTrue(!Charge::find(1)->wasDeclined()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 3a26fc11..cc7009fa 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -68,7 +68,6 @@ protected function seedDatabase() protected function createShops() { - // Paid shop, not grandfathered $shop = new Shop(); $shop->shopify_domain = 'example.myshopify.com'; @@ -92,6 +91,12 @@ protected function createShops() $shop = new Shop(); $shop->shopify_domain = 'no-token-shop.myshopify.com'; $shop->save(); + + // Trashed shop + $shop = new Shop(); + $shop->shopify_domain = 'trashed-shop.myshopify.com'; + $shop->save(); + $shop->delete(); } public function createCharges() @@ -100,11 +105,47 @@ public function createCharges() $charge->charge_id = 98298298; $charge->test = true; $charge->name = 'Test Plan'; + $charge->status = 'accepted'; $charge->type = 1; $charge->price = 15.00; $charge->trial_days = 7; $charge->trial_ends_on = Carbon::createFromDate(2018, 6, 3, 'UTC')->addWeeks(1); $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; $charge->save(); + + $charge = new Charge(); + $charge->charge_id = 67298298; + $charge->test = false; + $charge->name = 'Base Plan'; + $charge->status = 'accepted'; + $charge->type = 1; + $charge->price = 25.00; + $charge->trial_days = 7; + $charge->trial_ends_on = Carbon::now()->addDays(2); + $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; + $charge->save(); + + $charge = new Charge(); + $charge->charge_id = 78378873; + $charge->test = false; + $charge->name = 'Base Plan Old'; + $charge->status = 'accepted'; + $charge->type = 1; + $charge->price = 25.00; + $charge->trial_days = 7; + $charge->trial_ends_on = Carbon::now()->subWeeks(4); + $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; + $charge->save(); + + $charge = new Charge(); + $charge->charge_id = 89389389; + $charge->test = false; + $charge->name = 'Base Plan Old Non-Trial'; + $charge->status = 'accepted'; + $charge->type = 1; + $charge->price = 25.00; + $charge->trial_days = 0; + $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; + $charge->save(); } } From 9f756bdc3db976a208f321fbbb891ca9ad9f0cf6 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Tue, 12 Jun 2018 00:29:13 -0230 Subject: [PATCH 05/18] Completed building most of requirements for billing controller.. todo: restore on trial --- src/ShopifyApp/Middleware/Billable.php | 2 +- src/ShopifyApp/Models/Charge.php | 18 +++++-- .../Traits/BillingControllerTrait.php | 47 ++++++++++++++----- tests/Controllers/BillingControllerTest.php | 10 ++-- tests/Middleware/BillableMiddlewareTest.php | 16 +++++++ tests/Models/ChargeModelTest.php | 10 +++- tests/TestCase.php | 29 +++++++++--- ...d13096715a537f9bb6e2ea2ac0d493a24344c.json | 18 +++---- 8 files changed, 111 insertions(+), 39 deletions(-) diff --git a/src/ShopifyApp/Middleware/Billable.php b/src/ShopifyApp/Middleware/Billable.php index 43310c56..0ce90c8d 100644 --- a/src/ShopifyApp/Middleware/Billable.php +++ b/src/ShopifyApp/Middleware/Billable.php @@ -29,7 +29,7 @@ public function handle(Request $request, Closure $next) $query->latestByType(Charge::CHARGE_ONETIME); })->latest()->first(); - if (!$shop->isGrandfathered() && (is_null($lastCharge) || $lastCharge->wasDeclined())) { + if (!$shop->isGrandfathered() && (is_null($lastCharge) || $lastCharge->isDeclined())) { // They're not grandfathered in, and there is no charge or charge was declined... redirect to billing return redirect()->route('billing'); } diff --git a/src/ShopifyApp/Models/Charge.php b/src/ShopifyApp/Models/Charge.php index 0120d60c..d508d0ed 100644 --- a/src/ShopifyApp/Models/Charge.php +++ b/src/ShopifyApp/Models/Charge.php @@ -96,7 +96,7 @@ public function isTrial() */ public function isActiveTrial() { - return $this->isTrial() && Carbon::now()->lte(Carbon::parse($this->trial_ends_on)); + return $this->isTrial() && Carbon::today()->lte(Carbon::parse($this->trial_ends_on)); } /** @@ -110,7 +110,7 @@ public function remainingTrialDays() return null; } - return $this->isActiveTrial() ? Carbon::now()->diffInDays($this->trial_ends_on) : 0; + return $this->isActiveTrial() ? Carbon::today()->diffInDays($this->trial_ends_on) : 0; } /** @@ -127,12 +127,22 @@ public function usedTrialDays() return $this->trial_days - $this->remainingTrialDays(); } + /** + * Checks if the charge is active. + * + * @return bool + */ + public function isActive() + { + return $this->status === 'active'; + } + /** * Checks if the charge was accepted (for one-time and reccuring). * * @return bool */ - public function wasAccepted() + public function isAccepted() { return $this->status === 'accepted'; } @@ -142,7 +152,7 @@ public function wasAccepted() * * @return bool */ - public function wasDeclined() + public function isDeclined() { return $this->status === 'declined'; } diff --git a/src/ShopifyApp/Traits/BillingControllerTrait.php b/src/ShopifyApp/Traits/BillingControllerTrait.php index 82102185..d0b2e7c3 100644 --- a/src/ShopifyApp/Traits/BillingControllerTrait.php +++ b/src/ShopifyApp/Traits/BillingControllerTrait.php @@ -4,6 +4,8 @@ use OhMyBrew\ShopifyApp\Facades\ShopifyApp; use OhMyBrew\ShopifyApp\Libraries\BillingPlan; +use OhMyBrew\ShopifyApp\Models\Charge; +use Carbon\Carbon; trait BillingControllerTrait { @@ -38,23 +40,46 @@ public function process() // Setup the plan and get the charge $plan = new BillingPlan($shop, $this->chargeType()); $plan->setChargeId($chargeId); + $status = $plan->getCharge()->status; - // Check the customer's answer to the billing - $charge = $plan->getCharge(); - if ($charge->status == 'accepted') { - // Customer accepted, activate the charge - $plan->activate(); + // Grab the plan detailed used + $planDetails = $this->planDetails(); + unset($planDetails['return_url']); - // Save the charge ID to the shop - $shop->charge_id = $chargeId; - $shop->save(); + // Create a charge (regardless of the status) + $charge = new Charge(); + $charge->type = $this->chargeType() === 'recurring' ? Charge::CHARGE_RECURRING : Charge::CHARGE_ONETIME; + $charge->charge_id = $chargeId; + $charge->status = $status; - // Go to homepage of app - return redirect()->route('home'); + // Check the customer's answer to the billing + if ($status === 'accepted') { + // Activate and add details to our charge + $response = $plan->activate(); + $charge->status = $response->status; + $charge->billing_on = $response->billing_on; + $charge->trial_ends_on = $response->trial_ends_on; + $charge->activated_on = $response->activated_on; } else { - // Customer declined the charge, abort + // Customer declined the charge + $charge->cancelled_on = Carbon::today()->format('Y-m-d'); + } + + // Merge in the plan details since the fields match the database columns + foreach ($planDetails as $key => $value) { + $charge->{$key} = $value; + } + + // Save and link to the shop + $shop->charges()->save($charge); + + if ($status === 'declined') { + // Show the error... don't allow access return abort(403, 'It seems you have declined the billing charge for this application'); } + + // All good... go to homepage of app + return redirect()->route('home'); } /** diff --git a/tests/Controllers/BillingControllerTest.php b/tests/Controllers/BillingControllerTest.php index 20bcf47c..081db981 100644 --- a/tests/Controllers/BillingControllerTest.php +++ b/tests/Controllers/BillingControllerTest.php @@ -1,5 +1,4 @@ first(); - $this->assertEquals(678298290, $shop->charge_id); // Based on seedDatabase() - $response = $this->call('get', '/billing/process', ['charge_id' => 1029266947]); - $shop = $shop->fresh(); // Reload model $response->assertStatus(302); - $this->assertEquals(1029266947, $shop->charge_id); + $this->assertEquals(1029266947, $shop->charges()->get()->last()->charge_id); } public function testShopDeclinesBilling() { + $shop = Shop::where('shopify_domain', 'example.myshopify.com')->first(); $response = $this->call('get', '/billing/process', ['charge_id' => 10292]); + $lastCharge = $shop->charges()->get()->last(); $response->assertStatus(403); + $this->assertEquals(10292, $lastCharge->charge_id); + $this->assertEquals('declined', $lastCharge->status); $this->assertEquals( 'It seems you have declined the billing charge for this application', $response->exception->getMessage() diff --git a/tests/Middleware/BillableMiddlewareTest.php b/tests/Middleware/BillableMiddlewareTest.php index 11adfd37..d723eb0d 100644 --- a/tests/Middleware/BillableMiddlewareTest.php +++ b/tests/Middleware/BillableMiddlewareTest.php @@ -23,6 +23,22 @@ public function testEnabledBillingWithUnpaidShop() $this->assertEquals(true, strpos($result, 'Redirecting to http://localhost/billing') !== false); } + public function testEnabledBillingWithShopWhoDeclinedCharges() + { + // Enable billing and set a shop + config(['shopify-app.billing_enabled' => true]); + session(['shopify_domain' => 'trashed-shop.myshopify.com']); + + $called = false; + $result = (new Billable())->handle(request(), function ($request) use (&$called) { + // Should never be called + $called = true; + }); + + $this->assertFalse($called); + $this->assertEquals(true, strpos($result, 'Redirecting to http://localhost/billing') !== false); + } + public function testEnabledBillingWithPaidShop() { // Enable billing and set a shop diff --git a/tests/Models/ChargeModelTest.php b/tests/Models/ChargeModelTest.php index 2d0f3f8e..09ba6e1e 100644 --- a/tests/Models/ChargeModelTest.php +++ b/tests/Models/ChargeModelTest.php @@ -80,7 +80,13 @@ public function testUsedTrialDays() public function testAcceptedAndDeclined() { - $this->assertTrue(Charge::find(1)->wasAccepted()); - $this->assertTrue(!Charge::find(1)->wasDeclined()); + $this->assertTrue(Charge::find(1)->isAccepted()); + $this->assertFalse(Charge::find(1)->isDeclined()); + } + + public function testActive() + { + $this->assertFalse(Charge::find(1)->isActive()); + $this->assertTrue(Charge::find(4)->isActive()); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index cc7009fa..90a07f18 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -100,7 +100,8 @@ protected function createShops() } public function createCharges() - {; + { + // Test = true, status = accepted, trial = 7, active trial = no $charge = new Charge(); $charge->charge_id = 98298298; $charge->test = true; @@ -109,43 +110,57 @@ public function createCharges() $charge->type = 1; $charge->price = 15.00; $charge->trial_days = 7; - $charge->trial_ends_on = Carbon::createFromDate(2018, 6, 3, 'UTC')->addWeeks(1); + $charge->trial_ends_on = Carbon::createFromDate(2018, 6, 3, 'UTC')->addWeeks(1)->format('Y-m-d'); $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; $charge->save(); + // Test = false, status = active, trial = 7, active trial = yes $charge = new Charge(); $charge->charge_id = 67298298; $charge->test = false; $charge->name = 'Base Plan'; - $charge->status = 'accepted'; + $charge->status = 'active'; $charge->type = 1; $charge->price = 25.00; $charge->trial_days = 7; - $charge->trial_ends_on = Carbon::now()->addDays(2); + $charge->trial_ends_on = Carbon::today()->addDays(2)->format('Y-m-d'); $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; $charge->save(); + // Test = false, status = active, trial = 7, active trial = no $charge = new Charge(); $charge->charge_id = 78378873; $charge->test = false; $charge->name = 'Base Plan Old'; - $charge->status = 'accepted'; + $charge->status = 'active'; $charge->type = 1; $charge->price = 25.00; $charge->trial_days = 7; - $charge->trial_ends_on = Carbon::now()->subWeeks(4); + $charge->trial_ends_on = Carbon::today()->subWeeks(4)->format('Y-m-d'); $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; $charge->save(); + // Test = false, status = active, trial = 0 $charge = new Charge(); $charge->charge_id = 89389389; $charge->test = false; $charge->name = 'Base Plan Old Non-Trial'; - $charge->status = 'accepted'; + $charge->status = 'active'; $charge->type = 1; $charge->price = 25.00; $charge->trial_days = 0; $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; $charge->save(); + + // Test = false, status = declined, trial = 7, active trial = true + $charge = new Charge(); + $charge->charge_id = 78378378378; + $charge->test = false; + $charge->name = 'Base Plan Declined'; + $charge->status = 'declined'; + $charge->type = 1; + $charge->price = 25.00; + $charge->shop_id = Shop::where('shopify_domain', 'no-token-shop.myshopify.com')->first()->id; + $charge->save(); } } diff --git a/tests/fixtures/602d13096715a537f9bb6e2ea2ac0d493a24344c.json b/tests/fixtures/602d13096715a537f9bb6e2ea2ac0d493a24344c.json index bbe4d18d..e692b417 100644 --- a/tests/fixtures/602d13096715a537f9bb6e2ea2ac0d493a24344c.json +++ b/tests/fixtures/602d13096715a537f9bb6e2ea2ac0d493a24344c.json @@ -1,19 +1,19 @@ { "recurring_application_charge": { - "id": 1029266947, + "id": 455696195, "name": "Super Mega Plan", "api_client_id": 755357713, "price": "15.00", - "status": "accepted", - "return_url": "http:\/\/yourapp.com", - "billing_on": "2017-08-17", - "created_at": "2017-08-17T15:16:08-04:00", - "updated_at": "2017-08-17T15:29:50-04:00", + "status": "active", + "return_url": "http://yourapp.com", + "billing_on": "2018-06-06", + "created_at": "2018-05-07T15:33:38-04:00", + "updated_at": "2018-05-07T15:47:13-04:00", "test": null, - "activated_on": null, - "trial_ends_on": null, + "activated_on": "2018-05-07", + "trial_ends_on": "2018-05-07", "cancelled_on": null, "trial_days": 0, - "decorated_return_url": "http:\/\/yourapp.com?charge_id=1029266947" + "decorated_return_url": "http://yourapp.com?charge_id=455696195" } } \ No newline at end of file From ae669397cb3c7301eaa6e2856f112f162e4b297f Mon Sep 17 00:00:00 2001 From: Tyler King Date: Tue, 12 Jun 2018 03:01:07 +0000 Subject: [PATCH 06/18] Apply fixes from StyleCI --- src/ShopifyApp/Models/Charge.php | 17 +++++++++-------- .../Traits/BillingControllerTrait.php | 2 +- .../2018_06_03_185902_create_charges_table.php | 2 +- tests/Controllers/BillingControllerTest.php | 1 + tests/Models/ShopModelTest.php | 2 +- tests/TestCase.php | 4 ++-- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/ShopifyApp/Models/Charge.php b/src/ShopifyApp/Models/Charge.php index d508d0ed..f5a8f60c 100644 --- a/src/ShopifyApp/Models/Charge.php +++ b/src/ShopifyApp/Models/Charge.php @@ -2,10 +2,9 @@ namespace OhMyBrew\ShopifyApp\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use OhMyBrew\ShopifyApp\Facades\ShopifyApp; -use Carbon\Carbon; class Charge extends Model { @@ -28,6 +27,7 @@ class Charge extends Model * Scope for latest charge for a shop. * * @param \Illuminate\Database\Eloquent\Builder $query The query builder + * * @return \Illuminate\Database\Eloquent\Builder */ public function scopeLatest($query) @@ -39,7 +39,8 @@ public function scopeLatest($query) * Scope for latest charge by type for a shop. * * @param \Illuminate\Database\Eloquent\Builder $query The query builder - * @param integer $type The type of charge + * @param int $type The type of charge + * * @return \Illuminate\Database\Eloquent\Builder */ public function scopeLatestByType($query, int $type) @@ -70,7 +71,7 @@ public function isTest() /** * Checks if the charge is a type. * - * @param integer $type The charge type. + * @param int $type The charge type. * * @return bool */ @@ -102,12 +103,12 @@ public function isActiveTrial() /** * Returns the remaining trial days. * - * @return integer + * @return int */ public function remainingTrialDays() { if (!$this->isTrial()) { - return null; + return; } return $this->isActiveTrial() ? Carbon::today()->diffInDays($this->trial_ends_on) : 0; @@ -116,12 +117,12 @@ public function remainingTrialDays() /** * Returns the used trial days. * - * @return integer|null + * @return int|null */ public function usedTrialDays() { if (!$this->isTrial()) { - return null; + return; } return $this->trial_days - $this->remainingTrialDays(); diff --git a/src/ShopifyApp/Traits/BillingControllerTrait.php b/src/ShopifyApp/Traits/BillingControllerTrait.php index d0b2e7c3..f5293744 100644 --- a/src/ShopifyApp/Traits/BillingControllerTrait.php +++ b/src/ShopifyApp/Traits/BillingControllerTrait.php @@ -2,10 +2,10 @@ namespace OhMyBrew\ShopifyApp\Traits; +use Carbon\Carbon; use OhMyBrew\ShopifyApp\Facades\ShopifyApp; use OhMyBrew\ShopifyApp\Libraries\BillingPlan; use OhMyBrew\ShopifyApp\Models\Charge; -use Carbon\Carbon; trait BillingControllerTrait { diff --git a/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php b/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php index 26d7fe3f..3c4dee99 100644 --- a/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php +++ b/src/ShopifyApp/resources/database/migrations/2018_06_03_185902_create_charges_table.php @@ -50,7 +50,7 @@ public function up() $table->timestamp('billing_on')->nullable(); // When activation happened - $table->timestamp('activated_on')->nullable(); + $table->timestamp('activated_on')->nullable(); // Date the trial period ends $table->timestamp('trial_ends_on')->nullable(); diff --git a/tests/Controllers/BillingControllerTest.php b/tests/Controllers/BillingControllerTest.php index 081db981..a2adc2ae 100644 --- a/tests/Controllers/BillingControllerTest.php +++ b/tests/Controllers/BillingControllerTest.php @@ -1,4 +1,5 @@ assertTrue($shop->trashed()); $this->assertSoftDeleted('shops', [ - 'id' => $shop->id, + 'id' => $shop->id, 'shopify_domain' => $shop->shopify_domain, ]); diff --git a/tests/TestCase.php b/tests/TestCase.php index 90a07f18..879e6165 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,12 +2,12 @@ namespace OhMyBrew\ShopifyApp\Test; -use OhMyBrew\ShopifyApp\Models\Shop; +use Carbon\Carbon; use OhMyBrew\ShopifyApp\Models\Charge; +use OhMyBrew\ShopifyApp\Models\Shop; use OhMyBrew\ShopifyApp\ShopifyAppProvider; use Orchestra\Database\ConsoleServiceProvider; use Orchestra\Testbench\TestCase as OrchestraTestCase; -use Carbon\Carbon; abstract class TestCase extends OrchestraTestCase { From 7b8ca1ab9a4a5e53e8a6ff524bf2f44c9a7cd48e Mon Sep 17 00:00:00 2001 From: Tyler King Date: Tue, 12 Jun 2018 00:44:51 -0230 Subject: [PATCH 07/18] Adjustments to markdown files, and fix to enable current coverage to be complete and tests to pass --- CONTRIBUTING.md | 3 ++- LICENSE | 2 +- README.md | 1 + tests/Libraries/BillingPlanTest.php | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c20f58e9..a0356cff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,6 @@ Well, you can: You don't have to be a superstar, or someone with experience, you can just dive in and help if you feel you can. - ## Adding a new feature? Its best to: @@ -61,6 +60,8 @@ We use PHPUnit to run tests. Simply run `composer install` and a symlink `phpuni Next, run `bin/phpunit` and the rest will be taken care of. Upon any pull requests and merges, TravisCI will check the code to ensure all test suites pass. +For quicker tests, be sure to disable coverage with `bin/php --no-coverage`. + ----- Thats it! Enjoy. diff --git a/LICENSE b/LICENSE index 9372e8b7..ca83347c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2017 Tyler King +Copyright (C) 2018 Tyler King Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index f4aa56ed..d4bb9f54 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ For more information, tutorials, etc., please view the project's [wiki](https:// - [x] Integration with Shopify API - [x] Authentication & installation for shops - [x] Billing integration built-in for single and recurring application charges +- [x] Tracking charges to a shop (recurring, single, usage, etc) with trial support - [x] Auto install app webhooks and scripttags thorugh background jobs - [x] Provide basic ESDK views - [x] Handles and processes incoming webhooks diff --git a/tests/Libraries/BillingPlanTest.php b/tests/Libraries/BillingPlanTest.php index 16be9d53..9bfd4257 100644 --- a/tests/Libraries/BillingPlanTest.php +++ b/tests/Libraries/BillingPlanTest.php @@ -64,7 +64,7 @@ public function testShouldActivatePlan() $response = (new BillingPlan($this->shop))->setChargeId(1029266947)->activate(); $this->assertEquals(true, is_object($response)); - $this->assertEquals('accepted', $response->status); + $this->assertEquals('active', $response->status); } /** From a318d4d5ead6ea227f02a8be6a9e4cdb1ae9040e Mon Sep 17 00:00:00 2001 From: Tyler King Date: Mon, 18 Jun 2018 10:08:10 -0230 Subject: [PATCH 08/18] Start of the app uninstalled job --- src/ShopifyApp/Jobs/AppUninstalledJob.php | 108 ++++++++++++++++++++++ src/ShopifyApp/Middleware/Billable.php | 5 +- src/ShopifyApp/Models/Charge.php | 20 ++++ tests/Models/ChargeModelTest.php | 14 +++ tests/TestCase.php | 12 +++ 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/ShopifyApp/Jobs/AppUninstalledJob.php diff --git a/src/ShopifyApp/Jobs/AppUninstalledJob.php b/src/ShopifyApp/Jobs/AppUninstalledJob.php new file mode 100644 index 00000000..67654953 --- /dev/null +++ b/src/ShopifyApp/Jobs/AppUninstalledJob.php @@ -0,0 +1,108 @@ +shopDomain = $shopDomain; + $this->data = $data; + $this->shop = $this->findShop(); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->softDeleteShop(); + $this->cancelCharge(); + } + + /** + * Soft deletes the shop in the database. + * + * @return void + */ + protected function softDeleteShop() + { + if ($this->shop) { + $this->shop->delete(); + $this->shop->charges()->delete(); + } + } + + /** + * Cancels a recurring or one-time charge. + * + * @return void + */ + protected function cancelCharge() + { + $lastCharge = $shop->charges() + ->where(function ($query) { + $query->latestByType(Charge::CHARGE_RECURRING); + })->orWhere(function ($query) { + $query->latestByType(Charge::CHARGE_ONETIME); + })->latest()->first(); + + if ($lastCharge && (!$lastCharge->isDeclined() && !$lastCharge->isCancelled())) { + $lastCharge->status = 'cancelled'; + $lastCharge->cancelled_on = Carbon::today()->format('Y-m-d'); + $lastCharge->save(); + } + } + + /** + * Finds the shop based on domain from the webhook. + * + * @return Shop|null + */ + protected function findShop() + { + return Shop::where(['shopify_domain' => $this->shopDomain])->first(); + } +} diff --git a/src/ShopifyApp/Middleware/Billable.php b/src/ShopifyApp/Middleware/Billable.php index 0ce90c8d..f0a4608e 100644 --- a/src/ShopifyApp/Middleware/Billable.php +++ b/src/ShopifyApp/Middleware/Billable.php @@ -29,7 +29,10 @@ public function handle(Request $request, Closure $next) $query->latestByType(Charge::CHARGE_ONETIME); })->latest()->first(); - if (!$shop->isGrandfathered() && (is_null($lastCharge) || $lastCharge->isDeclined())) { + if ( + !$shop->isGrandfathered() && + (is_null($lastCharge) || $lastCharge->isDeclined() || $lastCharge->isCancelled()) + ) { // They're not grandfathered in, and there is no charge or charge was declined... redirect to billing return redirect()->route('billing'); } diff --git a/src/ShopifyApp/Models/Charge.php b/src/ShopifyApp/Models/Charge.php index f5a8f60c..b002f40d 100644 --- a/src/ShopifyApp/Models/Charge.php +++ b/src/ShopifyApp/Models/Charge.php @@ -157,4 +157,24 @@ public function isDeclined() { return $this->status === 'declined'; } + + /** + * Checks if the charge was cancelled. + * + * @return bool + */ + public function isCancelled() + { + return !is_null($this->cancelled_on) || $this->status === 'cancelled'; + } + + /** + * Checks if the charge is "active" (non-API check). + * + * @return boolean + */ + public function isOngoing() + { + return $this->isActive() && !$this->isCancelled(); + } } diff --git a/tests/Models/ChargeModelTest.php b/tests/Models/ChargeModelTest.php index 09ba6e1e..63a9908c 100644 --- a/tests/Models/ChargeModelTest.php +++ b/tests/Models/ChargeModelTest.php @@ -89,4 +89,18 @@ public function testActive() $this->assertFalse(Charge::find(1)->isActive()); $this->assertTrue(Charge::find(4)->isActive()); } + + public function testOngoing() + { + $this->assertFalse(Charge::find(1)->isOngoing()); + $this->assertFalse(Charge::find(6)->isOngoing()); + $this->assertTrue(Charge::find(4)->isOngoing()); + } + + public function testCancelled() + { + $this->assertFalse(Charge::find(1)->isCancelled()); + $this->assertFalse(Charge::find(4)->isCancelled()); + $this->assertTrue(Charge::find(6)->isCancelled()); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 879e6165..051b0bbe 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -162,5 +162,17 @@ public function createCharges() $charge->price = 25.00; $charge->shop_id = Shop::where('shopify_domain', 'no-token-shop.myshopify.com')->first()->id; $charge->save(); + + // Test = false, status = cancelled + $charge = new Charge(); + $charge->charge_id = 783873873; + $charge->test = false; + $charge->name = 'Base Plan Cancelled'; + $charge->status = 'active'; + $charge->type = 1; + $charge->price = 25.00; + $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; + $charge->cancelled_on = Carbon::today()->format('Y-m-d'); + $charge->save(); } } From 0cdfeb8762e9c6a3c53fd2656d1945346cfaf7a7 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Tue, 19 Jun 2018 23:32:49 -0230 Subject: [PATCH 09/18] Merge with master, completed test for app uninstall job --- src/ShopifyApp/Jobs/AppUninstalledJob.php | 33 +++++---- src/ShopifyApp/Middleware/Billable.php | 2 +- tests/Jobs/AppUninstalledJobTest.php | 88 +++++++++++++++++++++++ tests/fixtures/app_uninstalled.json | 54 ++++++++++++++ 4 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 tests/Jobs/AppUninstalledJobTest.php create mode 100644 tests/fixtures/app_uninstalled.json diff --git a/src/ShopifyApp/Jobs/AppUninstalledJob.php b/src/ShopifyApp/Jobs/AppUninstalledJob.php index 67654953..7c86099f 100644 --- a/src/ShopifyApp/Jobs/AppUninstalledJob.php +++ b/src/ShopifyApp/Jobs/AppUninstalledJob.php @@ -1,6 +1,6 @@ shopDomain = $shopDomain; $this->data = $data; + $this->shopDomain = $shopDomain; $this->shop = $this->findShop(); } /** * Execute the job. * - * @return void + * @return bool */ public function handle() { + if (!$this->shop) { + return false; + } + $this->softDeleteShop(); $this->cancelCharge(); + + return true; } /** @@ -69,10 +75,8 @@ public function handle() */ protected function softDeleteShop() { - if ($this->shop) { - $this->shop->delete(); - $this->shop->charges()->delete(); - } + $this->shop->delete(); + $this->shop->charges()->delete(); } /** @@ -82,14 +86,15 @@ protected function softDeleteShop() */ protected function cancelCharge() { - $lastCharge = $shop->charges() + $lastCharge = $this->shop->charges() + ->withTrashed() ->where(function ($query) { $query->latestByType(Charge::CHARGE_RECURRING); })->orWhere(function ($query) { $query->latestByType(Charge::CHARGE_ONETIME); - })->latest()->first(); + })->latest(); - if ($lastCharge && (!$lastCharge->isDeclined() && !$lastCharge->isCancelled())) { + if ($lastCharge && !$lastCharge->isDeclined() && !$lastCharge->isCancelled()) { $lastCharge->status = 'cancelled'; $lastCharge->cancelled_on = Carbon::today()->format('Y-m-d'); $lastCharge->save(); diff --git a/src/ShopifyApp/Middleware/Billable.php b/src/ShopifyApp/Middleware/Billable.php index f0a4608e..fb5d8713 100644 --- a/src/ShopifyApp/Middleware/Billable.php +++ b/src/ShopifyApp/Middleware/Billable.php @@ -27,7 +27,7 @@ public function handle(Request $request, Closure $next) $query->latestByType(Charge::CHARGE_RECURRING); })->orWhere(function ($query) { $query->latestByType(Charge::CHARGE_ONETIME); - })->latest()->first(); + })->latest(); if ( !$shop->isGrandfathered() && diff --git a/tests/Jobs/AppUninstalledJobTest.php b/tests/Jobs/AppUninstalledJobTest.php new file mode 100644 index 00000000..db56ce0a --- /dev/null +++ b/tests/Jobs/AppUninstalledJobTest.php @@ -0,0 +1,88 @@ +shop = Shop::find(1); + $this->data = json_decode(file_get_contents(__DIR__.'/../fixtures/app_uninstalled.json')); + } + + public function testJobAcceptsLoad() + { + $job = new AppUninstalledJob($this->shop->shopify_domain, $this->data); + + $refJob = new ReflectionObject($job); + $refData = $refJob->getProperty('data'); + $refData->setAccessible(true); + $refShopDomain = $refJob->getProperty('shopDomain'); + $refShopDomain->setAccessible(true); + $refShop = $refJob->getProperty('shop'); + $refShop->setAccessible(true); + + $this->assertEquals($this->shop->shopify_domain, $refShopDomain->getValue($job)); + $this->assertEquals($this->data, $refData->getValue($job)); + $this->assertEquals($this->shop->shopify_domain, $refShop->getValue($job)->shopify_domain); + } + + public function testJobSoftDeletesShopAndCharges() + { + // Create a new charge to test against + $charge = new Charge(); + $charge->charge_id = 987654321; + $charge->test = false; + $charge->name = 'Base Plan Dummy'; + $charge->status = 'active'; + $charge->type = 1; + $charge->price = 25.00; + $charge->trial_days = 0; + $charge->shop_id = $this->shop->id; + $charge->created_at = Carbon::now()->addDays(1); // Test runs too fast to make "latest" work + $charge->save(); + + // Ensure shop is not trashed, and has charges + $this->shop->refresh(); + $this->assertFalse($this->shop->trashed()); + $this->assertEquals(true, $this->shop->hasCharges()); + + // Run the job + $job = new AppUninstalledJob($this->shop->shopify_domain, $this->data); + $result = $job->handle(); + + // Refresh both models to see the changes + $this->shop->refresh(); + $lastCharge = $this->shop->charges() + ->withTrashed() + ->where(function ($query) { + $query->latestByType(Charge::CHARGE_RECURRING); + })->orWhere(function ($query) { + $query->latestByType(Charge::CHARGE_ONETIME); + })->latest(); + + // Confirm job worked... + $this->assertEquals(true, $result); + $this->assertEquals(true, $this->shop->trashed()); + $this->assertFalse($this->shop->hasCharges()); + $this->assertEquals($charge->charge_id, $lastCharge->charge_id); + $this->assertEquals('cancelled', $lastCharge->status); + } + + public function testJobDoesNothingForUnknownShop() + { + $job = new AppUninstalledJob('unknown-shop.myshopify.com', null); + $this->assertEquals(false, $job->handle()); + } +} diff --git a/tests/fixtures/app_uninstalled.json b/tests/fixtures/app_uninstalled.json new file mode 100644 index 00000000..bf49e29e --- /dev/null +++ b/tests/fixtures/app_uninstalled.json @@ -0,0 +1,54 @@ +{ + "id": 690933842, + "name": "Super Toys", + "email": "super@supertoys.com", + "domain": null, + "province": "Tennessee", + "country": "US", + "address1": "190 MacLaren Street", + "zip": "37178", + "city": "Houston", + "source": null, + "phone": "3213213210", + "latitude": null, + "longitude": null, + "primary_locale": "en", + "address2": null, + "created_at": null, + "updated_at": null, + "country_code": "US", + "country_name": "United States", + "currency": "USD", + "customer_email": "super@supertoys.com", + "timezone": "(GMT-05:00) Eastern Time (US & Canada)", + "iana_timezone": null, + "shop_owner": "Steve Jobs", + "money_format": "$", + "money_with_currency_format": "$ USD", + "weight_unit": "kg", + "province_code": "TN", + "taxes_included": null, + "tax_shipping": null, + "county_taxes": null, + "plan_display_name": "Shopify Plus", + "plan_name": "enterprise", + "has_discounts": true, + "has_gift_cards": true, + "myshopify_domain": "example.myshopify.com", + "google_apps_domain": null, + "google_apps_login_enabled": null, + "money_in_emails_format": "$", + "money_with_currency_in_emails_format": "$ USD", + "eligible_for_payments": true, + "requires_extra_payments_agreement": false, + "password_enabled": null, + "has_storefront": true, + "eligible_for_card_reader_giveaway": false, + "finances": true, + "primary_location_id": 905684977, + "checkout_api_supported": true, + "multi_location_enabled": false, + "setup_required": false, + "force_ssl": false, + "pre_launch_enabled": false +} \ No newline at end of file From 307b234018bb71eb5958e4c35c3d83cbb3d38705 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Tue, 19 Jun 2018 23:54:53 -0230 Subject: [PATCH 10/18] Removed unneeded helpers --- src/ShopifyApp/Jobs/AppUninstalledJob.php | 8 +++----- src/ShopifyApp/Middleware/Billable.php | 8 +++----- src/ShopifyApp/Models/Charge.php | 25 ----------------------- tests/Jobs/AppUninstalledJobTest.php | 8 +++----- tests/Models/ChargeModelTest.php | 16 --------------- 5 files changed, 9 insertions(+), 56 deletions(-) diff --git a/src/ShopifyApp/Jobs/AppUninstalledJob.php b/src/ShopifyApp/Jobs/AppUninstalledJob.php index 7c86099f..262f33db 100644 --- a/src/ShopifyApp/Jobs/AppUninstalledJob.php +++ b/src/ShopifyApp/Jobs/AppUninstalledJob.php @@ -88,11 +88,9 @@ protected function cancelCharge() { $lastCharge = $this->shop->charges() ->withTrashed() - ->where(function ($query) { - $query->latestByType(Charge::CHARGE_RECURRING); - })->orWhere(function ($query) { - $query->latestByType(Charge::CHARGE_ONETIME); - })->latest(); + ->whereIn('type', [Charge::CHARGE_RECURRING, Charge::CHARGE_ONETIME]) + ->orderBy('created_at', 'desc') + ->first(); if ($lastCharge && !$lastCharge->isDeclined() && !$lastCharge->isCancelled()) { $lastCharge->status = 'cancelled'; diff --git a/src/ShopifyApp/Middleware/Billable.php b/src/ShopifyApp/Middleware/Billable.php index fb5d8713..3d85557d 100644 --- a/src/ShopifyApp/Middleware/Billable.php +++ b/src/ShopifyApp/Middleware/Billable.php @@ -23,11 +23,9 @@ public function handle(Request $request, Closure $next) // Grab the shop and last recurring or one-time charge $shop = ShopifyApp::shop(); $lastCharge = $shop->charges() - ->where(function ($query) { - $query->latestByType(Charge::CHARGE_RECURRING); - })->orWhere(function ($query) { - $query->latestByType(Charge::CHARGE_ONETIME); - })->latest(); + ->whereIn('type', [Charge::CHARGE_RECURRING, Charge::CHARGE_ONETIME]) + ->orderBy('created_at', 'desc') + ->first(); if ( !$shop->isGrandfathered() && diff --git a/src/ShopifyApp/Models/Charge.php b/src/ShopifyApp/Models/Charge.php index b002f40d..7b09bae2 100644 --- a/src/ShopifyApp/Models/Charge.php +++ b/src/ShopifyApp/Models/Charge.php @@ -23,31 +23,6 @@ class Charge extends Model */ protected $dates = ['deleted_at']; - /** - * Scope for latest charge for a shop. - * - * @param \Illuminate\Database\Eloquent\Builder $query The query builder - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeLatest($query) - { - return $query->orderBy('created_at', 'desc')->first(); - } - - /** - * Scope for latest charge by type for a shop. - * - * @param \Illuminate\Database\Eloquent\Builder $query The query builder - * @param int $type The type of charge - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeLatestByType($query, int $type) - { - return $query->where('type', $type)->orderBy('created_at', 'desc')->first(); - } - /** * Gets the shop for the charge. * diff --git a/tests/Jobs/AppUninstalledJobTest.php b/tests/Jobs/AppUninstalledJobTest.php index db56ce0a..aaf21761 100644 --- a/tests/Jobs/AppUninstalledJobTest.php +++ b/tests/Jobs/AppUninstalledJobTest.php @@ -66,11 +66,9 @@ public function testJobSoftDeletesShopAndCharges() $this->shop->refresh(); $lastCharge = $this->shop->charges() ->withTrashed() - ->where(function ($query) { - $query->latestByType(Charge::CHARGE_RECURRING); - })->orWhere(function ($query) { - $query->latestByType(Charge::CHARGE_ONETIME); - })->latest(); + ->whereIn('type', [Charge::CHARGE_RECURRING, Charge::CHARGE_ONETIME]) + ->orderBy('created_at', 'desc') + ->first(); // Confirm job worked... $this->assertEquals(true, $result); diff --git a/tests/Models/ChargeModelTest.php b/tests/Models/ChargeModelTest.php index 63a9908c..73a8f8c9 100644 --- a/tests/Models/ChargeModelTest.php +++ b/tests/Models/ChargeModelTest.php @@ -24,22 +24,6 @@ public function testChargeImplementsType() ); } - public function testScopeLatest() - { - $this->assertEquals( - get_class(Charge::latest()), - Charge::class - ); - } - - public function testScopeLatestByType() - { - $this->assertEquals( - get_class(Charge::latestByType(Charge::CHARGE_RECURRING)), - Charge::class - ); - } - public function testIsTest() { $this->assertEquals(true, Charge::find(1)->isTest()); From 56523fe374efe3f211714feb65acdd37cdcd9d55 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Wed, 20 Jun 2018 02:26:25 +0000 Subject: [PATCH 11/18] Apply fixes from StyleCI --- src/ShopifyApp/Jobs/AppUninstalledJob.php | 6 +++--- src/ShopifyApp/Models/Charge.php | 2 +- tests/Jobs/AppUninstalledJobTest.php | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ShopifyApp/Jobs/AppUninstalledJob.php b/src/ShopifyApp/Jobs/AppUninstalledJob.php index 262f33db..46d5fdd6 100644 --- a/src/ShopifyApp/Jobs/AppUninstalledJob.php +++ b/src/ShopifyApp/Jobs/AppUninstalledJob.php @@ -4,12 +4,12 @@ use Carbon\Carbon; use Illuminate\Bus\Queueable; -use Illuminate\Queue\SerializesModels; -use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use OhMyBrew\ShopifyApp\Models\Shop; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; use OhMyBrew\ShopifyApp\Models\Charge; +use OhMyBrew\ShopifyApp\Models\Shop; class AppUninstalledJob implements ShouldQueue { diff --git a/src/ShopifyApp/Models/Charge.php b/src/ShopifyApp/Models/Charge.php index 7b09bae2..ab06b553 100644 --- a/src/ShopifyApp/Models/Charge.php +++ b/src/ShopifyApp/Models/Charge.php @@ -146,7 +146,7 @@ public function isCancelled() /** * Checks if the charge is "active" (non-API check). * - * @return boolean + * @return bool */ public function isOngoing() { diff --git a/tests/Jobs/AppUninstalledJobTest.php b/tests/Jobs/AppUninstalledJobTest.php index aaf21761..b2edec50 100644 --- a/tests/Jobs/AppUninstalledJobTest.php +++ b/tests/Jobs/AppUninstalledJobTest.php @@ -4,13 +4,12 @@ use Carbon\Carbon; use OhMyBrew\ShopifyApp\Jobs\AppUninstalledJob; -use OhMyBrew\ShopifyApp\Models\Shop; use OhMyBrew\ShopifyApp\Models\Charge; +use OhMyBrew\ShopifyApp\Models\Shop; use OhMyBrew\ShopifyApp\Test\TestCase; -use ReflectionMethod; use ReflectionObject; -class AppUninstalledJobJobTest extends TestCase +class AppUninstalledJobTest extends TestCase { public function setup() { From f1d18601881278819695a8c72f8ca7e95b04c404 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Wed, 20 Jun 2018 09:01:02 -0230 Subject: [PATCH 12/18] Run test for uninstall in isolated process --- tests/Jobs/AppUninstalledJobTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Jobs/AppUninstalledJobTest.php b/tests/Jobs/AppUninstalledJobTest.php index aaf21761..dd8da946 100644 --- a/tests/Jobs/AppUninstalledJobTest.php +++ b/tests/Jobs/AppUninstalledJobTest.php @@ -38,6 +38,9 @@ public function testJobAcceptsLoad() $this->assertEquals($this->shop->shopify_domain, $refShop->getValue($job)->shopify_domain); } + /** + * @runInSeparateProcess + */ public function testJobSoftDeletesShopAndCharges() { // Create a new charge to test against From f745b393cee1cbea964983a2d7d8357829e7cf96 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Wed, 20 Jun 2018 09:03:30 -0230 Subject: [PATCH 13/18] Run uninstall on isolated shop --- tests/Jobs/AppUninstalledJobTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Jobs/AppUninstalledJobTest.php b/tests/Jobs/AppUninstalledJobTest.php index dd8da946..416a339f 100644 --- a/tests/Jobs/AppUninstalledJobTest.php +++ b/tests/Jobs/AppUninstalledJobTest.php @@ -16,8 +16,11 @@ public function setup() { parent::setup(); - // Re-used variables - $this->shop = Shop::find(1); + // Isolated shop + $this->shop = new Shop(); + $this->shop->shopify_domain = 'example-isolated.myshopify.com'; + $this->shop->save(); + $this->data = json_decode(file_get_contents(__DIR__.'/../fixtures/app_uninstalled.json')); } @@ -38,9 +41,6 @@ public function testJobAcceptsLoad() $this->assertEquals($this->shop->shopify_domain, $refShop->getValue($job)->shopify_domain); } - /** - * @runInSeparateProcess - */ public function testJobSoftDeletesShopAndCharges() { // Create a new charge to test against From 74172976166a1130b4098868bf67ed04a8fe7600 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Wed, 27 Jun 2018 15:54:23 -0230 Subject: [PATCH 14/18] Publishable job --- src/ShopifyApp/ShopifyAppProvider.php | 5 +++++ src/ShopifyApp/resources/jobs/AppUninstalledJob.php | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 src/ShopifyApp/resources/jobs/AppUninstalledJob.php diff --git a/src/ShopifyApp/ShopifyAppProvider.php b/src/ShopifyApp/ShopifyAppProvider.php index c98c8131..cee048bb 100644 --- a/src/ShopifyApp/ShopifyAppProvider.php +++ b/src/ShopifyApp/ShopifyAppProvider.php @@ -28,6 +28,11 @@ public function boot() $this->publishes([ __DIR__.'/resources/database/migrations' => database_path('migrations'), ], 'migrations'); + + // Job publish + $this->publishes([ + __DIR__.'/resources/jobs/AppUninstalledJob.php' => app_path().'/Jobs/AppUninstalledJob.php', + ], 'jobs'); } /** diff --git a/src/ShopifyApp/resources/jobs/AppUninstalledJob.php b/src/ShopifyApp/resources/jobs/AppUninstalledJob.php new file mode 100644 index 00000000..8b81dc48 --- /dev/null +++ b/src/ShopifyApp/resources/jobs/AppUninstalledJob.php @@ -0,0 +1,5 @@ + Date: Wed, 27 Jun 2018 16:17:54 -0230 Subject: [PATCH 15/18] Determine the days difference from cancelled date and trial ends date --- src/ShopifyApp/Models/Charge.php | 23 +++++++++++++++++++++++ tests/Models/ChargeModelTest.php | 9 ++++++++- tests/TestCase.php | 14 ++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/ShopifyApp/Models/Charge.php b/src/ShopifyApp/Models/Charge.php index ab06b553..d7c8a8d0 100644 --- a/src/ShopifyApp/Models/Charge.php +++ b/src/ShopifyApp/Models/Charge.php @@ -89,6 +89,29 @@ public function remainingTrialDays() return $this->isActiveTrial() ? Carbon::today()->diffInDays($this->trial_ends_on) : 0; } + /** + * Returns the remaining trial days from cancellation date. + * + * @return int + */ + public function remainingTrialDaysFromCancel() + { + if (!$this->isTrial()) { + return; + } + + $cancelledDate = Carbon::parse($this->cancelled_on); + $trialEndsDate = Carbon::parse($this->trial_ends_on); + + // Ensure cancelled date happened before the trial was supposed to end + if ($this->isCancelled() && $cancelledDate->lte($trialEndsDate)) { + // Diffeence the two dates and subtract from the total trial days to get whats remaining + return $this->trial_days - ($this->trial_days - $cancelledDate->diffInDays($trialEndsDate)); + } + + return 0; + } + /** * Returns the used trial days. * diff --git a/tests/Models/ChargeModelTest.php b/tests/Models/ChargeModelTest.php index 73a8f8c9..6a7131e0 100644 --- a/tests/Models/ChargeModelTest.php +++ b/tests/Models/ChargeModelTest.php @@ -85,6 +85,13 @@ public function testCancelled() { $this->assertFalse(Charge::find(1)->isCancelled()); $this->assertFalse(Charge::find(4)->isCancelled()); - $this->assertTrue(Charge::find(6)->isCancelled()); + $this->assertTrue(Charge::find(6)->isCancelled());; + } + + public function testRemainingTrialDaysFromCancel() + { + $this->assertEquals(5, Charge::find(7)->remainingTrialDaysFromCancel()); + $this->assertEquals(0, Charge::find(1)->remainingTrialDaysFromCancel()); + $this->assertEquals(0, Charge::find(5)->remainingTrialDaysFromCancel()); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 051b0bbe..05ae4de4 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -174,5 +174,19 @@ public function createCharges() $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; $charge->cancelled_on = Carbon::today()->format('Y-m-d'); $charge->save(); + + // Test = false, status = cancelled, trial = 7 + $charge = new Charge(); + $charge->charge_id = 928736721; + $charge->test = false; + $charge->name = 'Base Plan Cancelled'; + $charge->status = 'cancelled'; + $charge->type = 1; + $charge->price = 25.00; + $charge->trial_days = 7; + $charge->trial_ends_on = Carbon::today()->addWeeks(1)->format('Y-m-d'); + $charge->cancelled_on = Carbon::today()->addDays(2)->format('Y-m-d'); + $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; + $charge->save(); } } From bef44d61f0453ba55b304ccc83606e3e04e38e29 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Wed, 27 Jun 2018 16:50:41 -0230 Subject: [PATCH 16/18] Change plan based on charge --- .../Traits/BillingControllerTrait.php | 34 +++++++++++--- tests/Controllers/BillingControllerTest.php | 47 +++++++++++++++++-- tests/TestCase.php | 2 +- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/ShopifyApp/Traits/BillingControllerTrait.php b/src/ShopifyApp/Traits/BillingControllerTrait.php index f5293744..bf13dd59 100644 --- a/src/ShopifyApp/Traits/BillingControllerTrait.php +++ b/src/ShopifyApp/Traits/BillingControllerTrait.php @@ -5,6 +5,7 @@ use Carbon\Carbon; use OhMyBrew\ShopifyApp\Facades\ShopifyApp; use OhMyBrew\ShopifyApp\Libraries\BillingPlan; +use OhMyBrew\ShopifyApp\Models\Shop; use OhMyBrew\ShopifyApp\Models\Charge; trait BillingControllerTrait @@ -17,8 +18,9 @@ trait BillingControllerTrait public function index() { // Get the confirmation URL - $plan = new BillingPlan(ShopifyApp::shop(), $this->chargeType()); - $plan->setDetails($this->planDetails()); + $shop = ShopifyApp::shop(); + $plan = new BillingPlan($shop, $this->chargeType()); + $plan->setDetails($this->planDetails($shop)); // Do a fullpage redirect return view('shopify-app::billing.fullpage_redirect', [ @@ -43,7 +45,7 @@ public function process() $status = $plan->getCharge()->status; // Grab the plan detailed used - $planDetails = $this->planDetails(); + $planDetails = $this->planDetails($shop); unset($planDetails['return_url']); // Create a charge (regardless of the status) @@ -83,18 +85,22 @@ public function process() } /** - * Base plan to use for billing. - * Setup as a function so its patchable. + * Base plan to use for billing. Setup as a function so its patchable. + * Checks for cancelled charge within trial day limit, and issues + * a new trial days number depending on the result for shops who + * resinstall the app. + * + * @param object $shop The shop object. * * @return array */ - protected function planDetails() + protected function planDetails(Shop $shop) { + // Initial plan details $plan = [ 'name' => config('shopify-app.billing_plan'), 'price' => config('shopify-app.billing_price'), 'test' => config('shopify-app.billing_test'), - 'trial_days' => config('shopify-app.billing_trial_days'), 'return_url' => url(config('shopify-app.billing_redirect')), ]; @@ -104,6 +110,20 @@ protected function planDetails() $plan['terms'] = config('shopify-app.billing_terms'); } + // Grab the last charge for the shop (if any) to determine if this shop + // reinstalled the app so we can issue new trial days based on result + $lastCharge = $shop->charges() + ->whereIn('type', [Charge::CHARGE_RECURRING, Charge::CHARGE_ONETIME]) + ->orderBy('created_at', 'desc') + ->first(); + if ($lastCharge && $lastCharge->isCancelled()) { + // Return the new trial days, could result in 0 + $plan['trial_days'] = $lastCharge->remainingTrialDaysFromCancel(); + } else { + // Set initial trial days fromc config + $plan['trial_days'] = config('shopify-app.billing_trial_days'); + } + return $plan; } diff --git a/tests/Controllers/BillingControllerTest.php b/tests/Controllers/BillingControllerTest.php index a2adc2ae..3d6764df 100644 --- a/tests/Controllers/BillingControllerTest.php +++ b/tests/Controllers/BillingControllerTest.php @@ -2,8 +2,10 @@ namespace OhMyBrew\ShopifyApp\Test\Controllers; +use Carbon\Carbon; use OhMyBrew\ShopifyApp\Controllers\BillingController; use OhMyBrew\ShopifyApp\Models\Shop; +use OhMyBrew\ShopifyApp\Models\Charge; use OhMyBrew\ShopifyApp\Test\Stubs\ApiStub; use OhMyBrew\ShopifyApp\Test\TestCase; use ReflectionMethod; @@ -18,7 +20,8 @@ public function setUp() config(['shopify-app.api_class' => new ApiStub()]); // Base shop for all tests here - session(['shopify_domain' => 'example.myshopify.com']); + $this->shop = Shop::where('shopify_domain', 'example.myshopify.com')->first(); + session(['shopify_domain' => $this->shop->shopify_domain]); } public function testSendsShopToBillingScreen() @@ -69,7 +72,7 @@ public function testReturnsBasePlanDetails() 'trial_days' => config('shopify-app.billing_trial_days'), 'return_url' => url(config('shopify-app.billing_redirect')), ], - $method->invoke($controller, 'planDetails') + $method->invoke($controller, $this->shop) ); } @@ -93,7 +96,43 @@ public function testReturnsBasePlanDetailsWithUsage() 'terms' => config('shopify-app.billing_terms'), 'return_url' => url(config('shopify-app.billing_redirect')), ], - $method->invoke($controller, 'planDetails') + $method->invoke($controller, $this->shop) + ); + } + + public function testReturnsBasePlanDetailsChangedByCancelledCharge() + { + $shop = new Shop(); + $shop->shopify_domain = 'test-cancelled-shop.myshopify.com'; + $shop->save(); + + $charge = new Charge(); + $charge->charge_id = 267921978; + $charge->test = false; + $charge->name = 'Base Plan Cancelled'; + $charge->status = 'cancelled'; + $charge->type = 1; + $charge->price = 25.00; + $charge->trial_days = 7; + $charge->trial_ends_on = Carbon::today()->addWeeks(1)->format('Y-m-d'); + $charge->cancelled_on = Carbon::today()->addDays(2)->format('Y-m-d'); + $charge->shop_id = $shop->id; + $charge->save(); + + $controller = new BillingController(); + $method = new ReflectionMethod(BillingController::class, 'planDetails'); + $method->setAccessible(true); + + // Based on default config + $this->assertEquals( + [ + 'name' => config('shopify-app.billing_plan'), + 'price' => config('shopify-app.billing_price'), + 'test' => config('shopify-app.billing_test'), + 'trial_days' => 5, + 'return_url' => url(config('shopify-app.billing_redirect')), + ], + $method->invoke($controller, $shop) ); } @@ -104,6 +143,6 @@ public function testReturnsBaseChargeType() $method->setAccessible(true); // Based on default config - $this->assertEquals(config('shopify-app.billing_type'), $method->invoke($controller, 'chargeType')); + $this->assertEquals(config('shopify-app.billing_type'), $method->invoke($controller)); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 05ae4de4..eb6fa09e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -186,7 +186,7 @@ public function createCharges() $charge->trial_days = 7; $charge->trial_ends_on = Carbon::today()->addWeeks(1)->format('Y-m-d'); $charge->cancelled_on = Carbon::today()->addDays(2)->format('Y-m-d'); - $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; + $charge->shop_id = Shop::withTrashed()->where('shopify_domain', 'trashed-shop.myshopify.com')->first()->id; $charge->save(); } } From f0d8891aa1ebb9db161ad326ea66af9faa7f09a4 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Wed, 27 Jun 2018 17:02:46 -0230 Subject: [PATCH 17/18] Cleaned up test object --- tests/TestCase.php | 242 ++++++++++++++++++++++++--------------------- 1 file changed, 132 insertions(+), 110 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index eb6fa09e..b3329ce6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -68,31 +68,42 @@ protected function seedDatabase() protected function createShops() { - // Paid shop, not grandfathered - $shop = new Shop(); - $shop->shopify_domain = 'example.myshopify.com'; - $shop->shopify_token = '1234'; - $shop->save(); - - // Non-paid shop, grandfathered - $shop = new Shop(); - $shop->shopify_domain = 'grandfathered.myshopify.com'; - $shop->shopify_token = '1234'; - $shop->grandfathered = true; - $shop->save(); - - // New shop... non-paid, not grandfathered - $shop = new Shop(); - $shop->shopify_domain = 'new-shop.myshopify.com'; - $shop->shopify_token = '1234'; - $shop->save(); + $shops = [ + // Paid shop, not grandfathered + [ + 'shopify_domain' => 'example.myshopify.com', + 'shopify_token' => '1234', + ], + + // Non-paid shop, grandfathered + [ + 'shopify_domain' => 'grandfathered.myshopify.com', + 'shopify_token' => '1234', + 'grandfathered' => true, + ], + + // New shop... non-paid, not grandfathered + [ + 'shopify_domain' => 'new-shop.myshopify.com', + 'shopify_token' => '1234', + ], + + // New shop... no token, not grandfathered + [ + 'shopify_domain' => 'no-token-shop.myshopify.com', + ], + ]; - // New shop... no token, not grandfathered - $shop = new Shop(); - $shop->shopify_domain = 'no-token-shop.myshopify.com'; - $shop->save(); + // Build the shops + foreach ($shops as $shopData) { + $shop = new Shop(); + foreach ($shopData as $key => $value) { + $shop->{$key} = $value; + } + $shop->save(); + } - // Trashed shop + // Special trashed shop $shop = new Shop(); $shop->shopify_domain = 'trashed-shop.myshopify.com'; $shop->save(); @@ -101,92 +112,103 @@ protected function createShops() public function createCharges() { - // Test = true, status = accepted, trial = 7, active trial = no - $charge = new Charge(); - $charge->charge_id = 98298298; - $charge->test = true; - $charge->name = 'Test Plan'; - $charge->status = 'accepted'; - $charge->type = 1; - $charge->price = 15.00; - $charge->trial_days = 7; - $charge->trial_ends_on = Carbon::createFromDate(2018, 6, 3, 'UTC')->addWeeks(1)->format('Y-m-d'); - $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; - $charge->save(); - - // Test = false, status = active, trial = 7, active trial = yes - $charge = new Charge(); - $charge->charge_id = 67298298; - $charge->test = false; - $charge->name = 'Base Plan'; - $charge->status = 'active'; - $charge->type = 1; - $charge->price = 25.00; - $charge->trial_days = 7; - $charge->trial_ends_on = Carbon::today()->addDays(2)->format('Y-m-d'); - $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; - $charge->save(); - - // Test = false, status = active, trial = 7, active trial = no - $charge = new Charge(); - $charge->charge_id = 78378873; - $charge->test = false; - $charge->name = 'Base Plan Old'; - $charge->status = 'active'; - $charge->type = 1; - $charge->price = 25.00; - $charge->trial_days = 7; - $charge->trial_ends_on = Carbon::today()->subWeeks(4)->format('Y-m-d'); - $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; - $charge->save(); - - // Test = false, status = active, trial = 0 - $charge = new Charge(); - $charge->charge_id = 89389389; - $charge->test = false; - $charge->name = 'Base Plan Old Non-Trial'; - $charge->status = 'active'; - $charge->type = 1; - $charge->price = 25.00; - $charge->trial_days = 0; - $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; - $charge->save(); - - // Test = false, status = declined, trial = 7, active trial = true - $charge = new Charge(); - $charge->charge_id = 78378378378; - $charge->test = false; - $charge->name = 'Base Plan Declined'; - $charge->status = 'declined'; - $charge->type = 1; - $charge->price = 25.00; - $charge->shop_id = Shop::where('shopify_domain', 'no-token-shop.myshopify.com')->first()->id; - $charge->save(); - - // Test = false, status = cancelled - $charge = new Charge(); - $charge->charge_id = 783873873; - $charge->test = false; - $charge->name = 'Base Plan Cancelled'; - $charge->status = 'active'; - $charge->type = 1; - $charge->price = 25.00; - $charge->shop_id = Shop::where('shopify_domain', 'example.myshopify.com')->first()->id; - $charge->cancelled_on = Carbon::today()->format('Y-m-d'); - $charge->save(); - - // Test = false, status = cancelled, trial = 7 - $charge = new Charge(); - $charge->charge_id = 928736721; - $charge->test = false; - $charge->name = 'Base Plan Cancelled'; - $charge->status = 'cancelled'; - $charge->type = 1; - $charge->price = 25.00; - $charge->trial_days = 7; - $charge->trial_ends_on = Carbon::today()->addWeeks(1)->format('Y-m-d'); - $charge->cancelled_on = Carbon::today()->addDays(2)->format('Y-m-d'); - $charge->shop_id = Shop::withTrashed()->where('shopify_domain', 'trashed-shop.myshopify.com')->first()->id; - $charge->save(); + $charges = [ + // Test = true, status = accepted, trial = 7, active trial = no + [ + 'charge_id' => 98298298, + 'test' => true, + 'name' => 'Test Plan', + 'status' => 'accepted', + 'type' => 1, + 'price' => 15.00, + 'trial_days' => 7, + 'trial_ends_on' => Carbon::createFromDate(2018, 6, 3, 'UTC')->addWeeks(1)->format('Y-m-d'), + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + ], + + // Test = false, status = active, trial = 7, active trial = yes + [ + 'charge_id' => 67298298, + 'test' => false, + 'name' => 'Base Plan', + 'status' => 'active', + 'type' => 1, + 'price' => 25.00, + 'trial_days' => 7, + 'trial_ends_on' => Carbon::today()->addDays(2)->format('Y-m-d'), + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + ], + + // Test = false, status = active, trial = 7, active trial = no + [ + 'charge_id' => 78378873, + 'test' => false, + 'name' => 'Base Plan Old', + 'status' => 'active', + 'type' => 1, + 'price' => 25.00, + 'trial_days' => 7, + 'trial_ends_on' => Carbon::today()->subWeeks(4)->format('Y-m-d'), + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + ], + + // Test = false, status = active, trial = 0 + [ + 'charge_id' => 89389389, + 'test' => false, + 'name' => 'Base Plan Old Non-Trial', + 'status' => 'active', + 'type' => 1, + 'price' => 25.00, + 'trial_days' => 0, + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + ], + + // Test = false, status = declined, trial = 7, active trial = true + [ + 'charge_id' => 78378378378, + 'test' => false, + 'name' => 'Base Plan Declined', + 'status' => 'declined', + 'type' => 1, + 'price' => 25.00, + 'shop_id' => Shop::where('shopify_domain', 'no-token-shop.myshopify.com')->first()->id, + ], + + // Test = false, status = cancelled + [ + 'charge_id' => 783873873, + 'test' => false, + 'name' => 'Base Plan Cancelled', + 'status' => 'active', + 'type' => 1, + 'price' => 25.00, + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + 'cancelled_on' => Carbon::today()->format('Y-m-d'), + ], + + // Test = false, status = cancelled, trial = 7 + [ + 'charge_id' => 928736721, + 'test' => false, + 'name' => 'Base Plan Cancelled', + 'status' => 'cancelled', + 'type' => 1, + 'price' => 25.00, + 'trial_days' => 7, + 'trial_ends_on' => Carbon::today()->addWeeks(1)->format('Y-m-d'), + 'cancelled_on' => Carbon::today()->addDays(2)->format('Y-m-d'), + 'shop_id' => Shop::withTrashed()->where('shopify_domain', 'trashed-shop.myshopify.com')->first()->id, + ], + ]; + + // Build the charges + foreach ($charges as $chargeData) { + $charge = new Charge(); + foreach ($chargeData as $key => $value) { + $charge->{$key} = $value; + } + $charge->save(); + } } } From 62600002f5ca09c2eeaad18b90c9dc836f0e3715 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Fri, 20 Jul 2018 01:45:02 +0000 Subject: [PATCH 18/18] Apply fixes from StyleCI --- .../Traits/BillingControllerTrait.php | 2 +- .../resources/jobs/AppUninstalledJob.php | 4 +- tests/Controllers/BillingControllerTest.php | 4 +- tests/Models/ChargeModelTest.php | 2 +- tests/TestCase.php | 114 +++++++++--------- 5 files changed, 64 insertions(+), 62 deletions(-) diff --git a/src/ShopifyApp/Traits/BillingControllerTrait.php b/src/ShopifyApp/Traits/BillingControllerTrait.php index bf13dd59..0be7ac17 100644 --- a/src/ShopifyApp/Traits/BillingControllerTrait.php +++ b/src/ShopifyApp/Traits/BillingControllerTrait.php @@ -5,8 +5,8 @@ use Carbon\Carbon; use OhMyBrew\ShopifyApp\Facades\ShopifyApp; use OhMyBrew\ShopifyApp\Libraries\BillingPlan; -use OhMyBrew\ShopifyApp\Models\Shop; use OhMyBrew\ShopifyApp\Models\Charge; +use OhMyBrew\ShopifyApp\Models\Shop; trait BillingControllerTrait { diff --git a/src/ShopifyApp/resources/jobs/AppUninstalledJob.php b/src/ShopifyApp/resources/jobs/AppUninstalledJob.php index 8b81dc48..5d608ce5 100644 --- a/src/ShopifyApp/resources/jobs/AppUninstalledJob.php +++ b/src/ShopifyApp/resources/jobs/AppUninstalledJob.php @@ -2,4 +2,6 @@ namespace App\Jobs; -class AppUninstalledJob extends \OhMyBrew\ShopifyApp\Jobs\AppUninstalledJob { } +class AppUninstalledJob extends \OhMyBrew\ShopifyApp\Jobs\AppUninstalledJob +{ +} diff --git a/tests/Controllers/BillingControllerTest.php b/tests/Controllers/BillingControllerTest.php index 3d6764df..7d3b8c79 100644 --- a/tests/Controllers/BillingControllerTest.php +++ b/tests/Controllers/BillingControllerTest.php @@ -4,8 +4,8 @@ use Carbon\Carbon; use OhMyBrew\ShopifyApp\Controllers\BillingController; -use OhMyBrew\ShopifyApp\Models\Shop; use OhMyBrew\ShopifyApp\Models\Charge; +use OhMyBrew\ShopifyApp\Models\Shop; use OhMyBrew\ShopifyApp\Test\Stubs\ApiStub; use OhMyBrew\ShopifyApp\Test\TestCase; use ReflectionMethod; @@ -122,7 +122,7 @@ public function testReturnsBasePlanDetailsChangedByCancelledCharge() $controller = new BillingController(); $method = new ReflectionMethod(BillingController::class, 'planDetails'); $method->setAccessible(true); - + // Based on default config $this->assertEquals( [ diff --git a/tests/Models/ChargeModelTest.php b/tests/Models/ChargeModelTest.php index 6a7131e0..765547a4 100644 --- a/tests/Models/ChargeModelTest.php +++ b/tests/Models/ChargeModelTest.php @@ -85,7 +85,7 @@ public function testCancelled() { $this->assertFalse(Charge::find(1)->isCancelled()); $this->assertFalse(Charge::find(4)->isCancelled()); - $this->assertTrue(Charge::find(6)->isCancelled());; + $this->assertTrue(Charge::find(6)->isCancelled()); } public function testRemainingTrialDaysFromCancel() diff --git a/tests/TestCase.php b/tests/TestCase.php index b3329ce6..8a622508 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -72,20 +72,20 @@ protected function createShops() // Paid shop, not grandfathered [ 'shopify_domain' => 'example.myshopify.com', - 'shopify_token' => '1234', + 'shopify_token' => '1234', ], // Non-paid shop, grandfathered [ 'shopify_domain' => 'grandfathered.myshopify.com', - 'shopify_token' => '1234', - 'grandfathered' => true, + 'shopify_token' => '1234', + 'grandfathered' => true, ], // New shop... non-paid, not grandfathered [ 'shopify_domain' => 'new-shop.myshopify.com', - 'shopify_token' => '1234', + 'shopify_token' => '1234', ], // New shop... no token, not grandfathered @@ -115,90 +115,90 @@ public function createCharges() $charges = [ // Test = true, status = accepted, trial = 7, active trial = no [ - 'charge_id' => 98298298, - 'test' => true, - 'name' => 'Test Plan', - 'status' => 'accepted', - 'type' => 1, - 'price' => 15.00, - 'trial_days' => 7, + 'charge_id' => 98298298, + 'test' => true, + 'name' => 'Test Plan', + 'status' => 'accepted', + 'type' => 1, + 'price' => 15.00, + 'trial_days' => 7, 'trial_ends_on' => Carbon::createFromDate(2018, 6, 3, 'UTC')->addWeeks(1)->format('Y-m-d'), - 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, ], // Test = false, status = active, trial = 7, active trial = yes [ - 'charge_id' => 67298298, - 'test' => false, - 'name' => 'Base Plan', - 'status' => 'active', - 'type' => 1, - 'price' => 25.00, - 'trial_days' => 7, + 'charge_id' => 67298298, + 'test' => false, + 'name' => 'Base Plan', + 'status' => 'active', + 'type' => 1, + 'price' => 25.00, + 'trial_days' => 7, 'trial_ends_on' => Carbon::today()->addDays(2)->format('Y-m-d'), - 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, ], // Test = false, status = active, trial = 7, active trial = no [ - 'charge_id' => 78378873, - 'test' => false, - 'name' => 'Base Plan Old', - 'status' => 'active', - 'type' => 1, - 'price' => 25.00, - 'trial_days' => 7, + 'charge_id' => 78378873, + 'test' => false, + 'name' => 'Base Plan Old', + 'status' => 'active', + 'type' => 1, + 'price' => 25.00, + 'trial_days' => 7, 'trial_ends_on' => Carbon::today()->subWeeks(4)->format('Y-m-d'), - 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, ], // Test = false, status = active, trial = 0 [ - 'charge_id' => 89389389, - 'test' => false, - 'name' => 'Base Plan Old Non-Trial', - 'status' => 'active', - 'type' => 1, - 'price' => 25.00, + 'charge_id' => 89389389, + 'test' => false, + 'name' => 'Base Plan Old Non-Trial', + 'status' => 'active', + 'type' => 1, + 'price' => 25.00, 'trial_days' => 0, - 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, ], // Test = false, status = declined, trial = 7, active trial = true [ 'charge_id' => 78378378378, - 'test' => false, - 'name' => 'Base Plan Declined', - 'status' => 'declined', - 'type' => 1, - 'price' => 25.00, - 'shop_id' => Shop::where('shopify_domain', 'no-token-shop.myshopify.com')->first()->id, + 'test' => false, + 'name' => 'Base Plan Declined', + 'status' => 'declined', + 'type' => 1, + 'price' => 25.00, + 'shop_id' => Shop::where('shopify_domain', 'no-token-shop.myshopify.com')->first()->id, ], // Test = false, status = cancelled [ - 'charge_id' => 783873873, - 'test' => false, - 'name' => 'Base Plan Cancelled', - 'status' => 'active', - 'type' => 1, - 'price' => 25.00, - 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, + 'charge_id' => 783873873, + 'test' => false, + 'name' => 'Base Plan Cancelled', + 'status' => 'active', + 'type' => 1, + 'price' => 25.00, + 'shop_id' => Shop::where('shopify_domain', 'example.myshopify.com')->first()->id, 'cancelled_on' => Carbon::today()->format('Y-m-d'), ], // Test = false, status = cancelled, trial = 7 [ - 'charge_id' => 928736721, - 'test' => false, - 'name' => 'Base Plan Cancelled', - 'status' => 'cancelled', - 'type' => 1, - 'price' => 25.00, - 'trial_days' => 7, + 'charge_id' => 928736721, + 'test' => false, + 'name' => 'Base Plan Cancelled', + 'status' => 'cancelled', + 'type' => 1, + 'price' => 25.00, + 'trial_days' => 7, 'trial_ends_on' => Carbon::today()->addWeeks(1)->format('Y-m-d'), - 'cancelled_on' => Carbon::today()->addDays(2)->format('Y-m-d'), - 'shop_id' => Shop::withTrashed()->where('shopify_domain', 'trashed-shop.myshopify.com')->first()->id, + 'cancelled_on' => Carbon::today()->addDays(2)->format('Y-m-d'), + 'shop_id' => Shop::withTrashed()->where('shopify_domain', 'trashed-shop.myshopify.com')->first()->id, ], ];