From 24bca2c8e96ab5fdbfbfe42220f7d9a52ee794ca Mon Sep 17 00:00:00 2001 From: abijah Date: Mon, 27 Jul 2009 04:18:38 +0000 Subject: [PATCH] Implemented a mechanism to track customer overpayments (credits). Also implemented a reconciling algorithm, matching payments to charges. Preliminary testing seems to show that it works well. More thorough testing required. git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@392 97e9348a-65ac-dc4b-aefc-98561f571b83 --- app_model.php | 3 + models/customer.php | 2 +- models/statement_entry.php | 199 ++++++++++++++++++++++++++++++++++++- 3 files changed, 202 insertions(+), 2 deletions(-) diff --git a/app_model.php b/app_model.php index 9aa4200..83eca67 100644 --- a/app_model.php +++ b/app_model.php @@ -100,6 +100,9 @@ class AppModel extends Model { $query['link'] = array(); if (!$link && !isset($query['contain'])) $query['contain'] = array(); + + // In case caller expects query to come back + return $query; } diff --git a/models/customer.php b/models/customer.php index d3568b4..57f0036 100644 --- a/models/customer.php +++ b/models/customer.php @@ -395,7 +395,7 @@ class Customer extends AppModel { $ar_transaction_stats += $ar_transaction_stats['LedgerEntry']; pr(compact('ar_transaction_stats')); - $stats = $statement_stats; + $stats = $ar_transaction_stats; return $stats; } diff --git a/models/statement_entry.php b/models/statement_entry.php index 681dafc..d7a97f2 100644 --- a/models/statement_entry.php +++ b/models/statement_entry.php @@ -39,7 +39,7 @@ class StatementEntry extends AppModel { ($sum ? ')' : '') . ' AS charge' . ($sum ? 's' : ''), ($sum ? 'SUM(' : '') . - "IF({$entry_name}.type = 'PAYMENT'," . + "IF({$entry_name}.type = 'PAYMENT' OR {$entry_name}.type = 'CREDIT'," . " {$entry_name}.amount, NULL)" . ($sum ? ')' : '') . ' AS payment' . ($sum ? 's' : ''), @@ -347,6 +347,203 @@ OPTION 2 } + /************************************************************************** + ************************************************************************** + ************************************************************************** + * function: assignCredits + * - Assigns all credits to existing charges + */ + function assignCredits($query = null, $receipt_id = null) { + pr(array('StatementEntry::assignCredits' => compact('query'))); + $this->queryInit($query); + + // First, find all known credits + $lquery = $query; + $lquery['conditions'][] = array('StatementEntry.type' => 'CREDIT'); + $credits = $this->find('all', $lquery); + pr(compact('lquery', 'credits')); + + // Then, find all receipts that have not had all + // monies dispursed for either payments or credits + // REVISIT : If we implement CREDITS as we're + // anticipating, then this concept of "anonymous" + // credits won't exists (i.e. credits that are + // not explicitly specified with a statement entry + // of type CREDIT). All transactions MUST balance + // out to the sum of their statement entries, so + // we'll be able to just delete all the anon_credit + // code. + $lquery = $query; + $lquery['link'] = array('StatementEntry' => array('fields' => array()) + $lquery['link']); + $lquery['conditions'][] = array('Transaction.type' => 'RECEIPT'); + $lquery['fields'] = array('Transaction.id', 'Transaction.stamp', 'Transaction.amount', + //'SUM(StatementEntry.amount) AS applied_amount', + 'Transaction.amount - SUM(StatementEntry.amount) AS balance'); + $lquery['group'] = 'Transaction.id HAVING balance > 0'; + $anon_credits = $this->Transaction->find('all', $lquery); + foreach ($anon_credits AS &$ac) { + $ac['Transaction'] += $ac[0]; + unset($ac[0]); + } + pr(compact('lquery', 'anon_credits')); + + // REVISIT : 20090726 + // This algorithm shouldn't be hardcoded. We need to allow + // the user to specify how payments should be applied. + + // Now find all unpaid charges + $lquery = $query; + $lquery['order'] = 'StatementEntry.effective_date ASC'; + $charges = $this->reconciledSet('CHARGE', $query, true); + pr(compact('lquery', 'charges')); + + // Initialize our list of used credits + $used_credits = array(); + $used_anon_credits = array(); + + // Work through all unpaid charges, applying payments as we go + foreach ($charges['entries'] AS $charge) { + + pr(array('StatementEntry::assignCredits' => + array('checkpoint' => 'Process Charge') + + compact('charge'))); + + // Check that we have available credits. + // Technically, this isn't necessary, since the loop + // will handle everything just fine. However, this + // just saves extra processing if/when there is no + // means to resolve a charge anyway. + if (count($credits) == 0 && count($anon_credits) == 0) { + pr(array('StatementEntry::assignCredits' => + array('checkpoint' => 'No available credits'))); + break; + } + + $charge['balance'] = $charge['StatementEntry']['balance']; + while ($charge['balance'] > 0 && + (count($credits) || count($anon_credits))) { + + pr(array('StatementEntry::assignCredits' => + array('checkpoint' => 'Attempt Charge Reconciliation') + + compact('charge'))); + + // Use explicit credits before using implicit credits + // (Not sure it matters though). + if (count($credits)) { + // Peel off the first credit available + $credit =& $credits[0]; + $payment_date = $credit['StatementEntry']['effective_date']; + $payment_transaction_id = $credit['StatementEntry']['transaction_id']; + + if (!isset($credit['balance'])) + $credit['balance'] = $credit['StatementEntry']['amount']; + } + elseif (count($anon_credits)) { + // Peel off the first credit available + $credit =& $anon_credits[0]; + $payment_date = $credit['Transaction']['stamp']; + $payment_transaction_id = $credit['Transaction']['id']; + + if (!isset($credit['balance'])) + $credit['balance'] = $credit['Transaction']['balance']; + + } + else { + die("HOW DID WE GET HERE WITH NO CREDITS?"); + } + + // Set the payment amount to the maximum amount + // possible without exceeding the charge or credit balance + $payment_amount = min($charge['balance'], $credit['balance']); + if (!isset($credit['applied'])) + $credit['applied'] = 0; + + $credit['applied'] += $payment_amount; + $credit['balance'] -= $payment_amount; + + pr(array('StatementEntry::assignCredits' => + array('checkpoint' => (($credit['balance'] > 0 ? 'Utilized' : 'Exhausted') + . (count($credits) ? '' : ' Anon') + . ' Credit')) + + compact('credit'))); + + if ($credit['balance'] < 0) + die("HOW DID WE END UP WITH NEGATIVE CREDIT BALANCE?"); + + // If we've exhaused the credit, get it out of the + // available credit pool (but keep track of it for later). + if ($credit['balance'] <= 0) { + if (count($credits)) + $used_credits[] = array_shift($credits); + else + $used_anon_credits[] = array_shift($anon_credits); + } + + // Add a payment that uses the available credit to pay the charge + $payment = array('type' => 'PAYMENT', + 'account_id' => $this->Account->accountReceivableAccountID(), + 'amount' => $payment_amount, + 'effective_date' => $payment_date, + 'transaction_id' => $payment_transaction_id, + 'customer_id' => $charge['StatementEntry']['customer_id'], + 'lease_id' => $charge['StatementEntry']['lease_id'], + 'charge_entry_id' => $charge['StatementEntry']['id'], + 'comment' => null, + ); + + pr(array('StatementEntry::assignCredits' => + array('checkpoint' => 'New Payment Entry') + + compact('payment'))); + + $SE = new StatementEntry(); + $SE->create(); + if (!$SE->save($payment)) + die("UNABLE TO SAVE NEW PAYMENT ENTRY"); + + // Adjust the charge balance to reflect the new payment + $charge['balance'] -= $payment_amount; + if ($charge['balance'] < 0) + die("HOW DID WE GET A NEGATIVE CHARGE AMOUNT?"); + + if ($charge['balance'] <= 0) + pr(array('StatementEntry::assignCredits' => + array('checkpoint' => 'Fully Paid Charge'))); + } + + } + + // Make partially used credits are added to the used list + if (isset($credits[0]['applied'])) + $used_credits[] = array_shift($credits); + if (isset($anon_credits[0]['applied'])) + $used_anon_credits[] = array_shift($anon_credits); + + pr(array('StatementEntry::assignCredits' => + array('checkpoint' => 'Payments added') + + compact('credits', 'used_credits', 'anon_credits', 'used_anon_credits'))); + + // Finally, clean up any credits that have been used + foreach ($used_credits AS $credit) { + if ($credit['balance'] > 0) { + pr(array('StatementEntry::assignCredits' => + array('checkpoint' => 'Update Credit Entry') + + compact('credit'))); + + $this->id = $credit['StatementEntry']['id']; + $this->saveField('amount', $credit['balance']); + } + else { + pr(array('StatementEntry::assignCredits' => + array('checkpoint' => 'Delete Exhausted Credit Entry') + + compact('credit'))); + + $this->del($credit['StatementEntry']['id'], false); + } + } + + } + + /************************************************************************** ************************************************************************** **************************************************************************