diff --git a/src/app/Console/Commands/WalletCharge.php b/src/app/Console/Commands/WalletCharge.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/WalletCharge.php @@ -0,0 +1,58 @@ +expectedCharges(); + + if ($charge > 0) { + $this->info( + "charging wallet {$wallet->id} for user {$wallet->owner->email} with {$charge}" + ); + + $wallet->chargeEntitlements(); + } + } + } +} diff --git a/src/app/Console/Commands/WalletExpected.php b/src/app/Console/Commands/WalletExpected.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/WalletExpected.php @@ -0,0 +1,55 @@ +expectedCharges(); + + if ($expected > 0) { + $this->info("expect charging wallet {$wallet->id} for user {$wallet->owner->email} with {$expected}"); + } + } + } +} diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -68,6 +68,7 @@ 'owner_id' => $user->id, 'wallet_id' => $wallet_id, 'sku_id' => $sku->id, + 'cost' => $sku->pivot->cost(), 'entitleable_id' => $this->id, 'entitleable_type' => Domain::class ] diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php --- a/src/app/Entitlement.php +++ b/src/app/Entitlement.php @@ -2,7 +2,9 @@ namespace App; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; /** * The eloquent definition of an Entitlement. @@ -16,6 +18,8 @@ */ class Entitlement extends Model { + use SoftDeletes; + /** * This table does not use auto-increment. * @@ -41,6 +45,7 @@ 'wallet_id', 'entitleable_id', 'entitleable_type', + 'cost', 'description' ]; diff --git a/src/app/Handlers/Domain.php b/src/app/Handlers/Domain.php --- a/src/app/Handlers/Domain.php +++ b/src/app/Handlers/Domain.php @@ -2,8 +2,6 @@ namespace App\Handlers; -use App\Sku; - class Domain extends \App\Handlers\Base { public static function entitleableClass() diff --git a/src/app/Handlers/DomainHosting.php b/src/app/Handlers/DomainHosting.php --- a/src/app/Handlers/DomainHosting.php +++ b/src/app/Handlers/DomainHosting.php @@ -2,8 +2,6 @@ namespace App\Handlers; -use App\Sku; - class DomainHosting extends \App\Handlers\Base { public static function entitleableClass() diff --git a/src/app/Handlers/DomainRegistration.php b/src/app/Handlers/DomainRegistration.php --- a/src/app/Handlers/DomainRegistration.php +++ b/src/app/Handlers/DomainRegistration.php @@ -2,8 +2,6 @@ namespace App\Handlers; -use App\Sku; - class DomainRegistration extends \App\Handlers\Base { public static function entitleableClass() diff --git a/src/app/Handlers/Groupware.php b/src/app/Handlers/Groupware.php --- a/src/app/Handlers/Groupware.php +++ b/src/app/Handlers/Groupware.php @@ -2,8 +2,6 @@ namespace App\Handlers; -use App\Sku; - class Groupware extends \App\Handlers\Base { public static function entitleableClass() diff --git a/src/app/Handlers/Mailbox.php b/src/app/Handlers/Mailbox.php --- a/src/app/Handlers/Mailbox.php +++ b/src/app/Handlers/Mailbox.php @@ -2,10 +2,6 @@ namespace App\Handlers; -use App\Entitlement; -use App\Sku; -use App\User; - class Mailbox extends \App\Handlers\Base { public static function entitleableClass() diff --git a/src/app/Handlers/Resource.php b/src/app/Handlers/Resource.php --- a/src/app/Handlers/Resource.php +++ b/src/app/Handlers/Resource.php @@ -2,8 +2,6 @@ namespace App\Handlers; -use App\Sku; - class Resource extends \App\Handlers\Base { public static function entitleableClass() diff --git a/src/app/Handlers/SharedFolder.php b/src/app/Handlers/SharedFolder.php --- a/src/app/Handlers/SharedFolder.php +++ b/src/app/Handlers/SharedFolder.php @@ -2,8 +2,6 @@ namespace App\Handlers; -use App\Sku; - class SharedFolder extends \App\Handlers\Base { public static function entitleableClass() diff --git a/src/app/Handlers/Storage.php b/src/app/Handlers/Storage.php --- a/src/app/Handlers/Storage.php +++ b/src/app/Handlers/Storage.php @@ -6,7 +6,7 @@ { public static function entitleableClass() { - return null; + return \App\User::class; } public static function preReq($entitlement, $object) diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php --- a/src/app/Observers/EntitlementObserver.php +++ b/src/app/Observers/EntitlementObserver.php @@ -3,6 +3,7 @@ namespace App\Observers; use App\Entitlement; +use Carbon\Carbon; /** * This is an observer for the Entitlement model definition. @@ -62,11 +63,5 @@ if (!$result) { return false; } - - // TODO: Handle the first free unit here? - - // TODO: Execute the Sku handler class or function? - - $wallet->debit($sku->cost); } } diff --git a/src/app/Observers/PackageSkuObserver.php b/src/app/Observers/PackageSkuObserver.php new file mode 100644 --- /dev/null +++ b/src/app/Observers/PackageSkuObserver.php @@ -0,0 +1,19 @@ +package; + $sku = $packageSku->sku; + + $package->skus()->updateExistingPivot( + $sku, + ['cost' => ($sku->cost * (100 - $package->discount_rate)) / 100], + false + ); + } +} diff --git a/src/app/Package.php b/src/app/Package.php --- a/src/app/Package.php +++ b/src/app/Package.php @@ -34,12 +34,26 @@ 'discount_rate' ]; + /** + * The costs of this package at its pre-defined, existing configuration. + * + * @return int The costs in cents. + */ public function cost() { $costs = 0; foreach ($this->skus as $sku) { - $costs += ($sku->pivot->qty - $sku->units_free) * $sku->cost; + $units = $sku->pivot->qty - $sku->units_free; + + if ($units < 0) { + \Log::debug("Package {$this->id} is misconfigured for more free units than qty."); + $units = 0; + } + + $ppu = $sku->cost * ((100 - $this->discount_rate) / 100); + + $costs += $units * $ppu; } return $costs; diff --git a/src/app/PackageSku.php b/src/app/PackageSku.php --- a/src/app/PackageSku.php +++ b/src/app/PackageSku.php @@ -12,10 +12,48 @@ protected $fillable = [ 'package_id', 'sku_id', + 'cost', 'qty' ]; protected $casts = [ + 'cost' => 'integer', 'qty' => 'integer' ]; + + /** + * Under this package, how much does this SKU cost? + * + * @return int The costs of this SKU under this package in cents. + */ + public function cost() + { + $costs = 0; + + $units = $this->qty - $this->sku->units_free; + + if ($units < 0) { + \Log::debug( + "Package {$this->package_id} is misconfigured for more free units than qty." + ); + + $units = 0; + } + + $ppu = $this->sku->cost * ((100 - $this->package->discount_rate) / 100); + + $costs += $units * $ppu; + + return $costs; + } + + public function package() + { + return $this->belongsTo('App\Package'); + } + + public function sku() + { + return $this->belongsTo('App\Sku'); + } } diff --git a/src/app/Plan.php b/src/app/Plan.php --- a/src/app/Plan.php +++ b/src/app/Plan.php @@ -49,7 +49,11 @@ 'description', ]; - + /** + * The list price for this package at the minimum configuration. + * + * @return int The costs in cents. + */ public function cost() { $costs = 0; diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -28,6 +28,7 @@ \App\Domain::observe(\App\Observers\DomainObserver::class); \App\Entitlement::observe(\App\Observers\EntitlementObserver::class); \App\Package::observe(\App\Observers\PackageObserver::class); + \App\PackageSku::observe(\App\Observers\PackageSkuObserver::class); \App\Plan::observe(\App\Observers\PlanObserver::class); \App\SignupCode::observe(\App\Observers\SignupCodeObserver::class); \App\Sku::observe(\App\Observers\SkuObserver::class); diff --git a/src/app/Sku.php b/src/app/Sku.php --- a/src/app/Sku.php +++ b/src/app/Sku.php @@ -21,6 +21,7 @@ 'description', 'cost', 'units_free', + // persist for annual domain registration 'period', 'handler_class', 'active' @@ -41,6 +42,6 @@ return $this->belongsToMany( 'App\Package', 'package_skus' - )->using('App\PackageSku')->withPivot(['qty']); + )->using('App\PackageSku')->withPivot(['cost', 'qty']); } } diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -134,6 +134,7 @@ 'owner_id' => $this->id, 'wallet_id' => $wallet_id, 'sku_id' => $sku->id, + 'cost' => $sku->pivot->cost(), 'entitleable_id' => $user->id, 'entitleable_type' => User::class ] diff --git a/src/app/Wallet.php b/src/app/Wallet.php --- a/src/app/Wallet.php +++ b/src/app/Wallet.php @@ -3,6 +3,7 @@ namespace App; use App\User; +use Carbon\Carbon; use Iatstuti\Database\Support\NullableFields; use Illuminate\Database\Eloquent\Model; @@ -55,6 +56,54 @@ } } + public function chargeEntitlements($apply = true) + { + $charges = 0; + + foreach ($this->entitlements()->get()->fresh() as $entitlement) { + // This entitlement has been created less than or equal to 14 days ago (this is at + // maximum the fourteenth 24-hour period). + if ($entitlement->created_at > Carbon::now()->subDays(14)) { + continue; + } + + // This entitlement was created, or billed last, less than a month ago. + if ($entitlement->updated_at > Carbon::now()->subMonths(1)) { + continue; + } + + // created more than a month ago -- was it billed? + if ($entitlement->updated_at <= Carbon::now()->subMonths(1)) { + $diff = $entitlement->updated_at->diffInMonths(Carbon::now()); + + $charges += $entitlement->cost * $diff; + + // if we're in dry-run, you know... + if (!$apply) { + continue; + } + + $entitlement->updated_at = $entitlement->updated_at->copy()->addMonths($diff); + $entitlement->save(); + + $this->debit($entitlement->cost * $diff); + } + } + + return $charges; + } + + + /** + * Calculate the expected charges to this wallet. + * + * @return int + */ + public function expectedCharges() + { + return $this->chargeEntitlements(false); + } + /** * Remove a controller from this wallet. * diff --git a/src/database/migrations/2019_09_17_102628_create_sku_entitlements.php b/src/database/migrations/2019_09_17_102628_create_sku_entitlements.php --- a/src/database/migrations/2019_09_17_102628_create_sku_entitlements.php +++ b/src/database/migrations/2019_09_17_102628_create_sku_entitlements.php @@ -36,6 +36,7 @@ $table->bigInteger('owner_id'); $table->bigInteger('entitleable_id'); $table->string('entitleable_type'); + $table->integer('cost')->default(0)->nullable(); $table->string('wallet_id', 36); $table->string('sku_id', 36); $table->string('description')->nullable(); diff --git a/src/database/migrations/2019_12_10_100355_create_package_skus_table.php b/src/database/migrations/2019_12_10_100355_create_package_skus_table.php --- a/src/database/migrations/2019_12_10_100355_create_package_skus_table.php +++ b/src/database/migrations/2019_12_10_100355_create_package_skus_table.php @@ -4,6 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; +// phpcs:ignore class CreatePackageSkusTable extends Migration { /** @@ -21,6 +22,8 @@ $table->string('sku_id', 36); $table->integer('qty')->default(1); + $table->integer('cost')->default(0)->nullable(); + $table->foreign('package_id')->references('id')->on('packages') ->onDelete('cascade')->onUpdate('cascade'); diff --git a/src/database/migrations/2020_02_11_110959_add_deleted_at.php b/src/database/migrations/2020_02_11_110959_users_add_deleted_at.php rename from src/database/migrations/2020_02_11_110959_add_deleted_at.php rename to src/database/migrations/2020_02_11_110959_users_add_deleted_at.php --- a/src/database/migrations/2020_02_11_110959_add_deleted_at.php +++ b/src/database/migrations/2020_02_11_110959_users_add_deleted_at.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -class AddDeletedAt extends Migration +// phpcs:ignore +class UsersAddDeletedAt extends Migration { /** * Run the migrations. @@ -14,13 +15,6 @@ public function up() { Schema::table( - 'domains', - function (Blueprint $table) { - $table->softDeletes(); - } - ); - - Schema::table( 'users', function (Blueprint $table) { $table->softDeletes(); @@ -36,12 +30,6 @@ public function down() { Schema::table( - 'domains', - function (Blueprint $table) { - $table->dropColumn(['deleted_at']); - } - ); - Schema::table( 'users', function (Blueprint $table) { $table->dropColumn(['deleted_at']); diff --git a/src/database/migrations/2020_02_11_110959_add_deleted_at.php b/src/database/migrations/2020_02_11_110960_domains_add_deleted_at.php copy from src/database/migrations/2020_02_11_110959_add_deleted_at.php copy to src/database/migrations/2020_02_11_110960_domains_add_deleted_at.php --- a/src/database/migrations/2020_02_11_110959_add_deleted_at.php +++ b/src/database/migrations/2020_02_11_110960_domains_add_deleted_at.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -class AddDeletedAt extends Migration +// phpcs:ignore +class DomainsAddDeletedAt extends Migration { /** * Run the migrations. @@ -19,13 +20,6 @@ $table->softDeletes(); } ); - - Schema::table( - 'users', - function (Blueprint $table) { - $table->softDeletes(); - } - ); } /** @@ -41,11 +35,5 @@ $table->dropColumn(['deleted_at']); } ); - Schema::table( - 'users', - function (Blueprint $table) { - $table->dropColumn(['deleted_at']); - } - ); } } diff --git a/src/database/migrations/2020_02_11_110959_add_deleted_at.php b/src/database/migrations/2020_02_26_085835_entitlements_add_deleted_at.php rename from src/database/migrations/2020_02_11_110959_add_deleted_at.php rename to src/database/migrations/2020_02_26_085835_entitlements_add_deleted_at.php --- a/src/database/migrations/2020_02_11_110959_add_deleted_at.php +++ b/src/database/migrations/2020_02_26_085835_entitlements_add_deleted_at.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -class AddDeletedAt extends Migration +// phpcs:ignore +class EntitlementsAddDeletedAt extends Migration { /** * Run the migrations. @@ -14,14 +15,7 @@ public function up() { Schema::table( - 'domains', - function (Blueprint $table) { - $table->softDeletes(); - } - ); - - Schema::table( - 'users', + 'entitlements', function (Blueprint $table) { $table->softDeletes(); } @@ -36,13 +30,7 @@ public function down() { Schema::table( - 'domains', - function (Blueprint $table) { - $table->dropColumn(['deleted_at']); - } - ); - Schema::table( - 'users', + 'entitlements', function (Blueprint $table) { $table->dropColumn(['deleted_at']); } diff --git a/src/database/seeds/PackageSeeder.php b/src/database/seeds/PackageSeeder.php --- a/src/database/seeds/PackageSeeder.php +++ b/src/database/seeds/PackageSeeder.php @@ -14,6 +14,10 @@ */ public function run() { + $skuGroupware = Sku::firstOrCreate(['title' => 'groupware']); + $skuMailbox = Sku::firstOrCreate(['title' => 'mailbox']); + $skuStorage = Sku::firstOrCreate(['title' => 'storage']); + $package = Package::create( [ 'title' => 'kolab', @@ -23,9 +27,9 @@ ); $skus = [ - Sku::firstOrCreate(['title' => 'mailbox']), - Sku::firstOrCreate(['title' => 'storage']), - Sku::firstOrCreate(['title' => 'groupware']) + $skuMailbox, + $skuGroupware, + $skuStorage ]; $package->skus()->saveMany($skus); @@ -33,7 +37,7 @@ // This package contains 2 units of the storage SKU, which just so happens to also // be the number of SKU free units. $package->skus()->updateExistingPivot( - Sku::firstOrCreate(['title' => 'storage']), + $skuStorage, ['qty' => 2], false ); @@ -47,8 +51,8 @@ ); $skus = [ - Sku::firstOrCreate(['title' => 'mailbox']), - Sku::firstOrCreate(['title' => 'storage']) + $skuMailbox, + $skuStorage ]; $package->skus()->saveMany($skus); diff --git a/src/database/seeds/UserSeeder.php b/src/database/seeds/UserSeeder.php --- a/src/database/seeds/UserSeeder.php +++ b/src/database/seeds/UserSeeder.php @@ -1,10 +1,11 @@ 'kolab.org', - 'status' => Domain::STATUS_NEW + Domain::STATUS_ACTIVE + Domain::STATUS_CONFIRMED + Domain::STATUS_VERIFIED, + 'status' => Domain::STATUS_NEW + + Domain::STATUS_ACTIVE + + Domain::STATUS_CONFIRMED + + Domain::STATUS_VERIFIED, 'type' => Domain::TYPE_EXTERNAL ] ); @@ -47,7 +51,7 @@ $john->setAliases(['john.doe@kolab.org']); - $user_wallets = $john->wallets()->get(); + $wallet = $john->wallets->first(); $package_domain = \App\Package::where('title', 'domain-hosting')->first(); $package_kolab = \App\Package::where('title', 'kolab')->first(); @@ -77,6 +81,12 @@ $john->assignPackage($package_kolab, $jack); + foreach ($john->entitlements as $entitlement) { + $entitlement->created_at = Carbon::now()->subMonths(1); + $entitlement->updated_at = Carbon::now()->subMonths(1); + $entitlement->save(); + } + factory(User::class, 10)->create(); } } diff --git a/src/phpstan.neon b/src/phpstan.neon --- a/src/phpstan.neon +++ b/src/phpstan.neon @@ -4,3 +4,4 @@ level: 3 paths: - app/ + - tests/ diff --git a/src/tests/Feature/BillingTest.php b/src/tests/Feature/BillingTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/BillingTest.php @@ -0,0 +1,246 @@ +deleteTestUser('jane@kolabnow.com'); + $this->deleteTestUser('jack@kolabnow.com'); + + \App\Package::where('title', 'kolab-kube')->delete(); + + $this->user = $this->getTestUser('jane@kolabnow.com'); + $this->package = \App\Package::where('title', 'kolab')->first(); + $this->user->assignPackage($this->package); + + $this->wallet = $this->user->wallets->first(); + + $this->wallet_id = $this->wallet->id; + } + + public function tearDown(): void + { + $this->deleteTestUser('jane@kolabnow.com'); + $this->deleteTestUser('jack@kolabnow.com'); + + \App\Package::where('title', 'kolab-kube')->delete(); + + parent::tearDown(); + } + + /** + * Test the expected results for a user that registers and is almost immediately gone. + */ + public function testTouchAndGo(): void + { + $this->assertCount(4, $this->wallet->entitlements); + + $this->assertEquals(0, $this->wallet->expectedCharges()); + + $this->user->delete(); + + $this->assertCount(0, $this->wallet->fresh()->entitlements->where('deleted_at', null)); + + $this->assertCount(4, $this->wallet->entitlements); + } + + /** + * Verify the last day before the end of a full month's trial. + */ + public function testNearFullTrial(): void + { + $this->backdateEntitlements( + $this->wallet->entitlements, + Carbon::now()->subMonths(1)->addDays(1) + ); + + $this->assertEquals(0, $this->wallet->expectedCharges()); + } + + /** + * Verify the exact end of the month's trial. + */ + public function testFullTrial(): void + { + $this->backdateEntitlements($this->wallet->entitlements, Carbon::now()->subMonths(1)); + + $this->assertEquals(999, $this->wallet->expectedCharges()); + } + + /** + * Verify that over-running the trial by a single day causes charges to be incurred. + */ + public function testOutRunTrial(): void + { + $this->backdateEntitlements( + $this->wallet->entitlements, + Carbon::now()->subMonths(1)->subDays(1) + ); + + $this->assertEquals(999, $this->wallet->expectedCharges()); + } + + /** + * Verify additional storage configuration entitlement created 'early' does incur additional + * charges to the wallet. + */ + public function testAddtStorageEarly(): void + { + $this->backdateEntitlements( + $this->wallet->entitlements, + Carbon::now()->subMonths(1)->subDays(1) + ); + + $this->assertEquals(999, $this->wallet->expectedCharges()); + + $sku = \App\Sku::where(['title' => 'storage'])->first(); + + $entitlement = \App\Entitlement::create( + [ + 'owner_id' => $this->user->id, + 'wallet_id' => $this->wallet_id, + 'sku_id' => $sku->id, + 'cost' => $sku->cost, + 'entitleable_id' => $this->user->id, + 'entitleable_type' => \App\User::class + ] + ); + + $this->backdateEntitlements( + [$entitlement], + Carbon::now()->subMonths(1)->subDays(1) + ); + + $this->assertEquals(1024, $this->wallet->expectedCharges()); + } + + /** + * Verify additional storage configuration entitlement created 'late' does not incur additional + * charges to the wallet. + */ + public function testAddtStorageLate(): void + { + $this->backdateEntitlements($this->wallet->entitlements, Carbon::now()->subMonths(1)); + + $this->assertEquals(999, $this->wallet->expectedCharges()); + + $sku = \App\Sku::where(['title' => 'storage'])->first(); + + $entitlement = \App\Entitlement::create( + [ + 'owner_id' => $this->user->id, + 'wallet_id' => $this->wallet_id, + 'sku_id' => $sku->id, + 'cost' => $sku->cost, + 'entitleable_id' => $this->user->id, + 'entitleable_type' => \App\User::class + ] + ); + + $this->backdateEntitlements([$entitlement], Carbon::now()->subDays(14)); + + $this->assertEquals(999, $this->wallet->expectedCharges()); + } + + public function testFifthWeek(): void + { + $targetDateA = Carbon::now()->subWeeks(5); + $targetDateB = $targetDateA->copy()->addMonths(1); + + $this->backdateEntitlements($this->wallet->entitlements, $targetDateA); + + $this->assertEquals(999, $this->wallet->expectedCharges()); + + $this->wallet->chargeEntitlements(); + + $this->assertEquals(-999, $this->wallet->balance); + + foreach ($this->wallet->entitlements()->get() as $entitlement) { + $this->assertTrue($entitlement->created_at->isSameSecond($targetDateA)); + $this->assertTrue($entitlement->updated_at->isSameSecond($targetDateB)); + } + } + + public function testSecondMonth(): void + { + $this->backdateEntitlements($this->wallet->entitlements, Carbon::now()->subMonths(2)); + + $this->assertCount(4, $this->wallet->entitlements); + + $this->assertEquals(1998, $this->wallet->expectedCharges()); + + $sku = \App\Sku::where(['title' => 'storage'])->first(); + + $entitlement = \App\Entitlement::create( + [ + 'owner_id' => $this->user->id, + 'entitleable_id' => $this->user->id, + 'entitleable_type' => \App\User::class, + 'cost' => $sku->cost, + 'sku_id' => $sku->id, + 'wallet_id' => $this->wallet_id + ] + ); + + $this->backdateEntitlements([$entitlement], Carbon::now()->subMonths(1)); + + $this->assertEquals(2023, $this->wallet->expectedCharges()); + } + + public function testWithDiscount(): void + { + $package = \App\Package::create( + [ + 'title' => 'kolab-kube', + 'description' => 'Kolab for Kube fans', + 'discount_rate' => 50 + ] + ); + + $skus = [ + \App\Sku::firstOrCreate(['title' => 'mailbox']), + \App\Sku::firstOrCreate(['title' => 'storage']), + \App\Sku::firstOrCreate(['title' => 'groupware']) + ]; + + $package->skus()->saveMany($skus); + + $package->skus()->updateExistingPivot( + \App\Sku::firstOrCreate(['title' => 'storage']), + ['qty' => 2], + false + ); + + $user = $this->getTestUser('jack@kolabnow.com'); + + $user->assignPackage($package); + + $wallet = $user->wallets->first(); + + $wallet_id = $wallet->id; + + $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subMonths(1)); + + $this->assertEquals(500, $wallet->expectedCharges()); + } +} diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php --- a/src/tests/Feature/Controller/SignupTest.php +++ b/src/tests/Feature/Controller/SignupTest.php @@ -151,10 +151,10 @@ */ public function testSignupInitValidInput() { - Queue::fake(); + $queue = Queue::fake(); // Assert that no jobs were pushed... - Queue::assertNothingPushed(); + $queue->assertNothingPushed(); $data = [ 'email' => 'testuser@external.com', @@ -171,10 +171,10 @@ $this->assertNotEmpty($json['code']); // Assert the email sending job was pushed once - Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1); + $queue->assertPushed(\App\Jobs\SignupVerificationEmail::class, 1); // Assert the job has proper data assigned - Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) { + $queue->assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) { $code = TestCase::getObjectProperty($job, 'code'); return $code->code === $json['code'] @@ -390,7 +390,7 @@ */ public function testSignupValidInput(array $result) { - Queue::fake(); + $queue = Queue::fake(); $domain = $this->getPublicDomain(); $identity = \strtolower('SignupLogin@') . $domain; @@ -414,8 +414,8 @@ $this->assertTrue(!empty($json['expires_in']) && is_int($json['expires_in']) && $json['expires_in'] > 0); $this->assertNotEmpty($json['access_token']); - Queue::assertPushed(\App\Jobs\UserCreate::class, 1); - Queue::assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($data) { + $queue->assertPushed(\App\Jobs\UserCreate::class, 1); + $queue->assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($data) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->email === \strtolower($data['login'] . '@' . $data['domain']); @@ -446,7 +446,7 @@ */ public function testSignupGroupAccount() { - Queue::fake(); + $queue = Queue::fake(); // Initial signup request $user_data = $data = [ @@ -464,10 +464,10 @@ $this->assertNotEmpty($json['code']); // Assert the email sending job was pushed once - Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1); + $queue->assertPushed(\App\Jobs\SignupVerificationEmail::class, 1); // Assert the job has proper data assigned - Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) { + $queue->assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) { $code = TestCase::getObjectProperty($job, 'code'); return $code->code === $json['code'] @@ -516,15 +516,15 @@ $this->assertTrue(!empty($result['expires_in']) && is_int($result['expires_in']) && $result['expires_in'] > 0); $this->assertNotEmpty($result['access_token']); - Queue::assertPushed(\App\Jobs\DomainCreate::class, 1); - Queue::assertPushed(\App\Jobs\DomainCreate::class, function ($job) use ($domain) { + $queue->assertPushed(\App\Jobs\DomainCreate::class, 1); + $queue->assertPushed(\App\Jobs\DomainCreate::class, function ($job) use ($domain) { $job_domain = TestCase::getObjectProperty($job, 'domain'); return $job_domain->namespace === $domain; }); - Queue::assertPushed(\App\Jobs\UserCreate::class, 1); - Queue::assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($data) { + $queue->assertPushed(\App\Jobs\UserCreate::class, 1); + $queue->assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($data) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->email === $data['login'] . '@' . $data['domain']; diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php --- a/src/tests/Feature/DomainTest.php +++ b/src/tests/Feature/DomainTest.php @@ -47,8 +47,8 @@ public function testCreateJobs(): void { // Fake the queue, assert that no jobs were pushed... - Queue::fake(); - Queue::assertNothingPushed(); + $queue = Queue::fake(); + $queue->assertNothingPushed(); $domain = Domain::create([ 'namespace' => 'gmail.com', @@ -56,9 +56,9 @@ 'type' => Domain::TYPE_EXTERNAL, ]); - Queue::assertPushed(\App\Jobs\DomainCreate::class, 1); + $queue->assertPushed(\App\Jobs\DomainCreate::class, 1); - Queue::assertPushed( + $queue->assertPushed( \App\Jobs\DomainCreate::class, function ($job) use ($domain) { $job_domain = TestCase::getObjectProperty($job, 'domain'); @@ -81,7 +81,7 @@ $this->assertNotContains('public-active.com', $public_domains); - Queue::fake(); + $queue = Queue::fake(); $domain = Domain::create([ 'namespace' => 'public-active.com', @@ -134,7 +134,7 @@ ci-failure-none MX 10 mx01.kolabnow.com. */ - Queue::fake(); + $queue = Queue::fake(); $domain_props = ['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL]; diff --git a/src/tests/Feature/EntitlementTest.php b/src/tests/Feature/EntitlementTest.php --- a/src/tests/Feature/EntitlementTest.php +++ b/src/tests/Feature/EntitlementTest.php @@ -4,8 +4,10 @@ use App\Domain; use App\Entitlement; +use App\Package; use App\Sku; use App\User; +use Carbon\Carbon; use Tests\TestCase; class EntitlementTest extends TestCase @@ -32,10 +34,15 @@ */ public function testUserAddEntitlement(): void { - $sku_domain = Sku::firstOrCreate(['title' => 'domain']); - $sku_mailbox = Sku::firstOrCreate(['title' => 'mailbox']); + $package_domain = Package::where('title', 'domain-hosting')->first(); + $package_kolab = Package::where('title', 'kolab')->first(); + + $sku_domain = Sku::where('title', 'domain-hosting')->first(); + $sku_mailbox = Sku::where('title', 'mailbox')->first(); + $owner = $this->getTestUser('entitlement-test@kolabnow.com'); $user = $this->getTestUser('entitled-user@custom-domain.com'); + $domain = $this->getTestDomain( 'custom-domain.com', [ @@ -44,50 +51,23 @@ ] ); - $wallet = $owner->wallets()->first(); + $domain->assignPackage($package_domain, $owner); - $entitlement_own_mailbox = new Entitlement( - [ - 'owner_id' => $owner->id, - 'entitleable_id' => $owner->id, - 'entitleable_type' => User::class, - 'wallet_id' => $wallet->id, - 'sku_id' => $sku_mailbox->id, - 'description' => "Owner Mailbox Entitlement Test" - ] - ); + $owner->assignPackage($package_kolab); + $owner->assignPackage($package_kolab, $user); - $entitlement_domain = new Entitlement( - [ - 'owner_id' => $owner->id, - 'entitleable_id' => $domain->id, - 'entitleable_type' => Domain::class, - 'wallet_id' => $wallet->id, - 'sku_id' => $sku_domain->id, - 'description' => "User Domain Entitlement Test" - ] - ); + $wallet = $owner->wallets->first(); - $entitlement_mailbox = new Entitlement( - [ - 'owner_id' => $owner->id, - 'entitleable_id' => $user->id, - 'entitleable_type' => User::class, - 'wallet_id' => $wallet->id, - 'sku_id' => $sku_mailbox->id, - 'description' => "User Mailbox Entitlement Test" - ] - ); + $this->assertCount(4, $owner->entitlements()->get()); + $this->assertCount(1, $sku_domain->entitlements()->where('owner_id', $owner->id)->get()); + $this->assertCount(2, $sku_mailbox->entitlements()->where('owner_id', $owner->id)->get()); + $this->assertCount(9, $wallet->entitlements); + + $this->backdateEntitlements($owner->entitlements, Carbon::now()->subMonths(1)); - $owner->addEntitlement($entitlement_own_mailbox); - $owner->addEntitlement($entitlement_domain); - $owner->addEntitlement($entitlement_mailbox); + $wallet->chargeEntitlements(); - $this->assertTrue($owner->entitlements()->count() == 3); - $this->assertTrue($sku_domain->entitlements()->where('owner_id', $owner->id)->count() == 1); - $this->assertTrue($sku_mailbox->entitlements()->where('owner_id', $owner->id)->count() == 2); - $this->assertTrue($wallet->entitlements()->count() == 3); - $this->assertTrue($wallet->fresh()->balance < 0.00); + $this->assertTrue($wallet->fresh()->balance < 0); } public function testAddExistingEntitlement(): void diff --git a/src/tests/Feature/Jobs/UserVerifyTest.php b/src/tests/Feature/Jobs/UserVerifyTest.php --- a/src/tests/Feature/Jobs/UserVerifyTest.php +++ b/src/tests/Feature/Jobs/UserVerifyTest.php @@ -5,11 +5,25 @@ use App\Jobs\UserCreate; use App\Jobs\UserVerify; use App\User; -use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\Queue; use Tests\TestCase; class UserVerifyTest extends TestCase { + public function setUp(): void + { + parent::setUp(); + + $this->deleteTestUser('jane@kolabnow.com'); + } + + public function tearDown(): void + { + $this->deleteTestUser('jane@kolabnow.com'); + + parent::tearDown(); + } + /** * Test job handle * @@ -17,11 +31,19 @@ */ public function testHandle(): void { - $user = $this->getTestUser('john@kolab.org'); - $user->status ^= User::STATUS_IMAP_READY; - $user->save(); + Queue::fake(); + + $user = $this->getTestUser('jane@kolabnow.com'); + + // This is a valid assertion in a feature, not functional test environment. + $this->assertFalse($user->isImapReady()); + $this->assertFalse($user->isLdapReady()); + + $job = new UserCreate($user); + $job->handle(); $this->assertFalse($user->isImapReady()); + $this->assertTrue($user->isLdapReady()); $job = new UserVerify($user); $job->handle(); diff --git a/src/tests/Feature/SignupCodeTest.php b/src/tests/Feature/SignupCodeTest.php --- a/src/tests/Feature/SignupCodeTest.php +++ b/src/tests/Feature/SignupCodeTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature; use App\SignupCode; +use Carbon\Carbon; use Tests\TestCase; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -23,7 +24,7 @@ ] ]; - $now = new \DateTime('now'); + $now = Carbon::now(); $code = SignupCode::create($data); @@ -38,11 +39,11 @@ ); $this->assertSame($data['data'], $code->data); - $this->assertInstanceOf(\DateTime::class, $code->expires_at); + $this->assertInstanceOf(Carbon::class, $code->expires_at); $this->assertSame( env('SIGNUP_CODE_EXPIRY', SignupCode::CODE_EXP_HOURS), - $code->expires_at->diff($now)->h + 1 + $code->expires_at->diffInHours($now) + 1 ); $inst = SignupCode::find($code->code); diff --git a/src/tests/Feature/SkuTest.php b/src/tests/Feature/SkuTest.php --- a/src/tests/Feature/SkuTest.php +++ b/src/tests/Feature/SkuTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature; -use App\Domain; use App\Entitlement; use App\Handlers; +use App\Package; use App\Sku; -use App\User; +use Carbon\Carbon; use Tests\TestCase; class SkuTest extends TestCase @@ -16,228 +16,79 @@ { parent::setUp(); - $this->deleteTestUser('sku-test-user@custom-domain.com'); - $this->deleteTestDomain('custom-domain.com'); + $this->deleteTestUser('jane@kolabnow.com'); } public function tearDown(): void { - $this->deleteTestUser('sku-test-user@custom-domain.com'); - $this->deleteTestDomain('custom-domain.com'); + $this->deleteTestUser('jane@kolabnow.com'); parent::tearDown(); } - /** - * Tests for Sku::registerEntitlements() - */ - public function testRegisterEntitlement(): void + public function testPackageEntitlements(): void { - // TODO: This test depends on seeded SKUs, but probably should not - $domain = $this->getTestDomain( - 'custom-domain.com', - [ - 'status' => Domain::STATUS_NEW, - 'type' => Domain::TYPE_EXTERNAL, - ] - ); - - \Log::debug(var_export($domain->toArray(), true)); + $user = $this->getTestUser('jane@kolabnow.com'); - $user = $this->getTestUser('sku-test-user@custom-domain.com'); $wallet = $user->wallets()->first(); - // \App\Handlers\Mailbox SKU - // Note, we're testing mailbox SKU before domain SKU as it may potentially fail in that - // order - $sku = Sku::where('title', 'mailbox')->first(); - Entitlement::create( - [ - 'owner_id' => $user->id, - 'wallet_id' => $wallet->id, - 'sku_id' => $sku->id, - 'entitleable_id' => $user->id, - 'entitleable_type' => User::class - ] - ); + $package = Package::where('title', 'lite')->first(); - $entitlements = $sku->entitlements()->where('owner_id', $user->id)->get(); - $wallet->refresh(); - - if ($sku->active) { - $balance = -$sku->cost; - $this->assertCount(1, $entitlements); - $this->assertEquals($user->id, $entitlements[0]->entitleable_id); - $this->assertSame( - Handlers\Mailbox::entitleableClass(), - $entitlements[0]->entitleable_type - ); - } else { - $this->assertCount(0, $entitlements); - } - - $this->assertEquals($balance, $wallet->balance); - - // \App\Handlers\Domain SKU - $sku = Sku::where('title', 'domain')->first(); - Entitlement::create( - [ - 'owner_id' => $user->id, - 'wallet_id' => $wallet->id, - 'sku_id' => $sku->id, - 'entitleable_id' => $domain->id, - 'entitleable_type' => Domain::class - ] - ); - - $entitlements = $sku->entitlements->where('owner_id', $user->id); - - foreach ($entitlements as $entitlement) { - \Log::debug(var_export($entitlement->toArray(), true)); - } - - $wallet->refresh(); - - if ($sku->active) { - $balance -= $sku->cost; - $this->assertCount(1, $entitlements); - - $_domain = Domain::find($entitlements->first()->entitleable_id); - - $this->assertEquals( - $domain->id, - $entitlements->first()->entitleable_id, - var_export($_domain->toArray(), true) - ); - - $this->assertSame( - Handlers\Domain::entitleableClass(), - $entitlements->first()->entitleable_type - ); - } else { - $this->assertCount(0, $entitlements); - } - - $this->assertEquals($balance, $wallet->balance); - - // \App\Handlers\DomainRegistration SKU - $sku = Sku::where('title', 'domain-registration')->first(); - Entitlement::create( - [ - 'owner_id' => $user->id, - 'wallet_id' => $user->wallets()->get()[0]->id, - 'sku_id' => $sku->id, - 'entitleable_id' => $domain->id, - 'entitleable_type' => Domain::class - ] - ); + $sku_mailbox = Sku::where('title', 'mailbox')->first(); + $sku_storage = Sku::where('title', 'storage')->first(); - $entitlements = $sku->entitlements->where('owner_id', $user->id); - $wallet->refresh(); + $user = $user->assignPackage($package); - if ($sku->active) { - $balance -= $sku->cost; - $this->assertCount(1, $entitlements); - $this->assertEquals($domain->id, $entitlements->first()->entitleable_id); - $this->assertSame( - Handlers\DomainRegistration::entitleableClass(), - $entitlements->first()->entitleable_type - ); - } else { - $this->assertCount(0, $entitlements); - } + $this->backdateEntitlements($user->fresh()->entitlements, Carbon::now()->subMonths(1)); - $this->assertEquals($balance, $wallet->balance); + $wallet->chargeEntitlements(); - // \App\Handlers\DomainHosting SKU - $sku = Sku::where('title', 'domain-hosting')->first(); - Entitlement::create( - [ - 'owner_id' => $user->id, - 'wallet_id' => $wallet->id, - 'sku_id' => $sku->id, - 'entitleable_id' => $domain->id, - 'entitleable_type' => Domain::class - ] - ); + $this->assertTrue($wallet->balance < 0); + } - $entitlements = $sku->entitlements->where('owner_id', $user->id); - $wallet->refresh(); - - if ($sku->active) { - $balance -= $sku->cost; - $this->assertCount(1, $entitlements); - $this->assertEquals($domain->id, $entitlements->first()->entitleable_id); - $this->assertSame( - Handlers\DomainHosting::entitleableClass(), - $entitlements->first()->entitleable_type - ); - } else { - $this->assertCount(0, $entitlements); - } - - $this->assertEquals($balance, $wallet->balance); - - // \App\Handlers\Groupware SKU - $sku = Sku::where('title', 'groupware')->first(); - Entitlement::create( - [ - 'owner_id' => $user->id, - 'wallet_id' => $user->wallets()->get()[0]->id, - 'sku_id' => $sku->id, - 'entitleable_id' => $user->id, - 'entitleable_type' => User::class - ] - ); + public function testSkuEntitlements(): void + { + $this->assertCount(2, Sku::where('title', 'mailbox')->first()->entitlements); + } - $entitlements = $sku->entitlements->where('owner_id', $user->id); - $wallet->refresh(); + public function testSkuPackages(): void + { + $this->assertCount(2, Sku::where('title', 'mailbox')->first()->packages); + } - if ($sku->active) { - $balance -= $sku->cost; - $this->assertCount(1, $entitlements); - $this->assertEquals($user->id, $entitlements->first()->entitleable_id); - $this->assertSame( - Handlers\Mailbox::entitleableClass(), - $entitlements->first()->entitleable_type - ); - } else { - $this->assertCount(0, $entitlements); - } + public function testSkuHandlerDomainHosting(): void + { + $sku = Sku::where('title', 'domain-hosting')->first(); - $this->assertEquals($balance, $wallet->balance); + $entitlement = $sku->entitlements->first(); - // \App\Handlers\Storage SKU - $sku = Sku::where('title', 'storage')->first(); - Entitlement::create( - [ - 'owner_id' => $user->id, - 'wallet_id' => $wallet->id, - 'sku_id' => $sku->id, - 'entitleable_id' => $user->id, - 'entitleable_type' => User::class - ] + $this->assertSame( + Handlers\DomainHosting::entitleableClass(), + $entitlement->entitleable_type ); + } - $entitlements = $sku->entitlements->where('owner_id', $user->id); - $wallet->refresh(); + public function testSkuHandlerMailbox(): void + { + $sku = Sku::where('title', 'mailbox')->first(); - if ($sku->active) { - $balance -= $sku->cost; - $this->assertCount(1, $entitlements); - } else { - $this->assertCount(0, $entitlements); - } + $entitlement = $sku->entitlements->first(); - $this->assertEquals($balance, $wallet->balance); + $this->assertSame( + Handlers\Mailbox::entitleableClass(), + $entitlement->entitleable_type + ); } - public function testSkuPackages(): void + public function testSkuHandlerStorage(): void { - $sku = Sku::where('title', 'mailbox')->first(); + $sku = Sku::where('title', 'storage')->first(); - $packages = $sku->packages; + $entitlement = $sku->entitlements->first(); - $this->assertCount(2, $packages); + $this->assertSame( + Handlers\Storage::entitleableClass(), + $entitlement->entitleable_type + ); } } diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php --- a/src/tests/Feature/UserTest.php +++ b/src/tests/Feature/UserTest.php @@ -50,22 +50,22 @@ public function testUserCreateJob(): void { // Fake the queue, assert that no jobs were pushed... - Queue::fake(); - Queue::assertNothingPushed(); + $queue = Queue::fake(); + $queue->assertNothingPushed(); $user = User::create([ 'email' => 'user-create-test@' . \config('app.domain') ]); - Queue::assertPushed(\App\Jobs\UserCreate::class, 1); - Queue::assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($user) { + $queue->assertPushed(\App\Jobs\UserCreate::class, 1); + $queue->assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($user) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->id === $user->id && $job_user->email === $user->email; }); - Queue::assertPushedWithChain(\App\Jobs\UserCreate::class, [ + $queue->assertPushedWithChain(\App\Jobs\UserCreate::class, [ \App\Jobs\UserVerify::class, ]); /* @@ -74,8 +74,8 @@ independently (not chained) and make sure there's no race-condition in status update - Queue::assertPushed(\App\Jobs\UserVerify::class, 1); - Queue::assertPushed(\App\Jobs\UserVerify::class, function ($job) use ($user) { + $queue->assertPushed(\App\Jobs\UserVerify::class, 1); + $queue->assertPushed(\App\Jobs\UserVerify::class, function ($job) use ($user) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->id === $user->id diff --git a/src/tests/Feature/VerificationCodeTest.php b/src/tests/Feature/VerificationCodeTest.php --- a/src/tests/Feature/VerificationCodeTest.php +++ b/src/tests/Feature/VerificationCodeTest.php @@ -48,6 +48,7 @@ $this->assertSame($data['mode'], $code->mode); $this->assertEquals($user->id, $code->user->id); $this->assertInstanceOf(\DateTime::class, $code->expires_at); + $this->assertSame($code_exp_hrs, $code->expires_at->diff($now)->h + 1); $inst = VerificationCode::find($code->code); diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php --- a/src/tests/Feature/WalletTest.php +++ b/src/tests/Feature/WalletTest.php @@ -47,7 +47,7 @@ { $user = $this->getTestUser('UserWallet1@UserWallet.com'); - $this->assertTrue($user->wallets()->count() == 1); + $this->assertCount(1, $user->wallets); } /** @@ -61,11 +61,11 @@ new Wallet(['currency' => 'USD']) ); - $this->assertTrue($user->wallets()->count() >= 2); + $this->assertCount(2, $user->wallets); $user->wallets()->each( function ($wallet) { - $this->assertTrue($wallet->balance === 0.00); + $this->assertEquals(0, $wallet->balance); } ); } @@ -79,7 +79,7 @@ $user->wallets()->each( function ($wallet) { - $wallet->credit(1.00)->save(); + $wallet->credit(100)->save(); } ); @@ -97,7 +97,7 @@ { $user = $this->getTestUser('UserWallet4@UserWallet.com'); - $this->assertTrue($user->wallets()->count() == 1); + $this->assertCount(1, $user->wallets); $user->wallets()->each( function ($wallet) { @@ -140,15 +140,12 @@ } ); - $this->assertTrue( - $userB->accounts()->count() == 1, - "number of accounts (1 expected): {$userB->accounts()->count()}" - ); + $this->assertCount(1, $userB->accounts); - $aWallet = $userA->wallets()->get(); - $bAccount = $userB->accounts()->get(); + $aWallet = $userA->wallets()->first(); + $bAccount = $userB->accounts()->first(); - $this->assertTrue($bAccount[0]->id === $aWallet[0]->id); + $this->assertTrue($bAccount->id === $aWallet->id); } /** @@ -173,6 +170,6 @@ } ); - $this->assertTrue($userB->accounts()->count() == 0); + $this->assertCount(0, $userB->accounts); } } diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php --- a/src/tests/TestCase.php +++ b/src/tests/TestCase.php @@ -11,10 +11,21 @@ { use CreatesApplication; + protected function backdateEntitlements($entitlements, $targetDate) + { + foreach ($entitlements as $entitlement) { + $entitlement->created_at = $targetDate; + $entitlement->updated_at = $targetDate; + $entitlement->save(); + } + } + protected function deleteTestDomain($name) { Queue::fake(); + $domain = Domain::withTrashed()->where('namespace', $name)->first(); + if (!$domain) { return; } @@ -28,6 +39,7 @@ protected function deleteTestUser($email) { Queue::fake(); + $user = User::withTrashed()->where('email', $email)->first(); if (!$user) { @@ -59,7 +71,17 @@ { // Disable jobs (i.e. skip LDAP oprations) Queue::fake(); - return User::firstOrCreate(['email' => $email], $attrib); + $user = User::withTrashed()->where('email', $email)->first(); + + if (!$user) { + return User::firstOrCreate(['email' => $email], $attrib); + } + + if ($user->deleted_at) { + $user->restore(); + } + + return $user; } /**