mathewparet's avatar

Accounting in the modern age

I am building a wallet in my Laravel application and need to track transactions (recharging of wallet, payment for services, refunds, etc).

I know a bit of double-entry accounting - this is what I understand so far:

Mary buys a service worth $100 using her wallet at one of our stores. This creates the below transactions in terms of double entry:

  1. deduct 100 from Mary's account
  2. add 100 to store's account

So if the accountant forgets one of the transactions we can identify it immediately.

However, if it was a single entry accounting transaction, we wouldn't know of the "mistake" immediately.

This holds good when things used to be done using pen and paper. But in the digital world, we do not require the accountant to enter both. We get one input and the application updates both entries in a single transaction (making it effectively double-entry transaction).

But is this needed? In the digital world I could use a table like this:

id, amount, from_account_id, to_acocunt_id

thereby making a single row entry. So do we really need double-entry accounting in the digital world?!

0 likes
9 replies
Dalma's avatar

In the double entry accounting world you have balancing equations such as:

Assets = Liabilities + Owners Equity (where Owners Equity = Capital + Revenue - Expenses)

In this world your postings always abide by this balancing equation. The double entry system makes sense and is required when you have a full accounting system. When you are just looking at a subset of this system then the double entry does not directly apply

If you are looking to only make one entry to some form of ledger this can be sufficient but keep in mind that people always want to know several items when it comes to status reporting.

What was my original balance, what deposits were made, what payment were withdrawn and what is my daily running balance and current balance.

lostdreamer_nl's avatar

Anything accounting related I would do it like the banks do..... Use event sourcing. (checkout Spatie Event Sourcing package)

Basically it comes down to this:

  • Mary buys a service worth $ 100
  • Website triggers event: BuyItem($mary, $service)
  • Event Projector checks if Mary has enough credit (and is allowed to buy service X ?)
  • Event Projector takes 100 from Mary and adds it to Store account
  • if you want logs, the Event Projector would also add the line to your transaction table
  • The event is never deleted (it is your audit trail as well).

Now, when you want to proof that anyone's credit is actually correct, you can simply drop all your data except for the event store, and re-process the events, which will come out on the same credit score.

Auditors love this.... especially when the event store is written to a 'write once' medium.

mathewparet's avatar

@LOSTDREAMER_NL - Just trying to understand. How different is using Spatie Event Sourcing package compared to maintaining a transactions table to store each transaction?

jlrdw's avatar

I have written bookkeeping custom apps. The bottom line is:

A small business is not interested in double entry accounting. You have to write apps similar to "Quickbooks". In the background the double entry transactions are there, but to the user it looks like an easy single entry system.

Usually a good accounting of expenses and income is enough for a small business, where an accounting firm gets their reports at year end.

Of course many large companies have internal full CPA's anyway.

1 like
lostdreamer_nl's avatar

the transaction table would be the output of the events.

In this single case, they amount to the same thing as the transaction is the event, but take this example for instance :

Throughout the year, Mary buys that service 3x but somewhere in the year the store account has changed bank account.

If you are doing any updates in your DB (like the bank account info of the store), you just lost a lot of important info, like which of the transactions have gone to which bank account. Unless ofcourse, you also add a sort of audit table on that as well.

In event sourcing terms, the update to the stores bank info would also have been a event that got handled and resulted in updating the bank_account field on the stores table, but now you never lost any data.

So you still have your regular mysql structure, but insert / delete / updating it goes through the events so you never actually lose information.

(This is why It's also usefull for getting any statistics out of your system after it was build. You simply add new listeners and replay your event store)

It might be overkill for this project, but certainly something interesting to look into.

martinbean's avatar

@mathewparet Stripe have a “balance transaction“ entity (https://stripe.com/docs/api/balance/balance_transaction) that records credits and debits for each account. So when moving money from one account to another, you’d create two balance transaction instances: one for the debit in account A, and one for the credit to account B.

You can wrap this up in a job, and you’ll also need to wrap it in a database transaction so either both the balance transactions are created, or none at all are created.

class MoveFunds
{
    use Dispatchable;

    public function __construct(int $amount, Account $source, Account $destination)
    {
        $this->amount = $amount;
        $this->source = $source;
        $this->destination = $destination;
    }

    public function handle()
    {
        DB::connection(function () {
            $this->createCredit();
            $this->createDebit();
        });
    }

    private function createCredit()
    {
        $this->destination->credit($this->amount);
    }

    private function createDebit()
    {
        $this->source->debit($this->amount);
    }
}
class Account extends Model
{
    public function balanceTransactions()
    {
        return $this->hasMany(BalanceTransaction::class);
    }

    public function credit(int $amount)
    {
        return $this->balanceTransactions()->create([
            'amount' => abs($amount),
        ]);
    }

    public function debit(int $amount)
    {
        return $this->balanceTransactions()->create([
            'amount' => abs($amount) * -1,
        ]);
    }
}
1 like
mathewparet's avatar

@MARTINBEAN - So that means modern banking transactions do use double-entry :). Thank you for the information. Of all the answers you have been able to answer to the point :)

1 like
mathewparet's avatar

@LOSTDREAMER_NL - Please bear with me for some more time.

Definitely, this is an interesting way of doing it. I had never thought of it. Now I have a couple more questions.

  1. I understand that if I delete accounts and replay it will be recreated. But what if the data loss (in accounts) is just partial? Hopefully, the library considers that.

  2. Here all transactions are triggered using events, which means it may not be real time if there are too many transactions happening at the same time. Consider the scenario:

Mary buys something for $100 from Store A. Within minutes she buys something else for $100 from Store B.

She had a balance of only $100 in the beginning.

Since the actual transaction happens in an event, and if the event is executed a delayed, then when she tries to buy something from Store B her balance would still show $100, isn't it, i.e if I use QueuedProjector interface.

lostdreamer_nl's avatar
  1. During a replay, the original table would be truncated, so the partial loss would become a complete loss, followed by a complete rebuild

  2. For exactly this reason I've never used the QueuedProjector, my events for these types of systems are always sync (you could also do partial sync / async events, but it would need careful planning about which events can be async)

Please or to participate in this conversation.