Page MenuHomePhorge

D2446.1775212312.diff
No OneTemporary

Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None

D2446.1775212312.diff

diff --git a/bin/phpstan b/bin/phpstan
--- a/bin/phpstan
+++ b/bin/phpstan
@@ -4,7 +4,7 @@
pushd ${cwd}/../src/
-php -dmemory_limit=400M \
+php -dmemory_limit=500M \
vendor/bin/phpstan \
analyse
diff --git a/src/app/Console/Commands/DomainSetWallet.php b/src/app/Console/Commands/DomainSetWallet.php
--- a/src/app/Console/Commands/DomainSetWallet.php
+++ b/src/app/Console/Commands/DomainSetWallet.php
@@ -60,6 +60,7 @@
'wallet_id' => $wallet->id,
'sku_id' => $sku->id,
'cost' => 0,
+ 'fee' => 0,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class,
]
diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -89,6 +89,7 @@
'wallet_id' => $wallet_id,
'sku_id' => $sku->id,
'cost' => $sku->pivot->cost(),
+ 'fee' => $sku->pivot->fee(),
'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
@@ -11,10 +11,18 @@
*
* Owned by a {@link \App\User}, billed to a {@link \App\Wallet}.
*
- * @property \App\User $owner The owner of this entitlement (subject).
- * @property \App\Sku $sku The SKU to which this entitlement applies.
- * @property \App\Wallet $wallet The wallet to which this entitlement is charged.
- * @property \App\Domain|\App\User $entitleable The entitled object (receiver of the entitlement).
+ * @property int $cost
+ * @property ?string $description
+ * @property \App\Domain|\App\User $entitleable The entitled object (receiver of the entitlement).
+ * @property int $entitleable_id
+ * @property string $entitleable_type
+ * @property int $fee
+ * @property string $id
+ * @property \App\User $owner The owner of this entitlement (subject).
+ * @property \App\Sku $sku The SKU to which this entitlement applies.
+ * @property string $sku_id
+ * @property \App\Wallet $wallet The wallet to which this entitlement is charged.
+ * @property string $wallet_id
*/
class Entitlement extends Model
{
@@ -45,11 +53,13 @@
'entitleable_id',
'entitleable_type',
'cost',
- 'description'
+ 'description',
+ 'fee',
];
protected $casts = [
'cost' => 'integer',
+ 'fee' => 'integer'
];
/**
diff --git a/src/app/Group.php b/src/app/Group.php
--- a/src/app/Group.php
+++ b/src/app/Group.php
@@ -64,6 +64,7 @@
'wallet_id' => $wallet->id,
'sku_id' => $sku->id,
'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
+ 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
'entitleable_id' => $this->id,
'entitleable_type' => Group::class
]);
diff --git a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
@@ -60,8 +60,9 @@
public function oneOff(Request $request, $id)
{
$wallet = Wallet::find($id);
+ $user = Auth::guard()->user();
- if (empty($wallet) || !Auth::guard()->user()->canRead($wallet)) {
+ if (empty($wallet) || !$user->canRead($wallet)) {
return $this->errorResponse(404);
}
@@ -97,6 +98,14 @@
]
);
+ if ($user->role == 'reseller') {
+ if ($user->tenant && ($tenant_wallet = $user->tenant->wallet())) {
+ $desc = ($amount > 0 ? 'Awarded' : 'Penalized') . " user {$wallet->owner->email}";
+ $method = $amount > 0 ? 'debit' : 'credit';
+ $tenant_wallet->{$method}(abs($amount), $desc);
+ }
+ }
+
DB::commit();
$response = [
diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
--- a/src/app/Http/Controllers/API/V4/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -186,7 +186,7 @@
$data['name'] = $sku->name;
$data['description'] = $sku->description;
- unset($data['handler_class'], $data['created_at'], $data['updated_at']);
+ unset($data['handler_class'], $data['created_at'], $data['updated_at'], $data['fee'], $data['tenant_id']);
return $data;
}
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
@@ -123,7 +123,6 @@
return;
}
- $cost = 0;
$now = Carbon::now();
// get the discount rate applied to the wallet.
@@ -131,7 +130,8 @@
// just in case this had not been billed yet, ever
$diffInMonths = $entitlement->updated_at->diffInMonths($now);
- $cost += (int) ($entitlement->cost * $discount * $diffInMonths);
+ $cost = (int) ($entitlement->cost * $discount * $diffInMonths);
+ $fee = (int) ($entitlement->fee * $diffInMonths);
// this moves the hypothetical updated at forward to however many months past the original
$updatedAt = $entitlement->updated_at->copy()->addMonthsWithoutOverflow($diffInMonths);
@@ -153,8 +153,18 @@
}
$pricePerDay = $entitlement->cost / $daysInMonth;
+ $feePerDay = $entitlement->fee / $daysInMonth;
$cost += (int) (round($pricePerDay * $discount * $diffInDays, 0));
+ $fee += (int) (round($feePerDay * $diffInDays, 0));
+
+ $profit = $cost - $fee;
+
+ if ($profit != 0 && $owner->tenant && ($wallet = $owner->tenant->wallet())) {
+ $desc = "Charged user {$owner->email}";
+ $method = $profit > 0 ? 'credit' : 'debit';
+ $wallet->{$method}(abs($profit), $desc);
+ }
if ($cost == 0) {
return;
diff --git a/src/app/Observers/PackageObserver.php b/src/app/Observers/PackageObserver.php
--- a/src/app/Observers/PackageObserver.php
+++ b/src/app/Observers/PackageObserver.php
@@ -27,5 +27,7 @@
break;
}
}
+
+ $package->tenant_id = \config('app.tenant_id');
}
}
diff --git a/src/app/Observers/PackageSkuObserver.php b/src/app/Observers/PackageSkuObserver.php
--- a/src/app/Observers/PackageSkuObserver.php
+++ b/src/app/Observers/PackageSkuObserver.php
@@ -6,6 +6,25 @@
class PackageSkuObserver
{
+ /**
+ * Handle the "creating" event on an PackageSku relation.
+ *
+ * Ensures that the entries belong to the same tenant.
+ *
+ * @param \App\PackageSku $packageSku The package-sku relation
+ *
+ * @return void
+ */
+ public function creating(PackageSku $packageSku)
+ {
+ $package = $packageSku->package;
+ $sku = $packageSku->sku;
+
+ if ($package->tenant_id != $sku->tenant_id) {
+ throw new \Exception("Package and SKU owned by different tenants");
+ }
+ }
+
/**
* Handle the "created" event on an PackageSku relation
*
diff --git a/src/app/Observers/PlanObserver.php b/src/app/Observers/PlanObserver.php
--- a/src/app/Observers/PlanObserver.php
+++ b/src/app/Observers/PlanObserver.php
@@ -27,5 +27,7 @@
break;
}
}
+
+ $plan->tenant_id = \config('app.tenant_id');
}
}
diff --git a/src/app/Observers/PlanPackageObserver.php b/src/app/Observers/PlanPackageObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/PlanPackageObserver.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Observers;
+
+use App\PlanPackage;
+
+class PlanPackageObserver
+{
+ /**
+ * Handle the "creating" event on an PlanPackage relation.
+ *
+ * Ensures that the entries belong to the same tenant.
+ *
+ * @param \App\PlanPackage $planPackage The plan-package relation
+ *
+ * @return void
+ */
+ public function creating(PlanPackage $planPackage)
+ {
+ $package = $planPackage->package;
+ $plan = $planPackage->plan;
+
+ if ($package->tenant_id != $plan->tenant_id) {
+ throw new \Exception("Package and Plan owned by different tenants");
+ }
+ }
+}
diff --git a/src/app/Observers/SkuObserver.php b/src/app/Observers/SkuObserver.php
--- a/src/app/Observers/SkuObserver.php
+++ b/src/app/Observers/SkuObserver.php
@@ -22,5 +22,9 @@
break;
}
}
+
+ $sku->tenant_id = \config('app.tenant_id');
+
+ // TODO: We should make sure that tenant_id + title is unique
}
}
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -108,6 +108,18 @@
}
});
}
+
+ // Debit the reseller's wallet with the user negative balance
+ $balance = 0;
+ foreach ($user->wallets as $wallet) {
+ // Note: here we assume all user wallets are using the same currency.
+ // It might get changed in the future
+ $balance += $wallet->balance;
+ }
+
+ if ($balance < 0 && $user->tenant && ($wallet = $user->tenant->wallet())) {
+ $wallet->debit($balance * -1, "Deleted user {$user->email}");
+ }
}
/**
diff --git a/src/app/Package.php b/src/app/Package.php
--- a/src/app/Package.php
+++ b/src/app/Package.php
@@ -21,6 +21,13 @@
* * Free package: mailbox + quota.
*
* Selecting a package will therefore create a set of entitlments from SKUs.
+ *
+ * @property string $description
+ * @property int $discount_rate
+ * @property string $id
+ * @property string $name
+ * @property ?int $tenant_id
+ * @property string $title
*/
class Package extends Model
{
@@ -69,7 +76,10 @@
return $costs;
}
- public function isDomain()
+ /**
+ * Checks whether the package contains a domain SKU.
+ */
+ public function isDomain(): bool
{
foreach ($this->skus as $sku) {
if ($sku->handler_class::entitleableClass() == \App\Domain::class) {
@@ -94,4 +104,14 @@
['qty']
);
}
+
+ /**
+ * The tenant for this package.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_id', 'id');
+ }
}
diff --git a/src/app/PackageSku.php b/src/app/PackageSku.php
--- a/src/app/PackageSku.php
+++ b/src/app/PackageSku.php
@@ -35,30 +35,50 @@
*/
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;
}
+ // FIXME: Why package_skus.cost value is not used anywhere?
+
$ppu = $this->sku->cost * ((100 - $this->package->discount_rate) / 100);
- $costs += $units * $ppu;
+ return $units * $ppu;
+ }
+
+ /**
+ * Under this package, what fee this SKU has?
+ *
+ * @return int The fee for this SKU under this package in cents.
+ */
+ public function fee()
+ {
+ $units = $this->qty - $this->sku->units_free;
+
+ if ($units < 0) {
+ $units = 0;
+ }
- return $costs;
+ return $this->sku->fee * $units;
}
+ /**
+ * The package for this relation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
public function package()
{
return $this->belongsTo('App\Package');
}
+ /**
+ * The SKU for this relation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
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
@@ -13,7 +13,16 @@
* A "Family Plan" as such may exist of "2 or more Kolab packages",
* and apply a discount for the third and further Kolab packages.
*
+ * @property string $description
+ * @property int $discount_qty
+ * @property int $discount_rate
+ * @property string $id
+ * @property string $name
* @property \App\Package[] $packages
+ * @property datetime $promo_from
+ * @property datetime $promo_to
+ * @property ?int $tenant_id
+ * @property string $title
*/
class Plan extends Model
{
@@ -105,4 +114,14 @@
return false;
}
+
+ /**
+ * The tenant for this plan.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_id', 'id');
+ }
}
diff --git a/src/app/PlanPackage.php b/src/app/PlanPackage.php
--- a/src/app/PlanPackage.php
+++ b/src/app/PlanPackage.php
@@ -15,6 +15,7 @@
* @property int $qty_max
* @property int $qty_min
* @property \App\Package $package
+ * @property \App\Plan $plan
*/
class PlanPackage extends Pivot
{
@@ -54,8 +55,23 @@
return $costs;
}
+ /**
+ * The package in this relation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
public function package()
{
return $this->belongsTo('App\Package');
}
+
+ /**
+ * The plan in this relation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function plan()
+ {
+ return $this->belongsTo('App\Plan');
+ }
}
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
@@ -35,6 +35,7 @@
\App\Package::observe(\App\Observers\PackageObserver::class);
\App\PackageSku::observe(\App\Observers\PackageSkuObserver::class);
\App\Plan::observe(\App\Observers\PlanObserver::class);
+ \App\PlanPackage::observe(\App\Observers\PlanPackageObserver::class);
\App\SignupCode::observe(\App\Observers\SignupCodeObserver::class);
\App\SignupInvitation::observe(\App\Observers\SignupInvitationObserver::class);
\App\Sku::observe(\App\Observers\SkuObserver::class);
diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php
--- a/src/app/Providers/PaymentProvider.php
+++ b/src/app/Providers/PaymentProvider.php
@@ -261,6 +261,8 @@
$refund['status'] = self::STATUS_PAID;
$refund['amount'] = -1 * $amount;
+ // FIXME: Refunds/chargebacks are out of the reseller comissioning for now
+
$this->storePayment($refund, $wallet->id);
}
diff --git a/src/app/Sku.php b/src/app/Sku.php
--- a/src/app/Sku.php
+++ b/src/app/Sku.php
@@ -7,6 +7,18 @@
/**
* The eloquent definition of a Stock Keeping Unit (SKU).
+ *
+ * @property bool $active
+ * @property int $cost
+ * @property string $description
+ * @property int $fee The fee that the tenant pays to us
+ * @property string $handler_class
+ * @property string $id
+ * @property string $name
+ * @property string $period
+ * @property ?int $tenant_id
+ * @property string $title
+ * @property int $units_free
*/
class Sku extends Model
{
@@ -23,6 +35,7 @@
'active',
'cost',
'description',
+ 'fee',
'handler_class',
'name',
// persist for annual domain registration
@@ -59,4 +72,14 @@
'package_skus'
)->using('App\PackageSku')->withPivot(['cost', 'qty']);
}
+
+ /**
+ * The tenant for this SKU.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_id', 'id');
+ }
}
diff --git a/src/app/Tenant.php b/src/app/Tenant.php
--- a/src/app/Tenant.php
+++ b/src/app/Tenant.php
@@ -37,4 +37,16 @@
{
return $this->hasMany('App\SignupInvitation');
}
+
+ /*
+ * Returns the wallet of the tanant (reseller's wallet).
+ *
+ * @return ?\App\Wallet A wallet object
+ */
+ public function wallet(): ?Wallet
+ {
+ $user = \App\User::where('role', 'reseller')->where('tenant_id', $this->id)->first();
+
+ return $user ? $user->wallets->first() : null;
+ }
}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -126,6 +126,7 @@
'wallet_id' => $wallet_id,
'sku_id' => $sku->id,
'cost' => $sku->pivot->cost(),
+ 'fee' => $sku->pivot->fee(),
'entitleable_id' => $user->id,
'entitleable_type' => User::class
]
@@ -179,6 +180,7 @@
'wallet_id' => $wallet->id,
'sku_id' => $sku->id,
'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
+ 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
'entitleable_id' => $this->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
@@ -76,6 +76,7 @@
return 0;
}
+ $profit = 0;
$charges = 0;
$discount = $this->getDiscountRate();
@@ -101,8 +102,10 @@
$diff = $entitlement->updated_at->diffInMonths(Carbon::now());
$cost = (int) ($entitlement->cost * $discount * $diff);
+ $fee = (int) ($entitlement->fee * $diff);
$charges += $cost;
+ $profit += $cost - $fee;
// if we're in dry-run, you know...
if (!$apply) {
@@ -126,7 +129,17 @@
}
if ($apply) {
- $this->debit($charges, $entitlementTransactions);
+ $this->debit($charges, '', $entitlementTransactions);
+
+ // Credit/debit the reseller
+ if ($profit != 0 && $this->owner->tenant) {
+ // FIXME: Should we have a simpler way to skip this for non-reseller tenant(s)
+ if ($wallet = $this->owner->tenant->wallet()) {
+ $desc = "Charged user {$this->owner->email}";
+ $method = $profit > 0 ? 'credit' : 'debit';
+ $wallet->{$method}(abs($profit), $desc);
+ }
+ }
}
DB::commit();
@@ -234,12 +247,13 @@
/**
* Deduct an amount of pecunia from this wallet's balance.
*
- * @param int $amount The amount of pecunia to deduct (in cents).
- * @param array $eTIDs List of transaction IDs for the individual entitlements that make up
- * this debit record, if any.
+ * @param int $amount The amount of pecunia to deduct (in cents).
+ * @param string $description The transaction description
+ * @param array $eTIDs List of transaction IDs for the individual entitlements
+ * that make up this debit record, if any.
* @return Wallet Self
*/
- public function debit(int $amount, array $eTIDs = []): Wallet
+ public function debit(int $amount, string $description = '', array $eTIDs = []): Wallet
{
if ($amount == 0) {
return $this;
@@ -254,11 +268,14 @@
'object_id' => $this->id,
'object_type' => \App\Wallet::class,
'type' => \App\Transaction::WALLET_DEBIT,
- 'amount' => $amount * -1
+ 'amount' => $amount * -1,
+ 'description' => $description
]
);
- \App\Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]);
+ if (!empty($eTIDs)) {
+ \App\Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]);
+ }
return $this;
}
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
@@ -21,7 +21,6 @@
$table->string('package_id', 36);
$table->string('sku_id', 36);
$table->integer('qty')->default(1);
-
$table->integer('cost')->default(0)->nullable();
$table->foreign('package_id')->references('id')->on('packages')
diff --git a/src/database/migrations/2020_05_05_095212_create_tenants_table.php b/src/database/migrations/2020_05_05_095212_create_tenants_table.php
--- a/src/database/migrations/2020_05_05_095212_create_tenants_table.php
+++ b/src/database/migrations/2020_05_05_095212_create_tenants_table.php
@@ -23,14 +23,34 @@
}
);
- Schema::table(
- 'users',
- function (Blueprint $table) {
- $table->bigInteger('tenant_id')->unsigned()->nullable();
+ \App\Tenant::create(['title' => 'Kolab Now']);
+
+ foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $table_name) {
+ Schema::table(
+ $table_name,
+ function (Blueprint $table) {
+ $table->bigInteger('tenant_id')->unsigned()->nullable();
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
+ }
+ );
- $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
+ if ($tenant_id = \config('app.tenant_id')) {
+ DB::statement("UPDATE `{$table_name}` SET `tenant_id` = {$tenant_id}");
}
- );
+ }
+
+ // Add fee column
+ foreach (['entitlements', 'skus'] as $table) {
+ Schema::table(
+ $table,
+ function (Blueprint $table) {
+ $table->integer('fee')->nullable();
+ }
+ );
+ }
+
+ // FIXME: Should we also have package_skus.fee ?
+ // We have package_skus.cost, but I think it is not used anywhere.
}
/**
@@ -40,13 +60,24 @@
*/
public function down()
{
- Schema::table(
- 'users',
- function (Blueprint $table) {
- $table->dropForeign(['tenant_id']);
- $table->dropColumn('tenant_id');
- }
- );
+ foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $table_name) {
+ Schema::table(
+ $table_name,
+ function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ }
+ );
+ }
+
+ foreach (['entitlements', 'skus'] as $table) {
+ Schema::table(
+ $table,
+ function (Blueprint $table) {
+ $table->dropColumn('fee');
+ }
+ );
+ }
Schema::dropIfExists('tenants');
}
diff --git a/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php b/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php
--- a/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php
+++ b/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php
@@ -15,14 +15,20 @@
public function up()
{
$beta_sku = \App\Sku::where('title', 'beta')->first();
- $beta_sku->name = 'Private Beta (invitation only)';
- $beta_sku->description = 'Access to the private beta program subscriptions';
- $beta_sku->save();
+
+ if ($beta_sku) {
+ $beta_sku->name = 'Private Beta (invitation only)';
+ $beta_sku->description = 'Access to the private beta program subscriptions';
+ $beta_sku->save();
+ }
$meet_sku = \App\Sku::where('title', 'meet')->first();
- $meet_sku->name = 'Voice & Video Conferencing (public beta)';
- $meet_sku->handler_class = 'App\Handlers\Meet';
- $meet_sku->save();
+
+ if ($meet_sku) {
+ $meet_sku->name = 'Voice & Video Conferencing (public beta)';
+ $meet_sku->handler_class = 'App\Handlers\Meet';
+ $meet_sku->save();
+ }
}
/**
diff --git a/src/database/migrations/2021_02_19_093832_discounts_add_tenant_id.php b/src/database/migrations/2021_02_19_093832_discounts_add_tenant_id.php
deleted file mode 100644
--- a/src/database/migrations/2021_02_19_093832_discounts_add_tenant_id.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-use Illuminate\Database\Migrations\Migration;
-use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Support\Facades\Schema;
-
-// phpcs:ignore
-class DiscountsAddTenantId extends Migration
-{
- /**
- * Run the migrations.
- *
- * @return void
- */
- public function up()
- {
- Schema::table(
- 'discounts',
- function (Blueprint $table) {
- $table->bigInteger('tenant_id')->unsigned()->default(\config('app.tenant_id'))->nullable();
-
- $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
- }
- );
- }
-
- /**
- * Reverse the migrations.
- *
- * @return void
- */
- public function down()
- {
- Schema::table(
- 'discounts',
- function (Blueprint $table) {
- $table->dropForeign(['tenant_id']);
- $table->dropColumn('tenant_id');
- }
- );
- }
-}
diff --git a/src/database/migrations/2021_02_19_093833_domains_add_tenant_id.php b/src/database/migrations/2021_02_19_093833_domains_add_tenant_id.php
deleted file mode 100644
--- a/src/database/migrations/2021_02_19_093833_domains_add_tenant_id.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-use Illuminate\Database\Migrations\Migration;
-use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Support\Facades\Schema;
-
-// phpcs:ignore
-class DomainsAddTenantId extends Migration
-{
- /**
- * Run the migrations.
- *
- * @return void
- */
- public function up()
- {
- Schema::table(
- 'domains',
- function (Blueprint $table) {
- $table->bigInteger('tenant_id')->unsigned()->default(\config('app.tenant_id'))->nullable();
-
- $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
- }
- );
- }
-
- /**
- * Reverse the migrations.
- *
- * @return void
- */
- public function down()
- {
- Schema::table(
- 'domains',
- function (Blueprint $table) {
- $table->dropForeign(['tenant_id']);
- $table->dropColumn('tenant_id');
- }
- );
- }
-}
diff --git a/src/database/migrations/2021_05_07_150000_groups_add_tenant_id.php b/src/database/migrations/2021_05_12_150000_groups_add_tenant_id.php
rename from src/database/migrations/2021_05_07_150000_groups_add_tenant_id.php
rename to src/database/migrations/2021_05_12_150000_groups_add_tenant_id.php
--- a/src/database/migrations/2021_05_07_150000_groups_add_tenant_id.php
+++ b/src/database/migrations/2021_05_12_150000_groups_add_tenant_id.php
@@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
// phpcs:ignore
@@ -17,11 +18,14 @@
Schema::table(
'groups',
function (Blueprint $table) {
- $table->bigInteger('tenant_id')->unsigned()->default(\config('app.tenant_id'))->nullable();
-
+ $table->bigInteger('tenant_id')->unsigned()->nullable();
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
}
);
+
+ if ($tenant_id = \config('app.tenant_id')) {
+ DB::statement("UPDATE `groups` SET `tenant_id` = {$tenant_id}");
+ }
}
/**
diff --git a/src/database/seeds/local/TenantSeeder.php b/src/database/seeds/local/TenantSeeder.php
--- a/src/database/seeds/local/TenantSeeder.php
+++ b/src/database/seeds/local/TenantSeeder.php
@@ -14,16 +14,16 @@
*/
public function run()
{
- Tenant::create(
- [
- 'title' => 'Kolab Now'
- ]
- );
+ if (!Tenant::find(1)) {
+ Tenant::create([
+ 'title' => 'Kolab Now'
+ ]);
+ }
- Tenant::create(
- [
- 'title' => 'Sample Tenant'
- ]
- );
+ if (!Tenant::find(2)) {
+ Tenant::create([
+ 'title' => 'Sample Tenant'
+ ]);
+ }
}
}
diff --git a/src/tests/Feature/Controller/Admin/WalletsTest.php b/src/tests/Feature/Controller/Admin/WalletsTest.php
--- a/src/tests/Feature/Controller/Admin/WalletsTest.php
+++ b/src/tests/Feature/Controller/Admin/WalletsTest.php
@@ -76,6 +76,10 @@
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$wallet = $user->wallets()->first();
$balance = $wallet->balance;
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $reseller_wallet = $reseller->wallets()->first();
+ $reseller_balance = $reseller_wallet->balance;
Transaction::where('object_id', $wallet->id)
->whereIn('type', [Transaction::WALLET_AWARD, Transaction::WALLET_PENALTY])
@@ -109,6 +113,7 @@
$this->assertSame('The bonus has been added to the wallet successfully.', $json['message']);
$this->assertSame($balance += 5000, $json['balance']);
$this->assertSame($balance, $wallet->fresh()->balance);
+ $this->assertSame($reseller_balance, $reseller_wallet->fresh()->balance);
$transaction = Transaction::where('object_id', $wallet->id)
->where('type', Transaction::WALLET_AWARD)->first();
@@ -128,6 +133,7 @@
$this->assertSame('The penalty has been added to the wallet successfully.', $json['message']);
$this->assertSame($balance -= 4000, $json['balance']);
$this->assertSame($balance, $wallet->fresh()->balance);
+ $this->assertSame($reseller_balance, $reseller_wallet->fresh()->balance);
$transaction = Transaction::where('object_id', $wallet->id)
->where('type', Transaction::WALLET_PENALTY)->first();
diff --git a/src/tests/Feature/Controller/Reseller/WalletsTest.php b/src/tests/Feature/Controller/Reseller/WalletsTest.php
--- a/src/tests/Feature/Controller/Reseller/WalletsTest.php
+++ b/src/tests/Feature/Controller/Reseller/WalletsTest.php
@@ -94,11 +94,14 @@
$reseller1 = $this->getTestUser('reseller@kolabnow.com');
$reseller2 = $this->getTestUser('reseller@reseller.com');
$wallet = $user->wallets()->first();
+ $reseller1_wallet = $reseller1->wallets()->first();
$balance = $wallet->balance;
+ $reseller1_balance = $reseller1_wallet->balance;
Transaction::where('object_id', $wallet->id)
->whereIn('type', [Transaction::WALLET_AWARD, Transaction::WALLET_PENALTY])
->delete();
+ Transaction::where('object_id', $reseller1_wallet->id)->delete();
// Non-admin user
$response = $this->actingAs($user)->post("api/v4/wallets/{$wallet->id}/one-off", []);
@@ -125,7 +128,7 @@
$this->assertCount(2, $json);
$this->assertCount(2, $json['errors']);
- // Admin user - a valid bonus
+ // A valid bonus
$post = ['amount' => '50', 'description' => 'A bonus'];
$response = $this->actingAs($reseller1)->post("api/v4/wallets/{$wallet->id}/one-off", $post);
$response->assertStatus(200);
@@ -136,6 +139,7 @@
$this->assertSame('The bonus has been added to the wallet successfully.', $json['message']);
$this->assertSame($balance += 5000, $json['balance']);
$this->assertSame($balance, $wallet->fresh()->balance);
+ $this->assertSame($reseller1_balance -= 5000, $reseller1_wallet->fresh()->balance);
$transaction = Transaction::where('object_id', $wallet->id)
->where('type', Transaction::WALLET_AWARD)->first();
@@ -144,7 +148,14 @@
$this->assertSame(5000, $transaction->amount);
$this->assertSame($reseller1->email, $transaction->user_email);
- // Admin user - a valid penalty
+ $transaction = Transaction::where('object_id', $reseller1_wallet->id)
+ ->where('type', Transaction::WALLET_DEBIT)->first();
+
+ $this->assertSame("Awarded user {$user->email}", $transaction->description);
+ $this->assertSame(-5000, $transaction->amount);
+ $this->assertSame($reseller1->email, $transaction->user_email);
+
+ // A valid penalty
$post = ['amount' => '-40', 'description' => 'A penalty'];
$response = $this->actingAs($reseller1)->post("api/v4/wallets/{$wallet->id}/one-off", $post);
$response->assertStatus(200);
@@ -155,6 +166,7 @@
$this->assertSame('The penalty has been added to the wallet successfully.', $json['message']);
$this->assertSame($balance -= 4000, $json['balance']);
$this->assertSame($balance, $wallet->fresh()->balance);
+ $this->assertSame($reseller1_balance += 4000, $reseller1_wallet->fresh()->balance);
$transaction = Transaction::where('object_id', $wallet->id)
->where('type', Transaction::WALLET_PENALTY)->first();
@@ -163,6 +175,13 @@
$this->assertSame(-4000, $transaction->amount);
$this->assertSame($reseller1->email, $transaction->user_email);
+ $transaction = Transaction::where('object_id', $reseller1_wallet->id)
+ ->where('type', Transaction::WALLET_CREDIT)->first();
+
+ $this->assertSame("Penalized user {$user->email}", $transaction->description);
+ $this->assertSame(4000, $transaction->amount);
+ $this->assertSame($reseller1->email, $transaction->user_email);
+
// Reseller from a different tenant
\config(['app.tenant_id' => 2]);
$response = $this->actingAs($reseller2)->post("api/v4/wallets/{$wallet->id}/one-off", []);
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
@@ -82,7 +82,6 @@
);
$domain->assignPackage($packageDomain, $owner);
-
$owner->assignPackage($packageKolab);
$owner->assignPackage($packageKolab, $user);
@@ -128,57 +127,4 @@
$this->assertEquals($user->id, $entitlement->entitleable->id);
$this->assertTrue($entitlement->entitleable instanceof \App\User);
}
-
- /**
- * @todo This really should be in User or Wallet tests file
- */
- public function testBillDeletedEntitlement(): void
- {
- $user = $this->getTestUser('entitlement-test@kolabnow.com');
- $package = \App\Package::where('title', 'kolab')->first();
-
- $storage = \App\Sku::where('title', 'storage')->first();
-
- $user->assignPackage($package);
- // some additional SKUs so we have something to delete.
- $user->assignSku($storage, 4);
-
- // the mailbox, the groupware, the 2 original storage and the additional 4
- $this->assertCount(8, $user->fresh()->entitlements);
-
- $wallet = $user->wallets()->first();
-
- $backdate = Carbon::now()->subWeeks(7);
- $this->backdateEntitlements($user->entitlements, $backdate);
-
- $charge = $wallet->chargeEntitlements();
-
- $this->assertSame(-1099, $wallet->balance);
-
- $balance = $wallet->balance;
- $discount = \App\Discount::where('discount', 30)->first();
- $wallet->discount()->associate($discount);
- $wallet->save();
-
- $user->removeSku($storage, 4);
-
- // we expect the wallet to have been charged for ~3 weeks of use of
- // 4 deleted storage entitlements, it should also take discount into account
- $backdate->addMonthsWithoutOverflow(1);
- $diffInDays = $backdate->diffInDays(Carbon::now());
-
- // entitlements-num * cost * discount * days-in-month
- $max = intval(4 * 25 * 0.7 * $diffInDays / 28);
- $min = intval(4 * 25 * 0.7 * $diffInDays / 31);
-
- $wallet->refresh();
- $this->assertTrue($wallet->balance >= $balance - $max);
- $this->assertTrue($wallet->balance <= $balance - $min);
-
- $transactions = \App\Transaction::where('object_id', $wallet->id)
- ->where('object_type', \App\Wallet::class)->get();
-
- // one round of the monthly invoicing, four sku deletions getting invoiced
- $this->assertCount(5, $transactions);
- }
}
diff --git a/src/tests/Feature/PlanTest.php b/src/tests/Feature/PlanTest.php
--- a/src/tests/Feature/PlanTest.php
+++ b/src/tests/Feature/PlanTest.php
@@ -106,4 +106,19 @@
$this->assertTrue($plan->cost() == $package_costs);
}
+
+ public function testTenant(): void
+ {
+ $plan = Plan::where('title', 'individual')->first();
+
+ $tenant = $plan->tenant()->first();
+
+ $this->assertInstanceof(\App\Tenant::class, $tenant);
+ $this->assertSame(1, $tenant->id);
+
+ $tenant = $plan->tenant;
+
+ $this->assertInstanceof(\App\Tenant::class, $tenant);
+ $this->assertSame(1, $tenant->id);
+ }
}
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
@@ -91,4 +91,19 @@
$entitlement->entitleable_type
);
}
+
+ public function testSkuTenant(): void
+ {
+ $sku = Sku::where('title', 'storage')->first();
+
+ $tenant = $sku->tenant()->first();
+
+ $this->assertInstanceof(\App\Tenant::class, $tenant);
+ $this->assertSame(1, $tenant->id);
+
+ $tenant = $sku->tenant;
+
+ $this->assertInstanceof(\App\Tenant::class, $tenant);
+ $this->assertSame(1, $tenant->id);
+ }
}
diff --git a/src/tests/Feature/TenantTest.php b/src/tests/Feature/TenantTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/TenantTest.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Tenant;
+use Tests\TestCase;
+
+class TenantTest extends TestCase
+{
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ }
+
+ /**
+ * Test Tenant::wallet() method
+ */
+ public function testWallet(): void
+ {
+ $tenant = Tenant::find(1);
+ $user = \App\User::where('email', 'reseller@kolabnow.com')->first();
+
+ $wallet = $tenant->wallet();
+
+ $this->assertInstanceof(\App\Wallet::class, $wallet);
+ $this->assertSame($user->wallets->first()->id, $wallet->id);
+ }
+}
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
@@ -464,6 +464,51 @@
Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 2);
}
+ /**
+ * Test handling negative balance on user deletion
+ */
+ public function testDeleteWithNegativeBalance(): void
+ {
+ $user = $this->getTestUser('user-test@' . \config('app.domain'));
+ $wallet = $user->wallets()->first();
+ $wallet->balance = -1000;
+ $wallet->save();
+ $reseller_wallet = $user->tenant->wallet();
+ $reseller_wallet->balance = 0;
+ $reseller_wallet->save();
+ \App\Transaction::where('object_id', $reseller_wallet->id)->where('object_type', \App\Wallet::class)->delete();
+
+ $user->delete();
+
+ $reseller_transactions = \App\Transaction::where('object_id', $reseller_wallet->id)
+ ->where('object_type', \App\Wallet::class)->get();
+
+ $this->assertSame(-1000, $reseller_wallet->fresh()->balance);
+ $this->assertCount(1, $reseller_transactions);
+ $trans = $reseller_transactions[0];
+ $this->assertSame("Deleted user {$user->email}", $trans->description);
+ $this->assertSame(-1000, $trans->amount);
+ $this->assertSame(\App\Transaction::WALLET_DEBIT, $trans->type);
+ }
+
+ /**
+ * Test handling positive balance on user deletion
+ */
+ public function testDeleteWithPositiveBalance(): void
+ {
+ $user = $this->getTestUser('user-test@' . \config('app.domain'));
+ $wallet = $user->wallets()->first();
+ $wallet->balance = 1000;
+ $wallet->save();
+ $reseller_wallet = $user->tenant->wallet();
+ $reseller_wallet->balance = 0;
+ $reseller_wallet->save();
+
+ $user->delete();
+
+ $this->assertSame(0, $reseller_wallet->fresh()->balance);
+ }
+
/**
* Tests for User::aliasExists()
*/
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
@@ -5,8 +5,10 @@
use App\Package;
use App\User;
use App\Sku;
+use App\Transaction;
use App\Wallet;
use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class WalletTest extends TestCase
@@ -39,6 +41,8 @@
$this->deleteTestUser($user);
}
+ Sku::select()->update(['fee' => 0]);
+
parent::tearDown();
}
@@ -279,4 +283,116 @@
$this->assertCount(0, $userB->accounts);
}
+
+ /**
+ * Test for charging and removing entitlements (including tenant commission calculations)
+ */
+ public function testChargeAndDeleteEntitlements(): void
+ {
+ $user = $this->getTestUser('jane@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $discount = \App\Discount::where('discount', 30)->first();
+ $wallet->discount()->associate($discount);
+ $wallet->save();
+
+ // Add 40% fee to all SKUs
+ Sku::select()->update(['fee' => DB::raw("`cost` * 0.4")]);
+
+ $package = Package::where('title', 'kolab')->first();
+ $storage = Sku::where('title', 'storage')->first();
+ $user->assignPackage($package);
+ $user->assignSku($storage, 2);
+ $user->refresh();
+
+ // Reset reseller's wallet balance and transactions
+ $reseller_wallet = $user->tenant->wallet();
+ $reseller_wallet->balance = 0;
+ $reseller_wallet->save();
+ Transaction::where('object_id', $reseller_wallet->id)->where('object_type', \App\Wallet::class)->delete();
+
+ // ------------------------------------
+ // Test normal charging of entitlements
+ // ------------------------------------
+
+ // Backdate and chanrge entitlements, we're expecting one month to be charged
+ // Set fake NOW date to make simpler asserting results that depend on number of days in current/last month
+ Carbon::setTestNow(Carbon::create(2021, 5, 21, 12));
+ $backdate = Carbon::now()->subWeeks(7);
+ $this->backdateEntitlements($user->entitlements, $backdate);
+ $charge = $wallet->chargeEntitlements();
+ $wallet->refresh();
+ $reseller_wallet->refresh();
+
+ // 388 + 310 + 17 + 17 = 732
+ $this->assertSame(-732, $wallet->balance);
+ // 388 - 555 x 40% + 310 - 444 x 40% + 34 - 50 x 40% = 312
+ $this->assertSame(312, $reseller_wallet->balance);
+
+ $transactions = Transaction::where('object_id', $wallet->id)
+ ->where('object_type', \App\Wallet::class)->get();
+ $reseller_transactions = Transaction::where('object_id', $reseller_wallet->id)
+ ->where('object_type', \App\Wallet::class)->get();
+
+ $this->assertCount(1, $reseller_transactions);
+ $trans = $reseller_transactions[0];
+ $this->assertSame("Charged user jane@kolabnow.com", $trans->description);
+ $this->assertSame(312, $trans->amount);
+ $this->assertSame(Transaction::WALLET_CREDIT, $trans->type);
+
+ $this->assertCount(1, $transactions);
+ $trans = $transactions[0];
+ $this->assertSame('', $trans->description);
+ $this->assertSame(-732, $trans->amount);
+ $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
+
+ // TODO: Test entitlement transaction records
+
+ // -----------------------------------
+ // Test charging on entitlement delete
+ // -----------------------------------
+
+ $transactions = Transaction::where('object_id', $wallet->id)
+ ->where('object_type', \App\Wallet::class)->delete();
+ $reseller_transactions = Transaction::where('object_id', $reseller_wallet->id)
+ ->where('object_type', \App\Wallet::class)->delete();
+
+ $user->removeSku($storage, 2);
+
+ // we expect the wallet to have been charged for 19 days of use of
+ // 2 deleted storage entitlements
+ $wallet->refresh();
+ $reseller_wallet->refresh();
+
+ // 2 x round(25 / 31 * 19 * 0.7) = 22
+ $this->assertSame(-(732 + 22), $wallet->balance);
+ // 22 - 2 x round(25 * 0.4 / 31 * 19) = 10
+ $this->assertSame(312 + 10, $reseller_wallet->balance);
+
+ $transactions = Transaction::where('object_id', $wallet->id)
+ ->where('object_type', \App\Wallet::class)->get();
+ $reseller_transactions = Transaction::where('object_id', $reseller_wallet->id)
+ ->where('object_type', \App\Wallet::class)->get();
+
+ $this->assertCount(2, $reseller_transactions);
+ $trans = $reseller_transactions[0];
+ $this->assertSame("Charged user jane@kolabnow.com", $trans->description);
+ $this->assertSame(5, $trans->amount);
+ $this->assertSame(Transaction::WALLET_CREDIT, $trans->type);
+ $trans = $reseller_transactions[1];
+ $this->assertSame("Charged user jane@kolabnow.com", $trans->description);
+ $this->assertSame(5, $trans->amount);
+ $this->assertSame(Transaction::WALLET_CREDIT, $trans->type);
+
+ $this->assertCount(2, $transactions);
+ $trans = $transactions[0];
+ $this->assertSame('', $trans->description);
+ $this->assertSame(-11, $trans->amount);
+ $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
+ $trans = $transactions[1];
+ $this->assertSame('', $trans->description);
+ $this->assertSame(-11, $trans->amount);
+ $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
+
+ // TODO: Test entitlement transaction records
+ }
}
diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php
--- a/src/tests/TestCase.php
+++ b/src/tests/TestCase.php
@@ -11,14 +11,24 @@
protected function backdateEntitlements($entitlements, $targetDate)
{
+ $wallets = [];
+ $ids = [];
+
foreach ($entitlements as $entitlement) {
- $entitlement->created_at = $targetDate;
- $entitlement->updated_at = $targetDate;
- $entitlement->save();
+ $ids[] = $entitlement->id;
+ $wallets[] = $entitlement->wallet_id;
+ }
+
+ \App\Entitlement::whereIn('id', $ids)->update([
+ 'created_at' => $targetDate,
+ 'updated_at' => $targetDate,
+ ]);
+
+ if (!empty($wallets)) {
+ $wallets = array_unique($wallets);
+ $owners = \App\Wallet::whereIn('id', $wallets)->pluck('user_id')->all();
- $owner = $entitlement->wallet->owner;
- $owner->created_at = $targetDate;
- $owner->save();
+ \App\User::whereIn('id', $owners)->update(['created_at' => $targetDate]);
}
}

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 10:31 AM (1 d, 10 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18808866
Default Alt Text
D2446.1775212312.diff (48 KB)

Event Timeline