Changeset View
Standalone View
src/app/Wallet.php
<?php | <?php | ||||
namespace App; | namespace App; | ||||
use App\User; | use App\User; | ||||
use App\Traits\SettingsTrait; | use App\Traits\SettingsTrait; | ||||
use Carbon\Carbon; | use Carbon\Carbon; | ||||
use Iatstuti\Database\Support\NullableFields; | use Iatstuti\Database\Support\NullableFields; | ||||
use Illuminate\Database\Eloquent\Model; | use Illuminate\Database\Eloquent\Model; | ||||
use Illuminate\Support\Facades\DB; | |||||
/** | /** | ||||
* The eloquent definition of a wallet -- a container with a chunk of change. | * The eloquent definition of a wallet -- a container with a chunk of change. | ||||
* | * | ||||
* A wallet is owned by an {@link \App\User}. | * A wallet is owned by an {@link \App\User}. | ||||
* | * | ||||
* @property integer $balance | * @property integer $balance | ||||
*/ | */ | ||||
Show All 37 Lines | public function addController(User $user) | ||||
if (!$this->controllers->contains($user)) { | if (!$this->controllers->contains($user)) { | ||||
$this->controllers()->save($user); | $this->controllers()->save($user); | ||||
} | } | ||||
} | } | ||||
public function chargeEntitlements($apply = true) | public function chargeEntitlements($apply = true) | ||||
{ | { | ||||
$charges = 0; | $charges = 0; | ||||
$discount = $this->discount ? $this->discount->discount : 0; | $discount = $this->getDiscountRate(); | ||||
$discount = (100 - $discount) / 100; | |||||
DB::beginTransaction(); | |||||
// used to parent individual entitlement billings to the wallet debit. | |||||
$entitlementTransactions = []; | |||||
foreach ($this->entitlements()->get()->fresh() as $entitlement) { | foreach ($this->entitlements()->get()->fresh() as $entitlement) { | ||||
// This entitlement has been created less than or equal to 14 days ago (this is at | // This entitlement has been created less than or equal to 14 days ago (this is at | ||||
// maximum the fourteenth 24-hour period). | // maximum the fourteenth 24-hour period). | ||||
if ($entitlement->created_at > Carbon::now()->subDays(14)) { | if ($entitlement->created_at > Carbon::now()->subDays(14)) { | ||||
continue; | continue; | ||||
} | } | ||||
// This entitlement was created, or billed last, less than a month ago. | // This entitlement was created, or billed last, less than a month ago. | ||||
if ($entitlement->updated_at > Carbon::now()->subMonthsWithoutOverflow(1)) { | if ($entitlement->updated_at > Carbon::now()->subMonthsWithoutOverflow(1)) { | ||||
continue; | continue; | ||||
} | } | ||||
// created more than a month ago -- was it billed? | // created more than a month ago -- was it billed? | ||||
machniak: Put it as a first check, it's the less expensive one. | |||||
Done Inline ActionsSure. vanmeeuwen: Sure. | |||||
if ($entitlement->updated_at <= Carbon::now()->subMonthsWithoutOverflow(1)) { | if ($entitlement->updated_at <= Carbon::now()->subMonthsWithoutOverflow(1)) { | ||||
$diff = $entitlement->updated_at->diffInMonths(Carbon::now()); | $diff = $entitlement->updated_at->diffInMonths(Carbon::now()); | ||||
$cost = (int) ($entitlement->cost * $discount * $diff); | $cost = (int) ($entitlement->cost * $discount * $diff); | ||||
$charges += $cost; | $charges += $cost; | ||||
// if we're in dry-run, you know... | // if we're in dry-run, you know... | ||||
if (!$apply) { | if (!$apply) { | ||||
continue; | continue; | ||||
} | } | ||||
$entitlement->updated_at = $entitlement->updated_at->copy()->addMonthsWithoutOverflow($diff); | $entitlement->updated_at = $entitlement->updated_at->copy()->addMonthsWithoutOverflow($diff); | ||||
$entitlement->save(); | $entitlement->save(); | ||||
// TODO: This would be better done out of the loop (debit() will call save()), | if ($cost == 0) { | ||||
// but then, maybe we should use a db transaction | continue; | ||||
$this->debit($cost); | |||||
} | |||||
} | } | ||||
return $charges; | $entitlementTransactions[] = $entitlement->createTransaction( | ||||
\App\Transaction::ENTITLEMENT_BILLED, | |||||
$cost | |||||
); | |||||
} | |||||
} | } | ||||
/** | if ($apply) { | ||||
* The discount assigned to the wallet. | $this->debit($charges, $entitlementTransactions); | ||||
* | |||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo | |||||
*/ | |||||
public function discount() | |||||
{ | |||||
return $this->belongsTo('App\Discount', 'discount_id', 'id'); | |||||
} | } | ||||
/** | DB::commit(); | ||||
* Calculate the expected charges to this wallet. | |||||
* | return $charges; | ||||
* @return int | |||||
*/ | |||||
public function expectedCharges() | |||||
{ | |||||
return $this->chargeEntitlements(false); | |||||
} | } | ||||
/** | /** | ||||
* Remove a controller from this wallet. | * Controllers of this wallet. | ||||
* | |||||
* @param \App\User $user The user to remove as a controller from this wallet. | |||||
* | * | ||||
* @return void | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany | ||||
*/ | */ | ||||
public function removeController(User $user) | public function controllers() | ||||
{ | { | ||||
if ($this->controllers->contains($user)) { | return $this->belongsToMany( | ||||
$this->controllers()->detach($user); | 'App\User', // The foreign object definition | ||||
} | 'user_accounts', // The table name | ||||
'wallet_id', // The local foreign key | |||||
'user_id' // The remote foreign key | |||||
); | |||||
} | } | ||||
/** | /** | ||||
* Add an amount of pecunia to this wallet's balance. | * Add an amount of pecunia to this wallet's balance. | ||||
* | * | ||||
* @param int $amount The amount of pecunia to add (in cents). | * @param int $amount The amount of pecunia to add (in cents). | ||||
* | * | ||||
* @return Wallet Self | * @return Wallet Self | ||||
*/ | */ | ||||
public function credit(int $amount): Wallet | public function credit(int $amount): Wallet | ||||
{ | { | ||||
$this->balance += $amount; | $this->balance += $amount; | ||||
$this->save(); | $this->save(); | ||||
\App\Transaction::create( | |||||
[ | |||||
'user_email' => \App\Utils::userEmailOrNull(), | |||||
'object_id' => $this->id, | |||||
'object_type' => \App\Wallet::class, | |||||
'type' => \App\Transaction::WALLET_CREDIT, | |||||
Not Done Inline ActionsUse a constant here. machniak: Use a constant here. | |||||
'amount' => $amount | |||||
] | |||||
); | |||||
return $this; | return $this; | ||||
} | } | ||||
/** | /** | ||||
* Deduct an amount of pecunia from this wallet's balance. | * Deduct an amount of pecunia from this wallet's balance. | ||||
* | * | ||||
* @param int $amount The amount of pecunia to deduct (in cents). | * @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. | |||||
* @return Wallet Self | * @return Wallet Self | ||||
*/ | */ | ||||
public function debit(int $amount): Wallet | public function debit(int $amount, array $eTIDs = []): Wallet | ||||
Done Inline Actions$eTIDs do not have to be a reference, does it? machniak: $eTIDs do not have to be a reference, does it? | |||||
Done Inline ActionsNo, but why copy if we don't have to? My thinking is that it is potentially a very large list. vanmeeuwen: No, but why copy if we don't have to?
My thinking is that it is potentially a very large list. | |||||
Not Done Inline ActionsPHP is smart. It will not create a copy if you do not modify the local variable. It's called "copy on write". machniak: PHP is smart. It will not create a copy if you do not modify the local variable. It's called… | |||||
{ | { | ||||
if ($amount == 0) { | |||||
return $this; | |||||
} | |||||
$this->balance -= $amount; | $this->balance -= $amount; | ||||
$this->save(); | $this->save(); | ||||
$transaction = \App\Transaction::create( | |||||
Done Inline ActionsWhen displaying transaction log it would be nice to display wallet balance after the operation. My bank for example does this. So, I think it would make sense to store that information with the on-wallet transaction entries (credit/debit/award/penalty). machniak: When displaying transaction log it would be nice to display wallet balance after the operation. | |||||
Done Inline ActionsIf we wanted to in the future, we can derive either the column value or the displayed data by going backwards from "current balance", and I would like to not have to duplicate data that can potentially mismatch (because of rounding or whatever). vanmeeuwen: If we wanted to in the future, we can derive either the column value or the displayed data by… | |||||
Done Inline ActionsYou could, but it would be more complicated/less efficient if you want to archive older entries. machniak: You could, but it would be more complicated/less efficient if you want to archive older entries. | |||||
[ | |||||
'user_email' => \App\Utils::userEmailOrNull(), | |||||
'object_id' => $this->id, | |||||
'object_type' => \App\Wallet::class, | |||||
'type' => \App\Transaction::WALLET_DEBIT, | |||||
Done Inline ActionsA contant here. And Wallet could have it's own createTransaction() method too. machniak: A contant here. And Wallet could have it's own createTransaction() method too. | |||||
'amount' => $amount | |||||
] | |||||
); | |||||
\App\Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]); | |||||
Done Inline ActionsThis probably would be better to do with a single sql query. machniak: This probably would be better to do with a single sql query. | |||||
Done Inline ActionsIs there an $ets->update('transaction_id', $transaction->id); that you know of? I'll investigate, but I would not at this moment consider it a blocker -- and I don't know if $ets->update() would skip observers in a way that is similar to an Something::where('blah', $blah)->delete();. vanmeeuwen: Is there an `$ets->update('transaction_id', $transaction->id);` that you know of?
I'll… | |||||
Done Inline ActionsYes, update() will skip observers, and for this case we don't have observers, so it's all right. You can't call it on $ets here as it's an array, not query builder, so you'd have to build a query using:
machniak: Yes, update() will skip observers, and for this case we don't have observers, so it's all right. | |||||
return $this; | return $this; | ||||
} | } | ||||
/** | /** | ||||
* Controllers of this wallet. | * The discount assigned to the wallet. | ||||
* | * | ||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||||
*/ | */ | ||||
public function controllers() | public function discount() | ||||
{ | { | ||||
return $this->belongsToMany( | return $this->belongsTo('App\Discount', 'discount_id', 'id'); | ||||
'App\User', // The foreign object definition | |||||
'user_accounts', // The table name | |||||
'wallet_id', // The local foreign key | |||||
'user_id' // The remote foreign key | |||||
); | |||||
} | } | ||||
/** | /** | ||||
* Entitlements billed to this wallet. | * Entitlements billed to this wallet. | ||||
* | * | ||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany | * @return \Illuminate\Database\Eloquent\Relations\HasMany | ||||
*/ | */ | ||||
public function entitlements() | public function entitlements() | ||||
{ | { | ||||
return $this->hasMany('App\Entitlement'); | return $this->hasMany('App\Entitlement'); | ||||
} | } | ||||
/** | /** | ||||
* Calculate the expected charges to this wallet. | |||||
* | |||||
* @return int | |||||
*/ | |||||
public function expectedCharges() | |||||
{ | |||||
return $this->chargeEntitlements(false); | |||||
} | |||||
/** | |||||
* Return the exact, numeric version of the discount to be applied. | |||||
* | |||||
* Ranges from 0 - 100. | |||||
* | |||||
* @return int | |||||
*/ | |||||
public function getDiscount() | |||||
{ | |||||
return $this->discount ? $this->discount->discount : 0; | |||||
} | |||||
/** | |||||
* The actual discount rate for use in multiplication | |||||
* | |||||
* Ranges from 0.00 to 1.00. | |||||
*/ | |||||
public function getDiscountRate() | |||||
{ | |||||
return (100 - $this->getDiscount()) / 100; | |||||
} | |||||
/** | |||||
* The owner of the wallet -- the wallet is in his/her back pocket. | * The owner of the wallet -- the wallet is in his/her back pocket. | ||||
* | * | ||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||||
*/ | */ | ||||
public function owner() | public function owner() | ||||
{ | { | ||||
return $this->belongsTo('App\User', 'user_id', 'id'); | return $this->belongsTo('App\User', 'user_id', 'id'); | ||||
} | } | ||||
/** | /** | ||||
* Payments on this wallet. | * Payments on this wallet. | ||||
* | * | ||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany | * @return \Illuminate\Database\Eloquent\Relations\HasMany | ||||
*/ | */ | ||||
public function payments() | public function payments() | ||||
{ | { | ||||
return $this->hasMany('App\Payment'); | return $this->hasMany('App\Payment'); | ||||
} | } | ||||
/** | /** | ||||
* Remove a controller from this wallet. | |||||
* | |||||
* @param \App\User $user The user to remove as a controller from this wallet. | |||||
* | |||||
* @return void | |||||
*/ | |||||
public function removeController(User $user) | |||||
{ | |||||
if ($this->controllers->contains($user)) { | |||||
$this->controllers()->detach($user); | |||||
} | |||||
} | |||||
/** | |||||
* Any (additional) properties of this wallet. | * Any (additional) properties of this wallet. | ||||
* | * | ||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany | * @return \Illuminate\Database\Eloquent\Relations\HasMany | ||||
*/ | */ | ||||
public function settings() | public function settings() | ||||
{ | { | ||||
return $this->hasMany('App\WalletSetting'); | return $this->hasMany('App\WalletSetting'); | ||||
} | } | ||||
/** | |||||
* Retrieve the transactions against this wallet. | |||||
* | |||||
* @return iterable \App\Transaction | |||||
*/ | |||||
public function transactions() | |||||
{ | |||||
return \App\Transaction::where( | |||||
[ | |||||
'object_id' => $this->id, | |||||
'object_type' => \App\Wallet::class | |||||
] | |||||
)->orderBy('created_at')->get(); | |||||
} | |||||
} | } |
Put it as a first check, it's the less expensive one.