diff --git a/app/Database/migrations/2018_02_28_231807_create_journal_transactions_table.php b/app/Database/migrations/2018_02_28_231807_create_journal_transactions_table.php new file mode 100644 index 00000000..6ede1c32 --- /dev/null +++ b/app/Database/migrations/2018_02_28_231807_create_journal_transactions_table.php @@ -0,0 +1,45 @@ +char('id', 36)->unique(); + $table->char('transaction_group', 36)->nullable(); + $table->integer('journal_id'); + $table->unsignedBigInteger('debit')->nullable(); + $table->unsignedBigInteger('credit')->nullable(); + $table->char('currency', 5); + $table->text('memo')->nullable(); + $table->text('tags')->nullable(); + $table->char('ref_class', 32)->nullable(); + $table->integer('ref_class_id')->nullable(); + $table->timestamps(); + $table->dateTime('post_date'); + $table->softDeletes(); + + $table->index('transaction_group'); + $table->index('journal_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('journal_transactions'); + } +} diff --git a/app/Database/migrations/2018_02_28_231813_create_journals_table.php b/app/Database/migrations/2018_02_28_231813_create_journals_table.php new file mode 100644 index 00000000..ac0c500f --- /dev/null +++ b/app/Database/migrations/2018_02_28_231813_create_journals_table.php @@ -0,0 +1,36 @@ +increments('id'); + $table->unsignedInteger('ledger_id')->nullable(); + $table->bigInteger('balance'); + $table->char('currency', 5); + $table->char('morphed_type', 32); + $table->integer('morphed_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('journals'); + } +} diff --git a/app/Database/migrations/2018_02_28_232438_create_ledgers_table.php b/app/Database/migrations/2018_02_28_232438_create_ledgers_table.php new file mode 100644 index 00000000..3b755349 --- /dev/null +++ b/app/Database/migrations/2018_02_28_232438_create_ledgers_table.php @@ -0,0 +1,33 @@ +increments('id'); + $table->string('name'); + $table->enum('type', ['asset', 'liability', 'equity', 'income', 'expense']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('ledgers'); + } +} diff --git a/app/Models/Journal.php b/app/Models/Journal.php new file mode 100644 index 00000000..57b1578f --- /dev/null +++ b/app/Models/Journal.php @@ -0,0 +1,388 @@ +morphTo(); + } + + /** + * @internal Journal $journal + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + protected static function boot() + { + static::created(function (Journal $journal) { + $journal->resetCurrentBalances(); + }); + + parent::boot(); + } + + /** + * Relationship + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function ledger() + { + return $this->belongsTo(Ledger::class); + } + + /** + * Relationship + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function transactions() + { + return $this->hasMany(JournalTransaction::class); + } + + /** + * @param string $currency + */ + public function setCurrency($currency) + { + $this->currency = $currency; + } + + /** + * @param Ledger $ledger + * @return Journal + */ + public function assignToLedger(Ledger $ledger) + { + $ledger->journals()->save($this); + return $this; + } + + /** + * + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function resetCurrentBalances() + { + $this->balance = $this->getBalance(); + $this->save(); + } + + /** + * @param $value + * @return Money + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getBalanceAttribute($value): Money + { + return new Money($value); + } + + /** + * @param $value + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function setBalanceAttribute($value): void + { + $value = ($value instanceof Money) + ? $value + : new Money($value); + + $this->attributes['balance'] = $value ? (int)$value->getAmount() : null; + } + + /** + * Get the debit only balance of the journal based on a given date. + * @param Carbon $date + * @return Money + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getDebitBalanceOn(Carbon $date): Money + { + $balance = $this->transactions() + ->where('post_date', '<=', $date) + ->sum('debit') ?: 0; + + return new Money($balance); + } + + /** + * @param Journal $object + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function transactionsReferencingObjectQuery($object) + { + return $this + ->transactions() + ->where('ref_class', \get_class($object)) + ->where('ref_class_id', $object->id); + } + + /** + * Get the credit only balance of the journal based on a given date. + * @param Carbon $date + * @return Money + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getCreditBalanceOn(Carbon $date) + { + $balance = $this->transactions() + ->where('post_date', '<=', $date) + ->sum('credit') ?: 0; + + return new Money($balance); + } + + /** + * Get the balance of the journal based on a given date. + * @param Carbon $date + * @return Money + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getBalanceOn(Carbon $date) + { + return $this->getCreditBalanceOn($date) + ->subtract($this->getDebitBalanceOn($date)); + } + + /** + * Get the balance of the journal as of right now, excluding future transactions. + * @return Money + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getCurrentBalance() + { + return $this->getBalanceOn(Carbon::now()); + } + + /** + * Get the balance of the journal. This "could" include future dates. + * @return Money + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getBalance() + { + $balance = $this + ->transactions() + ->sum('credit') - $this->transactions()->sum('debit'); + + return new Money($balance); + } + + /** + * Get the balance of the journal in dollars. This "could" include future dates. + * @return float|int + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getCurrentBalanceInDollars() + { + return $this->getCurrentBalance()->getValue(); + } + + /** + * Get balance + * @return float|int + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getBalanceInDollars() + { + return $this->getBalance()->getValue(); + } + + /** + * @param $value + * @param null $memo + * @param null $post_date + * @return JournalTransaction + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function credit($value, $memo = null, $post_date = null, $transaction_group = null) + { + $value = ($value instanceof Money) + ? $value + : new Money($value); + + return $this->post($value, null, $memo, $post_date, $transaction_group); + } + + /** + * @param $value + * @param null $memo + * @param null $post_date + * @return JournalTransaction + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function debit($value, $memo = null, $post_date = null, $transaction_group = null) + { + $value = ($value instanceof Money) + ? $value + : new Money($value); + + return $this->post(null, $value, $memo, $post_date, $transaction_group); + } + + /** + * @param Money $credit + * @param Money $debit + * @param $memo + * @param Carbon $post_date + * @return JournalTransaction + */ + private function post(Money $credit = null, Money $debit = null, $memo = null, $post_date = null, $transaction_group) + { + $transaction = new JournalTransaction(); + $transaction->credit = $credit ? $credit->getAmount() : null; + $transaction->debit = $debit ? $debit->getAmount() : null; + $currency_code = $credit + ? $credit->getCurrency()->getCode() + : $debit->getCurrency()->getCode(); + + $transaction->memo = $memo; + $transaction->currency = $currency_code; + $transaction->post_date = $post_date ?: Carbon::now(); + $transaction->transaction_group = $transaction_group; + + $this->transactions()->save($transaction); + + return $transaction; + } + + /** + * Credit a journal by a given dollar amount + * @param $value + * @param null $memo + * @param null $post_date + * @return JournalTransaction + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function creditDollars($value, $memo = null, $post_date = null) + { + $value = Money::convertToSubunit($value); + return $this->credit($value, $memo, $post_date); + } + + /** + * Debit a journal by a given dollar amount + * @param $value + * @param null $memo + * @param null $post_date + * @return JournalTransaction + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function debitDollars($value, $memo = null, $post_date = null) + { + $value = Money::convertToSubunit($value); + return $this->debit($value, $memo, $post_date); + } + + /** + * Calculate the dollar amount debited to a journal today + * @return float|int + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getDollarsDebitedToday() + { + $today = Carbon::now(); + return $this->getDollarsDebitedOn($today); + } + + /** + * Calculate the dollar amount credited to a journal today + * @return Money + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getDollarsCreditedToday() + { + $today = Carbon::now(); + return $this->getDollarsCreditedOn($today); + } + + /** + * Calculate the dollar amount debited to a journal on a given day + * @param Carbon $date + * @return Money|float|int + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getDollarsDebitedOn(Carbon $date) + { + $amount = $this->transactions() + ->whereBetween('post_date', [ + $date->copy()->startOfDay(), + $date->copy()->endOfDay() + ]) + ->sum('debit'); + + return new Money($amount); + } + + /** + * Calculate the dollar amount credited to a journal on a given day + * @param Carbon $date + * @return Money + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getDollarsCreditedOn(Carbon $date) + { + $amount = $this + ->transactions() + ->whereBetween('post_date', [ + $date->copy()->startOfDay(), + $date->copy()->endOfDay() + ]) + ->sum('credit'); + + return new Money($amount); + } +} diff --git a/app/Models/JournalTransaction.php b/app/Models/JournalTransaction.php new file mode 100644 index 00000000..32991edf --- /dev/null +++ b/app/Models/JournalTransaction.php @@ -0,0 +1,103 @@ + 'integer', + 'credits' => 'integer', + 'post_date' => 'datetime', + 'tags' => 'array', + ]; + + /** + * + */ + protected static function boot() + { + static::creating(function ($transaction) { + $transaction->id = \Ramsey\Uuid\Uuid::uuid4()->toString(); + }); + + static::saved(function ($transaction) { + $transaction->journal->resetCurrentBalances(); + }); + + static::deleted(function ($transaction) { + $transaction->journal->resetCurrentBalances(); + }); + + parent::boot(); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function journal() + { + return $this->belongsTo(Journal::class); + } + + /** + * @param Model $object + * @return JournalTransaction + */ + public function referencesObject($object) + { + $this->ref_class = \get_class($object); + $this->ref_class_id = $object->id; + $this->save(); + return $this; + } + + /** + * + */ + public function getReferencedObject() + { + if ($classname = $this->ref_class) { + $_class = new $this->ref_class; + return $_class->find($this->ref_class_id); + } + return false; + } + + /** + * @param string $currency + */ + public function setCurrency($currency) + { + $this->currency = $currency; + } +} diff --git a/app/Models/Ledger.php b/app/Models/Ledger.php new file mode 100644 index 00000000..d7276d2e --- /dev/null +++ b/app/Models/Ledger.php @@ -0,0 +1,66 @@ +hasMany(Journal::class); + } + + /** + * Get all of the posts for the country. + */ + public function journal_transctions() + { + return $this->hasManyThrough(JournalTransaction::class, Journal::class); + } + + /** + * + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getCurrentBalance(): Money + { + if ($this->type === 'asset' || $this->type === 'expense') { + $balance = $this->journal_transctions->sum('debit') - $this->journal_transctions->sum('credit'); + } else { + $balance = $this->journal_transctions->sum('credit') - $this->journal_transctions->sum('debit'); + } + + return new Money($balance); + } + + /** + * + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + */ + public function getCurrentBalanceInDollars() + { + return $this->getCurrentBalance()->getValue(); + } +} diff --git a/app/Support/Money.php b/app/Support/Money.php index 6dd7ecef..398f0f91 100644 --- a/app/Support/Money.php +++ b/app/Support/Money.php @@ -39,7 +39,7 @@ class Money public static function convertToSubunit($amount) { $currency = config('phpvms.currency'); - return $amount * config('money.'.$currency.'.subunit'); + return (int) $amount * config('money.'.$currency.'.subunit'); } /** @@ -64,9 +64,20 @@ class Money } /** + * Return the amount of currency in smallest denomination * @return string */ public function getAmount() + { + return $this->money->getAmount(); + } + + /** + * Returns the value in whole amounts, e.g: 100.00 + * vs returning in all cents + * @return float + */ + public function getValue() { return $this->money->getValue(); }