Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117760080
D2446.1775212312.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None
D2446.1775212312.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D2446: Reseller comissioning system
Attached
Detach File
Event Timeline