Deposit Management - Partial Payment in Invoice Links
===
Documentation
---
### Business Rules
* If deposit is unpaid - Can’t add new deposit.
* Only one deposit at the time. If paid it allows to add new deposit.
* If it has deposit: on invoice link show the deposit amount (% of the balance due).
* If it has valid entry of deposit in the order - In Invoice link: Show deposit amount + what the deposit is. For example:
```
$1,500 (10% Deposit)
```
* Paid invoice - hide Deposit option
* Payment Transactions add comment in front of payment method, example:
```
11/09/2021 Stripe (10% Deposit) $50.00
```
Implementation
---
### Database
**Table** `order_deposits`
**Columns**
* id `int, not null, auto_increment`
* order_id `int`
* percent `float`
* amount_deposit `float`
* status `varchar`
* created_at `int`
* updated_at `int`
### System Features
- Deposit Management in View Order page

- Modal Popup for adding/editing deposit

- Display deposit in Invoice Link

- Display deposit comment in Payment Transaction

### Code
**Migration**
- Adding `order_deposit` table and columns
```php=
class Create_order_deposits_table
{
function up()
{
// fill up migration
\DBUtil::create_table('order_deposits', array(
'id' => array('type' => 'int', 'null' => false, 'auto_increment' => true, 'constraint' => '11'),
'percent' => array('type' => 'float', 'null' => true, 'default' => 0),
'label' => array('type' => 'varchar', 'null' => true, 'default' => null, 'constraint' => 255),
'status' => array('type' => 'varchar', 'null' => true, 'default' => null, 'constraint' => 255),
'created_at' => array('type' => 'int', 'null' => true, 'default' => 0),
'updated_at' => array('type' => 'int', 'null' => true, 'default' => 0),
), array('id'));
DB::query('ALTER TABLE `order_types`
ENGINE = InnoDB ;
')->execute();
}
function down()
{
// fill down migration
\DBUtil::drop_table('order_deposits');
}
}
```
- Adding `order_id` in `order_deposits` columns
```php=
class Add_order_id_to_order_deposits_table
{
function up()
{
// fill up migration
\DB::query('ALTER TABLE `order_deposits`
ADD COLUMN `order_id` INT(11) NULL AFTER `id`,
ADD INDEX `fk_order_deposits_order_id_idx` (`order_id` ASC);
')->execute();
DB::query('ALTER TABLE `order_deposits`
ADD CONSTRAINT `fk_order_deposits_order_id`
FOREIGN KEY (`order_id`)
REFERENCES `orders` (`id`)
ON DELETE SET NULL
ON UPDATE NO ACTION;
')->execute();
}
function down()
{
// fill down migration
\DB::query('ALTER TABLE `order_deposits`
DROP FOREIGN KEY `fk_order_deposits_order_id`;
')->execute();
DB::query('ALTER TABLE `order_deposits`
DROP COLUMN `order_id`,
DROP INDEX `fk_order_deposits_order_id_idx`;
')->execute();
}
}
```
**Model**
- Order Deposits Model
```php=
/**
* Class OrderDeposits
* @property $id
* @property $order_id
* @property $payment_id
* @property $percent
* @property $amount_deposit
* @property $status
* @property $created_at
* @property $updated_at
*/
class OrderDeposits extends \Model_Base
{
use LazyloadTraits;
const DEPOSIT_STATUS_PAID = 'paid';
const DEPOSIT_STATUS_UNPAID = 'unpaid';
// Set the table to use
protected static $_table_name = 'order_deposits';
// List of all columns that will be used on create/update
protected static $_properties = array(
'id',
'order_id',
'payment_id',
'percent',
'amount_deposit',
'status',
'created_at',
'updated_at',
);
protected static $_created_at = 'created_at';
protected static $_updated_at = 'updated_at';
protected $present_class = OrderDepositsPresenter::class;
public function is_deposit_paid()
{
return $this->status == self::DEPOSIT_STATUS_PAID;
}
/**
* @return OrderDepositsPresenter
*/
public function present()
{
return parent::present();
}
}
```
**View**
- Deposit in View Order page
```htmlembedded=
<div class="row">
<div class="col-md-6">
<?php if (PaymentSettings::forge()->get_enable_partial_payment()) : ?>
<div class="js-deposit-items-container ">
<?php if ($order->payment_status != \Order\Model_Order::PAYMENT_STATUS_PAID): ?>
<h4 class="title-legend mb-15px mt-10px"><?php echo _('Partial Payments for the Customer') ?><i rel="tooltip" class="fa fa-info-circle " data-placement="top" title="<?php echo _('This is the payment due amount that will display on the Invoice Link.') ?>"></i></h4>
<table class="table table-form table-bordered js-deposit-items-table ">
<thead>
<tr>
<th><?php echo _('Deposit Percentage') ?></th>
<th><?php echo _('Amount') ?></th>
<th><?php echo _('Status') ?></th>
<th><?php echo _('Action') ?></th>
</tr>
</thead>
<tbody>
<?php if ($order_deposits): ?>
<?php foreach ($order_deposits as $order_deposit):
/**
* @var $order_deposit \Flex\Models\OrderDeposits
*/
?>
<tr>
<td><?php echo $order_deposit->present()->percent_with_symbol_label(); ?></td>
<td><?php echo $order_deposit->present()->fix_amount_formatted(); ?></td>
<td><span class="label label-success <?php echo $order_deposit->present()->status_label_class(); ?>"><?php echo $order_deposit->present()->status()?></span></td>
<td><a href="javascript:void(0)" data-target-url="<?php echo Uri::create(sprintf("admin/order/get_deposit_modal_form/%s/%s", $order->id, $order_deposit->id)); ?>" class="js-update-deposit <?php echo $order_deposit->is_deposit_paid() ? 'disabled' : ''; ?>">Edit</a><a class="text-danger confirmation-pop-up" data-message="<?php echo $order_deposit->present()->delete_confirmation_message(); ?>" href="javascript:void(0)" data-target-url="<?php echo \Uri::create('admin/order/delete_deposit/' . $order_deposit->id); ?>"><?php echo _('Delete'); ?></a></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td class="center"><strong><?php echo _('There are no deposits.'); ?></strong></td> </tr>
<?php endif; ?>
</tbody>
</table>
<div class="clearfix">
<div class="pull-right">
<a href="javascript:void(0)" data-target-url="<?php echo Uri::create(sprintf("admin/order/get_deposit_modal_form/%s", $order->id)); ?>" class="btn btn-default js-update-deposit <?php echo $order->check_if_has_unpaid_deposit() ? 'disabled' : ''; ?>">
<i class="fa fa-plus"></i> <?php echo _('Add New Payment Amount'); ?></a>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
```
- Create/Update deposit Modal
- Modal Container
```htmlembedded=
<div class="modal fade js-deposit-form-modal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title"><!--Dynamically loaded from ajax--></h4>
</div>
<div class="modal-body js-deposit-form-wrapper">
<!--Dynamically loaded from ajax-->
</div>
</div>
</div>
</div>
```
- Modal Form
```htmlembedded=
<?php
/**
* @var $order_deposit \Flex\Models\OrderDeposits
*/
echo \Form::open(array(
'action' => Uri::create('admin/order/update_deposit'),
'method' => 'post',
));
?>
<?php echo \Form::csrf() ?>
<div class="row">
<div class="col-xs-12">
<div class="panel-body">
<div class="form-horizontal">
<input type="hidden" name="order_id" value="<?php echo e($order->id); ?>">
<input class="js-order-grand-total" type="hidden" name="order_grand_total" value="<?php echo e($order->get_remaining_balance()); ?>">
<?php if (!$order_deposit->is_new()): ?>
<input type="hidden" name="deposit_id" value="<?php echo e($order_deposit->id); ?>">
<?php endif; ?>
<div class="col-xs-12">
<div class="form-group">
<label class="col-xs-4 control-label force-right"><?php echo _('Deposit Percentage'); ?></label>
<div class="col-xs-8">
<input class="form-control js-deposit-percentage" name="deposit_percentage" value="<?php echo $order_deposit ? $order_deposit->present()->percentage() : ''; ?>"
placeholder="<?php echo _('Deposit'); ?>" required>
</div>
</div>
</div>
<div class="col-xs-12">
<div class="form-group">
<label class="col-xs-4 control-label force-right"><?php echo _('Amount'); ?></label>
<div class="col-xs-8">
<input class="form-control js-fix-amount" value="<?php echo $order_deposit->present()->fix_amount_formatted(); ?>" readonly>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary"><?php echo $order_deposit->is_new() ? _('Add Deposit') : _('Update Deposit'); ?></button>
</div>
<?php echo \Form::close(); ?>
```
- Ajax to call modal and submit data
```javascript=
var depositManagement = function($container){
var form = {
init: function ($form){
var grandTotalAmount = $form.find('.js-order-grand-total'),
getFixAmount = $form.find('.js-fix-amount');
$form.find('.js-deposit-percentage').keyup(function (){
var depositVal = $(this).val();
calculatePercentage = (depositVal / 100 ) * grandTotalAmount.val();
getFixAmount.val(flex.formatMoney(calculatePercentage));
});
},
};
var table = {
init: function (){
$container.find('.js-update-deposit').on('click', function (e) {
e.preventDefault();
var $this = $(this),
depositUrl = $this.data('target-url');
$.ajax({
type: 'get',
dataType: 'json',
url: depositUrl,
success: function (data) {
var $modal = $('.js-deposit-form-modal');
$modal.find('.modal-body').html(data.form_html);
$modal.find('.modal-title').html(data.header_title);
$modal.modal('show');
form.init($modal.find('form'));
},
});
});
}
};
table.init();
};
// Call variable depositManagement
$(function () {
addPaymentForm($('.js-add-payment-form'));
depositManagement($('.js-deposit-items-container'));
});
```
- Display `order_deposit` in Invoice Link Page
```php=
// Declare order_deposit
$order_deposit = $order->get_first_unpaid_deposit();
$amount_to_pay = $order_deposit ? $order_deposit>amount_deposit : $order>get_remaining_balance();
// Set hidden data for deposit
<?php echo \Form::hidden('deposit_id', ($order_deposit ? $order_deposit->id : null), ['class' => 'js-deposit-id']); ?>
<?php echo \Form::hidden('deposit_amount', ($order_deposit ? $order_deposit->amount_deposit : null), ['class' => 'js-deposit-amount']); ?>
```
```htmlembedded=
<!-- Display if has deposit -->
<div class="amount">
<span class="js-amount-to-pay"><?php echo Helper::format_money($amount_to_pay); ?></span>
<?php if ($order_deposit) : ?>
<label class="js-percent-info ml-2"><?php echo e($order_deposit->present()->percent_with_deposit_label()); ?></label>
<?php endif; ?>
</div>
```
- Dynamically display the deposit in js
```javascript=
var paymentDue = data.grand_total - data.total_payments,
amountToPay = paymentDue;
// check if has we allowed deposit
var depositId = $wrapper.find('.js-deposit-id').val(),
depositAmount = $wrapper.find('.js-deposit-amount').val(),
$depositInfo = $wrapper.find('.js-percent-info');
$depositInfo.hide()
if (depositId && depositAmount < paymentDue) {
$depositInfo.show();
amountToPay = depositAmount;
}
$('.js-amount-to-pay').text(flex.formatMoney(amountToPay));
flex.cart.initiateAfterPayFromContainer($wrapper, paymentDue);
```
**Controller**
- Fetch data of deposits by its `order_id`
```php=
public function get_order_deposits()
{
return OrderDeposits::find_by_order_id($this->id);
}
```
- Fetch unpaid order deposits
```php=
/**
* @param false $recache
* @return mixed
*/
public function get_first_unpaid_deposit()
{
$item = $this;
return OrderDeposits::find_one_by(function ($query) use ($item) {
$query->where('order_id', $item->id);
$query->where('status', OrderDeposits::DEPOSIT_STATUS_UNPAID);
});
}
```
- Call ajax and submit data
```php=
public function action_get_deposit_modal_form($order_id, $deposit_id = null)
{
$order = Model_Order::find_one_by_id($order_id);
if (!empty($deposit_id))
{
$order_deposit = OrderDeposits::find_one_by_id($deposit_id);
}
else
{
$order_deposit = OrderDeposits::forge();
}
$form_html = \Theme::instance('admin')->view('views/order/deposit/_modal_form')
->set('order_deposit', $order_deposit, false)
->set('order', $order, false)
->render();
return $this->jsonResponse([
'header_title' => $order_deposit->is_new() ?_('Add Deposit') : _('Edit Deposit'),
'form_html' => $form_html
]);
}
```
- Add/Update the deposit process
```php=
public function action_update_deposit()
{
$post = Input::post();
$order_id = Arr::get($post, 'order_id');
$deposit_id = Arr::get($post, 'deposit_id');
$order = Model_Order::find_one_by_id($order_id);
$order_deposit = OrderDeposits::find_one_by_id($deposit_id);
$redirect_back = \Input::post('_redirect', \Uri::create("admin/order/update/{$order_id}"));
if ($order_id)
{
if ($deposit_id) {
$order_deposit = OrderDeposits::find_one_by_id($deposit_id);
} else {
$order_deposit = OrderDeposits::forge();
}
$is_new = $order_deposit->is_new();
$total_amount = $order->get_remaining_balance();
$deposit_percentage = Arr::get($post, 'deposit_percentage');
$amount_deposit = \OrderHelper::calculate_percentage_deposit($deposit_percentage, $total_amount);
if ($amount_deposit > $total_amount) {
\Messages::error(_('Invalid deposit amount.'));
\Response::redirect($redirect_back);
}
$order_deposit->set([
'percent' => $deposit_percentage,
'order_id' => $order_id,
'amount_deposit' => $amount_deposit,
'status' => OrderDeposits::DEPOSIT_STATUS_UNPAID,
]);
$order_deposit->save();
\Messages::success($is_new ? _('Deposit successfully created.') : _('Deposit successfully updated.'));
Response::redirect($redirect_back);
}
}
```
- Process payment in `action_paynow`
```php=
// override amount based on received deposit
$deposit_id = \Arr::get($post, 'deposit_id');
$order_deposit = OrderDeposits::find_one_by_id($deposit_id);
if ($order_deposit && $order_deposit->order_id == $order->id && $order_deposit->amount_deposit <= $totals->get_balance_due())
{
$amount = \Helper::format_number_plain($order_deposit->amount_deposit);
}
else
{
// deposit is invalid, make it empty
$deposit_id = null;
$amount = $totals->get_balance_due();
}
$data_handler->set_amount($amount);
$payment
->set_data_handler($data_handler)
->save_session([
'amount' => $amount,
'deposit_id' => $deposit_id,
])
->set_inputs(\Input::post())
->set_return_url(\Uri::create(sprintf('order/complete_payment/%s/%s', $order->uuid, $payment_method)))
->set_cancel_url(\Uri::create(sprintf('order/paynow/%s?action=', $order->uuid, 'payment_cancelled')));
$payment->process();
$amount = $payment->get_data_handler()->get_amount();
$this->complete_order($order, $totals, $payment, $amount);
```
- Set comment and deposit status in complete order
```php=
// check if has deposit id included
$session_data = $payment->get_session();
$deposit_id = \Arr::get($session_data, 'deposit_id');
$deposit = OrderDeposits::find_one_by_id($deposit_id);
if ($deposit_id && $deposit) {
$deposit->set([
'status' => OrderDeposits::DEPOSIT_STATUS_PAID,
]);
$deposit->save();
$model_payment = $payment->get_last_payment_created();
// insert comment to payment.
if ($model_payment) {
$model_payment->set([
'comment' => $deposit->present()->percent_with_deposit_label(),
]);
$model_payment->save();
}
}
```
- Set `last_payment_created` in PaymentsAbstract
```php=
/**
* Last model payment created
*
* @var Model_Payment
*/
protected $last_payment_created;
/**
* @inheritdoc
*/
public function set_last_payment_created(Model_Payment $payment)
{
$this->last_payment_created = $payment;
return $this;
}
/**
* @inheritdoc
*/
public function get_last_payment_created()
{
return $this->last_payment_created;
}
```
- Set `last_payment_created` in PaymentsInterface
```php=
/**
* @param Model_Payment $payment
*
* @return mixed
*/
public function set_last_payment_created(Model_Payment $payment);
/**
* @return Model_Payment
*/
public function get_last_payment_created();
```
- And set `last_payment_created` in OrderDataHandler
```php=
public function complete($transaction_id, $create_payment = true)
{
if ($create_payment) {
$payment = $this->create_payment();
$payment->set([
'transaction_id' => $transaction_id,
'status' => Model_Payment::STATUS_COMPLETED,
'response' => 'Ok',
]);
$payment->save();
PaymentTriggers::created($payment);
$this->payment->set_last_payment_created($payment);
}
$order = $this->order;
$order->re_check_payments_status();
}
```