Files
pmgr/site/models/transaction.php
abijah 90ecbda541 Several minor changes including a couple minor bugfixes.
git-svn-id: file:///svn-source/pmgr/branches/surplus_account_20090815@592 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 02:15:02 +00:00

1224 lines
42 KiB
PHP

<?php
class Transaction extends AppModel {
var $belongsTo = array(
'Customer',
'Account',
'Ledger',
);
var $hasOne = array(
'NsfTender' => array(
'className' => 'Tender',
'foreignKey' => 'nsf_transaction_id',
),
);
var $hasMany = array(
'LedgerEntry' => array(
'dependent' => true,
),
'StatementEntry' => array(
'dependent' => true,
),
'DepositTender' => array(
'className' => 'Tender',
'foreignKey' => 'deposit_transaction_id',
),
'Charge' => array(
'className' => 'StatementEntry',
'conditions' => array('Charge.type' => 'CHARGE')
),
'Disbursement' => array(
'className' => 'StatementEntry',
'conditions' => array('Disbursement.type' => 'DISBURSEMENT')
),
'Debit' => array(
'className' => 'LedgerEntry',
'conditions' => array('Debit.crdr' => 'DEBIT')
),
'Credit' => array(
'className' => 'LedgerEntry',
'conditions' => array('Credit.crdr' => 'CREDIT')
),
);
var $default_log_level = array('log' => 30, 'show' => 15);
//var $max_log_level = 10;
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addInvoice
* - Adds a new invoice invoice
*/
function addInvoice($data, $customer_id, $lease_id = null) {
$this->prEnter(compact('data', 'customer_id', 'lease_id'));
// Set up control parameters
$data += array('control' => array());
$data['control'] +=
array('assign' => true,
'include_ledger_entry' => true,
'include_statement_entry' => true,
);
// Establish the transaction as an invoice
$data['Transaction'] +=
array('type' => 'INVOICE',
'crdr' => 'DEBIT',
'account_id' => $this->Account->accountReceivableAccountID(),
'customer_id' => $customer_id,
'lease_id' => $lease_id,
);
// Go through the statement entries and flag as charges
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'CHARGE',
);
$ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
if (isset($ids['transaction_id']))
$ids['invoice_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addReceipt
* - Adds a new receipt
*/
function addReceipt($data, $customer_id, $lease_id = null) {
$this->prEnter(compact('data', 'customer_id', 'lease_id'));
// Set up control parameters
$data += array('control' => array());
$data['control'] +=
array('assign' => true,
'assign_receipt' => true,
'include_ledger_entry' => true,
'include_statement_entry' => false,
);
// Establish the transaction as a receipt
$data['Transaction'] +=
array('type' => 'RECEIPT',
'crdr' => 'CREDIT',
'account_id' => $this->Account->accountReceivableAccountID(),
'customer_id' => $customer_id,
'lease_id' => $lease_id,
);
// Go through the statement entries and flag as disbursements
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'DISBURSEMENT', // not used
'account_id' =>
(isset($entry['Tender']['tender_type_id'])
? ($this->LedgerEntry->Tender->TenderType->
accountID($entry['Tender']['tender_type_id']))
: null),
);
$ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
if (isset($ids['transaction_id']))
$ids['receipt_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addWaiver
* - Adds a new waiver
*/
function addWaiver($data, $charge_id, $customer_id, $lease_id = null) {
$this->prEnter(compact('data', 'charge_id', 'customer_id', 'lease_id'));
if (count($data['Entry']) != 1)
$this->INTERNAL_ERROR("Should be one Entry for addWaiver");
// No assignment of credits, as we'll manually assign
// using charge_entry_id as part of the entry (below).
$data += array('control' => array());
$data['control'] +=
array('assign' => false,
'include_ledger_entry' => true,
'include_statement_entry' => true,
);
// Just make sure the disbursement(s) are marked as waivers
// and that they go to cover the specific charge.
$data['Entry'][0] +=
array('type' => 'WAIVER',
'account_id' => $this->Account->waiverAccountID(),
'charge_entry_id' => $charge_id);
// In all other respects this is just a receipt.
$ids = $this->addReceipt($data, $customer_id, $lease_id);
if (isset($ids['transaction_id']))
$ids['waiver_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addWriteOff
* - Adds a new write off of bad debt
*/
function addWriteOff($data, $customer_id, $lease_id = null) {
$this->prEnter(compact('data', 'customer_id', 'lease_id'));
if (count($data['Entry']) != 1)
$this->INTERNAL_ERROR("Should be one Entry for addWriteOff");
// Just make sure the disbursement(s) are marked as write offs
// and that the write-off account is used for the charge.
$data['Entry'][0] +=
array('type' => 'WRITEOFF',
'account_id' => $this->Account->badDebtAccountID());
// In all other respects this is just a receipt.
$ids = $this->addReceipt($data, $customer_id, $lease_id);
if (isset($ids['transaction_id']))
$ids['writeoff_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addDeposit
* - Adds a new bank deposit
*/
function addDeposit($data, $account_id) {
$this->prEnter(compact('data', 'account_id'));
// Set up control parameters
$data += array('control' => array());
$data['control'] +=
array('assign' => false,
'include_ledger_entry' => true,
'include_statement_entry' => false,
);
// Establish the transaction as a deposit
$data['Transaction'] +=
array('type' => 'DEPOSIT',
'crdr' => 'DEBIT',
'account_id' => $account_id,
'customer_id' => null,
'lease_id' => null,
);
// Save the list of IDs, so that we can mark their
// deposit transaction after it has been created.
$tender_ids = array_map(create_function('$item', 'return $item["tender_id"];'),
$data['Entry']);
// Go through the statement entries and re-group by account id
$group = array();
foreach ($data['Entry'] AS &$entry) {
if (!isset($group[$entry['account_id']]))
$group[$entry['account_id']] =
array('account_id' => $entry['account_id'],
'amount' => 0);
$group[$entry['account_id']]['amount'] += $entry['amount'];
}
$data['Entry'] = $group;
$ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
if (isset($ids['transaction_id']))
$ids['deposit_id'] = $ids['transaction_id'];
if (!empty($ids['deposit_id'])) {
$this->LedgerEntry->Tender->updateAll
(array('Tender.deposit_transaction_id' => $ids['deposit_id']),
array('Tender.id' => $tender_ids)
);
}
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addClose
* - Adds a new transaction for closing ledgers
*/
function addClose($data) {
$this->prEnter(compact('data'));
// Set up control parameters
$data += array('control' => array());
$data['control'] +=
array('assign' => false,
'include_ledger_entry' => true,
'include_statement_entry' => false,
);
// Establish the transaction as a close
$data['Transaction'] +=
array('type' => 'CLOSE',
'crdr' => null,
'account_id' => null,
'customer_id' => null,
'lease_id' => null,
);
$ledger_ids = array();
$data['Entry'] = array();
foreach ($data['Ledger'] AS $ledger) {
$ledger_id = $ledger['old_ledger_id'];
$new_ledger_id = $ledger['new_ledger_id'];
$amount = $ledger['amount'];
$account_id = $this->Account->Ledger->accountID($ledger_id);
$crdr = strtoupper($this->Account->fundamentalOpposite($account_id));
$comment = "Ledger Carry Forward (c/f)";
// Save the ledger ID for later, to mark it as closed
$ledger_ids[] = $ledger_id;
// No need to generate ledger entries if there is no balance
if (empty($ledger['amount']) || $ledger['amount'] == 0)
continue;
// Add an entry to carry the ledger balance forward
$data['Entry'][] = compact('account_id', 'ledger_id', 'new_ledger_id',
'crdr', 'amount', 'comment');
}
unset($data['Ledger']);
// Add the transaction and carry forward balances
$ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
if (isset($ids['transaction_id']))
$ids['close_id'] = $ids['transaction_id'];
// Mark the older ledgers as closed
if (!empty($ids['close_id'])) {
$this->LedgerEntry->Ledger->updateAll
(array('Ledger.close_transaction_id' => $ids['close_id']),
array('Ledger.id' => $ledger_ids)
);
}
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addRefund
* - Adds a new refund
*/
function addRefund($data, $customer_id, $lease_id = null) {
$this->prEnter(compact('data', 'customer_id', 'lease_id'));
// Set up control parameters
$data += array('control' => array());
$data['control'] +=
array('assign' => true,
);
// Establish the transaction as a Refund. This is just like a
// Payment, except instead of paying out of the account payable,
// it comes from the customer credit in the account receivable.
// Someday, perhaps we'll just issue a Credit Note or similar,
// but for now, a refund means it's time to actually PAY.
$data['Transaction'] +=
array('account_id' => $this->Account->accountReceivableAccountID());
// Also, to make it clear to the user, we flag as a REFUND
// even though that type works and operates just as PAYMENT
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'REFUND');
$ids = $this->addPayment($data, $customer_id, $lease_id);
if (isset($ids['transaction_id']))
$ids['refund_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addPayment
* - Adds a new payment transaction, which is money outflow
*/
function addPayment($data, $customer_id, $lease_id = null) {
$this->prEnter(compact('data', 'customer_id', 'lease_id'));
// Set up control parameters
$data += array('control' => array());
$data['control'] +=
array('assign' => false,
'include_ledger_entry' => true,
'include_statement_entry' => true,
);
// Establish the transaction as an payment
$data['Transaction'] +=
array('type' => 'PAYMENT',
'crdr' => 'DEBIT',
'account_id' => $this->Account->accountPayableAccountID(),
'customer_id' => $customer_id,
'lease_id' => $lease_id,
);
// Go through the statement entries and flag as payments
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'PAYMENT',
);
$ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
if (isset($ids['transaction_id']))
$ids['payment_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: verifyTransaction
* - Verifies consistenty of new transaction data
* (not in a pre-existing transaction)
*/
function verifyTransaction($transaction, $entries) {
//$this->prFunctionLevel(10);
$this->prEnter(compact('transaction', 'entries'));
// Verify required Transaction data is present
if (empty($transaction['type']) ||
($transaction['type'] != 'CLOSE'
&& (empty($transaction['account_id']) ||
empty($transaction['crdr']))) ||
(in_array($transaction['type'], array('INVOICE', 'RECEIPT'))
&& empty($transaction['customer_id']))
) {
return $this->prReturn(false);
}
// Verify all entries
foreach ($entries AS $entry) {
// Ensure these items are null'ed out so we don't
// accidentally pick up stale data.
$le1 = $le1_tender = $le2 = $se = null;
extract($entry);
if (!empty($le1) && !empty($le2) &&
!$this->LedgerEntry->DoubleEntry->verifyDoubleEntry($le1, $le2, $le1_tender)) {
return $this->prReturn(false);
}
if (!empty($se) &&
!$this->StatementEntry->verifyStatementEntry($se)) {
return $this->prReturn(false);
}
}
return $this->prReturn(true);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addTransaction
* - Adds a new transaction, and the appropriate ledger and statement
* entries, as layed out in the $data['Entry'] array. The array is
* overloaded, since it is used to create both ledger _and_ statement
* entries.
*/
function addTransaction($control, $transaction, $entries) {
$this->prEnter(compact('control', 'transaction', 'entries'));
$result = $this->_splitEntries($control, $transaction, $entries);
if (!empty($result['error']))
return $this->prReturn(array('error' => true));
// Make use of the work done by splitEntries
$transaction = $this->filter_null($transaction) + $result['transaction'];
$entries = $result['entries'];
extract($result['vars']);
$this->pr(20, compact('transaction', 'entries'));
// Move forward, verifying and saving everything.
$ret = array('data' => $transaction);
if (!$this->verifyTransaction($transaction, $entries))
return $this->prReturn(array('error' => true) + $ret);
// Save transaction to the database
$this->create();
if (!$this->save($transaction))
return $this->prReturn(array('error' => true) + $ret);
$ret['transaction_id'] = $transaction['id'] = $this->id;
// Add the entries
$ret += $this->addTransactionEntries($control, $transaction, $entries, false);
// If the caller requests 'assign'=>true, they really
// want to do a credit assignment, and _then_ create
// an explicit credit with any leftover. If an array
// is specified, they get full control of the order.
if (empty($control['assign']))
$assign_ops = array();
elseif (is_array($control['assign']))
$assign_ops = $control['assign'];
elseif (is_bool($control['assign']))
$assign_ops = (empty($control['assign_receipt'])
? array('assign')
: array('assign', 'create'));
else
$this->INTERNAL_ERROR('Invalid control[assign] parameter');
$this->pr(17, compact('assign_ops'), 'Credit operations');
// Go through the requested assignment mechanisms
foreach ($assign_ops AS $method) {
if (!empty($ret['error']))
break;
$this->pr(17, compact('method'), 'Handling credits');
if ($method === 'assign') {
$result = $this->StatementEntry->assignCredits
(null,
(empty($control['assign_receipt']) ? null
: $ret['transaction_id']),
null,
$assign_disbursement_type,
$transaction['customer_id'],
$transaction['lease_id']
);
}
elseif ($method === 'create' || is_numeric($method)) {
if (is_numeric($method))
$credit_amount = $method;
else {
$stats = $this->stats($transaction['id']);
$credit_amount = $stats['undisbursed'];
}
if ($credit_amount < 0)
$this->INTERNAL_ERROR('Receipt has negative undisbursed balance');
if (empty($credit_amount))
continue;
$result = $this->addTransactionEntries
(array('include_ledger_entry' => true,
'include_statement_entry' => true),
array('crdr' => 'DEBIT') + $transaction,
array(array('type' => 'SURPLUS',
'account_id' => $this->Account->customerCreditAccountID(),
'amount' => $credit_amount,
),
));
}
else
$this->INTERNAL_ERROR('Invalid assign method');
$ret['credit'][$method] = $result;
if (!empty($result['error']))
$ret['error'] = true;
}
$this->Customer->update($transaction['customer_id']);
return $this->prReturn($ret);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addTransactionEntries
* - Largely a helper function to addTransaction, this function is
* responsible for adding ledger/statement entries to an existing
* transaction. If needed, this function can also be called outside
* of addTransaction, although it's not clear where that would be
* appropriate, since transactions are really snapshots of some
* event, and shouldn't be mucked with after creation.
*/
function addTransactionEntries($control, $transaction, $entries, $split = true) {
$this->prEnter(compact('control', 'transaction', 'entries', 'split'));
// Verify that we have a transaction
if (empty($transaction['id']))
return $this->prReturn(array('error' => true));
// If the entries are not already split, do so now.
if ($split) {
$result = $this->_splitEntries($control, $transaction, $entries);
if (!empty($result['error']))
return $this->prReturn(array('error' => true));
// Make use of the work done by splitEntries
$transaction = $this->filter_null($transaction) + $result['transaction'];
$entries = $result['entries'];
extract($result['vars']);
/* // Verify the entries */
/* $ret = array(); */
/* if (!$this->verifyTransaction($transaction, $entries)) */
/* return $this->prReturn(array('error' => true) + $ret); */
}
$this->id = $transaction['id'];
$transaction['stamp'] = $this->field('stamp');
$transaction['customer_id'] = $this->field('customer_id');
// Set up our return array
$ret = array();
$ret['entries'] = array();
$ret['error'] = false;
// Go through the entries
foreach ($entries AS $e_index => &$entry) {
// Ensure these items are null'ed out so we don't
// accidentally pick up stale data.
$le1 = $le1_tender = $le2 = $se = null;
extract($entry);
if (!empty($le1) && !empty($le2)) {
$le1['transaction_id'] = $le2['transaction_id'] = $transaction['id'];
if (isset($le1_tender))
$le1_tender['customer_id'] = $transaction['customer_id'];
$result = $this->LedgerEntry->DoubleEntry->addDoubleEntry($le1, $le2, $le1_tender);
$ret['entries'][$e_index]['DoubleEntry'] = $result;
if ($result['error']) {
$ret['error'] = true;
continue;
}
}
if (!empty($se)) {
$se['transaction_id'] = $transaction['id'];
if (empty($se['effective_date']))
$se['effective_date'] = $transaction['stamp'];
$result = $this->StatementEntry->addStatementEntry($se);
$ret['entries'][$e_index]['StatementEntry'] = $result;
if ($result['error']) {
$ret['error'] = true;
continue;
}
}
}
return $this->prReturn($ret);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: _splitEntries
* - An internal helper function capable of splitting an array of
* combined ledger/statement entries into their indivdual entry
* components (2 ledger entries, and a statement entry), based
* on the typical requirements. Any custom split will have to
* be done outside of this function.
*/
function _splitEntries($control, $transaction, $entries) {
$this->prEnter(compact('control', 'transaction', 'entries'));
// Verify that we have a transaction and entries
if (empty($transaction) ||
($transaction['type'] !== 'CLOSE' && empty($entries)))
return $this->prReturn(array('error' => true));
// set ledger ID as the current ledger of the specified account
if (empty($transaction['ledger_id']))
$transaction['ledger_id'] =
$this->Account->currentLedgerID($transaction['account_id']);
// Automatically figure out the customer if we have the lease
if (!empty($transaction['lease_id']) && empty($transaction['customer_id'])) {
$L = new Lease();
$L->id = $transaction['lease_id'];
$transaction['customer_id'] = $L->field('customer_id');
}
if (!empty($transaction['account_id'])) {
if (empty($transaction['ledger_id']))
$transaction['ledger_id'] =
$this->Account->currentLedgerID($transaction['account_id']);
if (empty($transaction['crdr']))
$transaction['crdr'] = strtoupper($this->Account->fundamentalType
($transaction['account_id']));
}
// Some transactions do not have their statement entries
// generated directly as part of the transaction, but are
// created in the final steps during the reconciliation
// phase by the assignCredits function. Keep track of
// what type the statement entries _would_ have been, so
// that the assignCredits function can do the same.
$assign_disbursement_type = null;
// Break each entry out of the combined statement/ledger entry
// and into individual entries appropriate for saving. While
// we're at it, calculate the transaction total as well.
$transaction['amount'] = 0;
foreach ($entries AS &$entry) {
// Ensure these items are null'ed out so we don't
// accidentally pick up stale data.
$le1 = $le1_tender = $le2 = $se = null;
// Really, data should be sanitized at the controller,
// and not here. However, it's a one stop cleanup.
$entry['amount'] = str_replace('$', '', $entry['amount']);
// Set up our comments, possibly using the default 'comment' field
if (empty($entry['ledger_entry_comment'])) {
if ($transaction['type'] != 'INVOICE' && !empty($entry['comment']))
$entry['ledger_entry_comment'] = $entry['comment'];
else
$entry['ledger_entry_comment'] = null;
}
if (empty($entry['statement_entry_comment'])) {
if ($transaction['type'] == 'INVOICE' && !empty($entry['comment']))
$entry['statement_entry_comment'] = $entry['comment'];
else
$entry['statement_entry_comment'] = null;
}
if (empty($entry['crdr']) && !empty($transaction['crdr']))
$entry['crdr'] = strtoupper($this->Account->fundamentalOpposite
($transaction['crdr']));
// Priority goes to settings defined in $entry, but
// use the control information as defaults.
$entry += $control;
if (!empty($entry['include_ledger_entry'])) {
// Create one half of the Double Ledger Entry (and the Tender)
$le1 =
array_intersect_key($entry,
array_flip(array('ledger_id', 'account_id', 'crdr', 'amount')));
$le1['comment'] = $entry['ledger_entry_comment'];
$le1_tender = isset($entry['Tender']) ? $entry['Tender'] : null;
// Create the second half of the Double Ledger Entry
if ($transaction['type'] == 'CLOSE') {
$le2 =
array_intersect_key($entry,
array_flip(array('account_id', 'amount')));
$le2['ledger_id'] = $entry['new_ledger_id'];
$le2['crdr'] = strtoupper($this->Account->fundamentalType($le2['account_id']));
$le2['comment'] = "Ledger Balance Forward (b/f)";
}
else {
$le2 =
array_intersect_key($entry,
array_flip(array('amount'))) +
array_intersect_key($transaction,
array_flip(array('ledger_id', 'account_id', 'crdr')));
}
if ($entry['amount'] < 0 && !empty($entry['force_positive'])) {
$le1['amount'] *= -1;
$le2['amount'] *= -1;
$entry += array('swap_crdr' => true);
}
if (!empty($entry['swap_crdr']))
list($le1['crdr'], $le2['crdr']) = array($le2['crdr'], $le1['crdr']);
}
else
$le1 = $le1_tender = $le2 = null;
// Now that the ledger entries are in place, respect the 'negative' flag
if (!empty($entry['negative']))
$entry['amount'] *= -1;
if (!empty($entry['include_statement_entry'])) {
// Create the statement entry
$se =
array_intersect_key($entry,
array_flip(array('type', 'account_id', 'amount',
'effective_date', 'through_date', 'due_date',
'customer_id', 'lease_id',
'charge_entry_id'))) +
array_intersect_key($transaction,
array_flip(array('customer_id', 'lease_id')));
$se['comment'] = $entry['statement_entry_comment'];
}
else {
if (!empty($entry['assign']) &&
!empty($entry['type']) &&
empty($entry['charge_entry_id'])) {
if (empty($assign_disbursement_type))
$assign_disbursement_type = $entry['type'];
elseif ($entry['type'] != $assign_disbursement_type)
$this->INTERNAL_ERROR('Multiple disbursement types for this transaction');
}
$se = null;
}
// Add entry amount into the transaction total
$transaction['amount'] += $entry['amount'];
// Replace combined entry with our new individual entries
$entry = compact('le1', 'le1_tender', 'le2', 'se');
}
return $this->prReturn(compact('transaction', 'entries')
+ array('vars' => compact('assign_disbursement_type'))
+ array('error' => false));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addNsf
* - Adds NSF transaction
*/
function addNsf($tender, $stamp = null, $comment = null) {
$this->prEnter(compact('tender', 'stamp', 'comment'));
$ret = array();
// Enter the NSF
// This is the transaction pulling money from the bank account
// and recording it in the NSF account. It has nothing to do
// with the customer statement (charges, disbursements, credits, etc).
$bounce_result = $this->addDeposit
(array('Transaction' =>
array('stamp' => $stamp,
'type' => 'WITHDRAWAL',
'crdr' => 'CREDIT'),
'Entry' =>
array(array('tender_id' => null,
'account_id' => $this->Account->nsfAccountID(),
'amount' => $tender['LedgerEntry']['amount'],
))),
$tender['Transaction']['account_id']);
$this->pr(20, compact('bounce_result'));
$ret['bounce'] = $bounce_result;
if ($bounce_result['error'])
return $this->prReturn(array('error' => true) + $ret);
// Since we may have saved the nsf transaction with a null
// timestamp, query it back out of the database to find out
// what timestamp was _really_ specified, for later use.
$bounce = $this->find
('first', array('contain' => false, 'id' => $bounce_result['transaction_id']));
$this->pr(20, compact('bounce'));
$stamp = $bounce['Transaction']['stamp'];
// OK, now move into customer realm, finding all statement
// entries that were affected by the bad payment (tender).
$nsf_ledger_entry = $this->LedgerEntry->find
('first', array
('contain' => array('Transaction' =>
array(//'fields' => array(),
'StatementEntry' =>
array(//'fields' => array(),
),
),
),
'conditions' => array('LedgerEntry.id' => $tender['LedgerEntry']['id']),
));
$this->pr(20, compact('nsf_ledger_entry'));
if (!$nsf_ledger_entry)
return $this->prReturn(array('error' => true) + $ret);
// Build a transaction to adjust all of the statement entries
$rollback =
array('control' =>
array('assign' => false,
'include_ledger_entry' => false,
'include_statement_entry' => true,
),
'Transaction' =>
array('stamp' => $stamp,
'type' => 'RECEIPT',
'crdr' => 'CREDIT',
'account_id' => $this->Account->nsfAccountID(),
'customer_id' => $tender['Tender']['customer_id'],
'comment' => $comment,
),
'Entry' => array());
$rollback['Transaction']['amount'] = 0;
foreach ($nsf_ledger_entry['Transaction']['StatementEntry'] AS $disbursement) {
if ($disbursement['type'] === 'SURPLUS') {
$disbursement['type'] = 'VOID';
$this->StatementEntry->id = $disbursement['id'];
$this->StatementEntry->saveField('type', $disbursement['type']);
}
else {
$rollback['Entry'][] =
array('type' => $disbursement['type'],
'amount' => -1 * $disbursement['amount'],
'account_id' => $this->Account->nsfAccountID(),
'customer_id' => $disbursement['customer_id'],
'lease_id' => $disbursement['lease_id'],
'charge_entry_id' => $disbursement['charge_entry_id'],
);
$rollback['Transaction']['amount'] += $disbursement['amount'];
}
}
// Add the sole ledger entry for this transaction
$rollback['Entry'][] =
array('include_ledger_entry' => true,
'include_statement_entry' => false,
'amount' => $rollback['Transaction']['amount'],
'account_id' => $this->Account->accountReceivableAccountID(),
);
// Set the transaction amount to be negative
$rollback['Transaction']['amount'] *= -1;
// Record the transaction, which will un-pay previously paid
// charges, void any credits, and other similar work.
if (count($rollback['Entry'])) {
$rollback_result = $this->addTransaction($rollback['control'],
$rollback['Transaction'],
$rollback['Entry']);
$this->pr(20, compact('rollback', 'rollback_result'));
$ret['rollback'] = $rollback_result;
if ($rollback_result['error'])
return $this->prReturn(array('error' => true) + $ret);
}
// Add NSF Charge
$charge_result = $this->addInvoice
(array('Transaction' => compact('stamp'),
'Entry' =>
array
(array('account_id' => $this->Account->nsfChargeAccountID(),
'effective_date' => $stamp,
// REVISIT <AP>: 20090730
// BAD, BAD, BAD... who would actually
// hardcode a value like this???? ;-)
'amount' => 35,
'comment' => "NSF: " . $tender['Tender']['name'],
),
),
),
$tender['Tender']['customer_id']);
$this->pr(20, compact('charge_result'));
$ret['charge'] = $charge_result;
if ($charge_result['error'])
return $this->prReturn(array('error' => true) + $ret);
if (!empty($ret['rollback'])) {
foreach ($ret['rollback']['entries'] AS $rentry) {
if (!empty($rentry['DoubleEntry'])) {
if (!empty($rentry['DoubleEntry']['error']))
continue;
foreach (array('Entry1', 'Entry2') AS $n) {
$entry = $rentry['DoubleEntry'][$n];
if ($entry['data']['account_id'] == $this->Account->nsfAccountID()) {
if (!empty($ret['nsf_ledger_entry_id']))
$this->INTERNAL_ERROR("More than one NSF LE ID");
$ret['nsf_ledger_entry_id'] = $entry['ledger_entry_id'];
}
}
}
}
}
if (empty($ret['rollback']['error']) && empty($ret['nsf_ledger_entry_id']))
$this->INTERNAL_ERROR("NSF LE ID not found under rollback entries");
$ret['nsf_transaction_id'] = $ret['bounce']['transaction_id'];
return $this->prReturn($ret + array('error' => false));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addReversal
* - Adds a new charge reversal
*/
function addReversal($charge, $stamp = null, $comment = null) {
$this->prEnter(compact('charge', 'stamp', 'comment'));
$ret = array();
// Finding all statement entries affected by reversing this charge.
$disb_entries = $this->StatementEntry->find
('first', array
('contain' => array('DisbursementEntry' =>
array(//'fields' => array(),
),
),
'conditions' => array('StatementEntry.id' => $charge['StatementEntry']['id']),
));
$this->pr(20, compact('disb_entries'));
if (!$disb_entries)
return $this->prReturn(array('error' => true) + $ret);
// Build a transaction to adjust all of the statement entries
// These are all disbursements against the charge we're reversing
$rollback =
array('control' =>
array('include_ledger_entry' => false,
'include_statement_entry' => true,
),
'Transaction' =>
array('stamp' => $stamp,
'type' => 'CREDIT_NOTE',
'crdr' => 'CREDIT',
'account_id' => $this->Account->accountReceivableAccountID(),
'amount' => $charge['StatementEntry']['amount'],
'customer_id' => $charge['StatementEntry']['customer_id'],
'lease_id' => null,
'comment' => $comment,
),
'Entry' => array());
// Reverse the charge
$rollback['Entry'][] =
array('include_ledger_entry' => true,
'include_statement_entry' => true,
'type' => 'REVERSAL',
'account_id' => $charge['StatementEntry']['account_id'],
'amount' => $charge['StatementEntry']['amount'],
'customer_id' => $charge['StatementEntry']['customer_id'],
'lease_id' => $charge['StatementEntry']['lease_id'],
'charge_entry_id' => $charge['StatementEntry']['id'],
);
$customer_credit = 0;
foreach ($disb_entries['DisbursementEntry'] AS $disbursement) {
$rollback['Entry'][] =
array(
'include_ledger_entry' =>
($disbursement['type'] !== 'DISBURSEMENT'),
'force_positive' => true,
'type' => $disbursement['type'],
'amount' => -1 * $disbursement['amount'],
'account_id' =>
($disbursement['type'] === 'DISBURSEMENT'
? $this->Account->customerCreditAccountID()
: $disbursement['account_id']),
'customer_id' => $disbursement['customer_id'],
'lease_id' => $disbursement['lease_id'],
'charge_entry_id' => $disbursement['charge_entry_id'],
);
if ($disbursement['type'] === 'DISBURSEMENT')
$customer_credit += $disbursement['amount'];
}
// Create an explicit surplus entry for the customer credit.
// Do it BEFORE assigning credits to outstanding charges to
// ensure that those charges are paid from the customer surplus
// account thus and we don't end up with bizarre disbursements,
// like having Rent paid from Damage (or whatever account the
// reversed charge came from).
$rollback['control']['assign'] = array($customer_credit, 'assign');
// Record the transaction, which will un-disburse previously
// disbursed payments, and other similar work.
if (count($rollback['Entry'])) {
$rollback_result = $this->addTransaction($rollback['control'],
$rollback['Transaction'],
$rollback['Entry']);
$this->pr(20, compact('rollback', 'rollback_result'));
$ret = $rollback_result;
if ($rollback_result['error'])
return $this->prReturn(array('error' => true) + $ret);
}
return $this->prReturn($ret + array('error' => false));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: stats
* - Deletes a transaction and associated entries
* - !!WARNING!! This should be used with EXTREME caution, as it
* irreversibly destroys the data. It is not for normal use,
* and can leave the database in an inconsistent state. Expected
* scenario is to remove a bad transaction directly after creation,
* before it gets tied to anything else in the system (such as
* a late charge invoice for a customer that isn't actually late).
*/
function destroy($id) {
$this->prFunctionLevel(30);
$this->prEnter(compact('id'));
/* $transaction = $this->find */
/* ('first', */
/* array('contain' => */
/* array(// Models */
/* 'StatementEntry', */
/* 'LedgerEntry' => array('Tender'), */
/* ), */
/* 'conditions' => array(array('Transaction.id' => $id)), */
/* )); */
/* pr($transaction); */
$this->id = $id;
$customer_id = $this->field('customer_id');
$result = $this->delete($id);
if (!empty($customer_id)) {
$this->StatementEntry->assignCredits
(null, null, null, null, $customer_id, null);
//$this->Customer->update($customer_id);
}
return $this->prReturn($result);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: update
* - Update any cached or calculated fields
*/
function update($id) {
$this->INTERNAL_ERROR("Transaction::update not yet implemented");
$result = $this->find
('first',
array('link' => array('StatementEntry'),
'fields' => array("SUM(LedgerEntry.amount) AS total"),
'conditions' => array(array('LedgerEntry.account_id = Transaction.account_id'),
),
));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: stats
* - Returns summary data from the requested transaction
*/
function stats($id = null, $query = null, $balance_account_id = null) {
$this->prEnter(compact('id', 'query'));
$this->queryInit($query);
unset($query['group']);
if (isset($id)) {
$query['conditions'][] = array('Transaction.id' => $id);
$query['group'] = 'Transaction.id';
}
else
// CakePHP seems to automagically add in our ID as a part
// of the query conditions, but only on a 'first' query,
// not an 'all'. I suppose this is helpful :-/
unset($this->id);
if (empty($query['fields']))
$query['fields'] = array();
// Get the overall total
$squery = $query;
$squery['fields'][] = "SUM(Transaction.amount) AS total";
$squery['fields'][] = "COUNT(Transaction.id) AS count";
$stats = $this->find('first', $squery);
if (empty($stats))
return $this->prReturn(array());
$stats = $stats[0];
unset($stats[0]);
foreach ($this->hasMany AS $table => $association) {
// Only calculate stats for *Entry types
if (!preg_match("/Entry$/", $table) &&
!preg_match("/Entry$/", $association['className']))
continue;
$squery = $query;
$squery['link'][$table] = array('fields' => array());
if ($table == 'LedgerEntry') {
if (isset($balance_account_id)) {
$squery['link']['LedgerEntry']['Account'] = array('fields' => array());
$squery['conditions'][] = array("Account.id" => $balance_account_id);
}
$squery['fields'] = array_merge($squery['fields'],
$this->LedgerEntry->debitCreditFields(true, $balance_account_id != null));
}
elseif ($table == 'StatementEntry') {
$squery['fields'] = array_merge($squery['fields'],
$this->StatementEntry->chargeDisbursementFields(true));
}
else {
$squery['fields'][] = "SUM({$table}.amount) AS total";
$squery['fields'][] = "COUNT({$table}.id) AS entries";
}
$stats[$table] = $this->find('first', $squery);
// REVISIT <AP>: 20090724
// [0][0] is for when we do an 'all' query. This can
// be removed at some point, but I'm keeping it while
// toggling between 'all' and 'first' (testing).
if (isset($stats[$table][0][0]))
$stats[$table] += $stats[$table][0][0];
else
$stats[$table] += $stats[$table][0];
unset($stats[$table][0]);
}
// Add summary data, which may or may not be useful
// or even meaningful, depending on what the caller
// has queried (it's up to them to make that decision).
$stats['undisbursed'] = $stats['total'] - $stats['StatementEntry']['disbursements'];
return $this->prReturn($stats);
}
}
?>