I'm working on an accounting software, where I have to create the voucher then that voucher should be paid instantly after creation, mean the transaction against that voucher should be made as debit from one account and credit to another account.
So I have made the two tables as.
I have made the model, controller for each and added CRUD operation methods.
I have created the API for both and consuming the API for CRUD operations.
So the real problem starts now.
Against one voucher two transactions will be happen. Example as.
Voucher Record:
id = auto_generated, amount = 500, from_account_id = 8, to_account_id = 9
So against this voucher I have to record two transactions as below.
Transaction Record:
id = auto_generated, debit = 500, credit = 0, account_id = 8
id = auto_generated, debit = 0, credit = 500, account_id = 9.
Now i'm handling the above totally manually, mean I'm adding first the voucher then create the two transaction object and assign the value based on the data.
So I'm writing code for 3 models in store
method of voucher. so I have to re-write all the code for transaction adding in the voucher store
method. So is there any convenient and less code/error prone to automatically call both transactions against that voucher?
I want also the same process on the update, let suppose I have updated my voucher later, so the transaction behind that should be automatically updated.
You should treat your controller as one interface of many, ideally your controller should only care about anything that is HTTP specific. When you're writing code in your controller think "will I ever need this functionality outside of this controller?" and if the answer is yes then the functionality belongs elsewhere.
In this case you're talking about behaviour of your model, you're saying "Vouchers (your model) should be paid (behaviour)". This is a good sign that the behaviour belongs in your model as a method that you can call, either on creation (using events) or externally on demand. For example in App\Voucher.php
:
/**
* Account that is debited by the voucher.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function from(): HasOne
{
return $this->hasOne(Account::class, 'id', 'from_account_id');
}
/**
* Account that is credited by the voucher.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function to(): HasOne
{
return $this->hasOne(Account::class, 'id', 'to_account_id');
}
/**
* Credit the voucher amount.
*
* @return void
*/
public function pay(): void
{
$this->from()->transactions()->create([
'debit' => $this->amount,
'credit' => 0,
]);
$this->to()->transactions()->create([
'debit' => 0,
'credit' => $this->amount,
]);
}
Then your controller method would look something like this:
/**
* Create the Voucher and pay it out.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\RedirectResponse
*/
public function store(Request $request): RedirectResponse
{
$voucher = Voucher::create([
'from_account_id' => $request->from,
'to_account_id' => $request->to,
'amount' => $request->amount,
]);
$voucher->pay();
return redirect()->route('vouchers.index')->with([
'success' => "{$request->amount} has been credited to your recipient."
]);
}
Then any other time you need to pay a voucher, you can just call the pay
method on it. If the only time you want a voucher to be paid is on creation you can set the pay
method to protected
instead of public
and then use an Eloquent event listener to trigger it on the created
event and prevent it from being used any other time.
Regarding updating vouchers later on: it would be a mistake to allow vouchers to be mutable but if you must do it then you should still treat transactions as immutable, meaning any change in the voucher should create new transactions with the recipient and sender that would equal out the amounts, you should never modify historic transaction data in an accounting system.
Note: if I was designing this I almost certainly wouldn't do it this way but I'd need to better understand your applications purpose and functionality to design it properly, so what I've given here is a best effort based on what you've shared but not the perfect solution.