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
This commit is contained in:
abijah
2009-07-27 04:18:38 +00:00
parent 2cc58255c3
commit 24bca2c8e9
3 changed files with 202 additions and 2 deletions

View File

@@ -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;
}

View File

@@ -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 <AP>: 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 <AP>: 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);
}
}
}
/**************************************************************************
**************************************************************************
**************************************************************************