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 ![](https://i.imgur.com/40YOYYj.png) - Modal Popup for adding/editing deposit ![](https://i.imgur.com/SuvLedE.png) - Display deposit in Invoice Link ![](https://i.imgur.com/Q8W0Sya.png) - Display deposit comment in Payment Transaction ![](https://i.imgur.com/I8TmqF8.png) ### 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">&times;</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(); } ```