Files
pmgr/models/transaction.php
abijah e76ad897a4 Getting closer on the reversal issue. There is definitely more testing to do, and some tweaks as well, but this may be approximately what we will finally settle on.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@565 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-15 03:11:16 +00:00

1167 lines
40 KiB
PHP

<?php
class Transaction extends AppModel {
var $belongsTo = array(
'Customer',
'Account',
'Ledger',
);
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);
/**************************************************************************
**************************************************************************
**************************************************************************
* 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',
'crdr' => 'CREDIT',
);
$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
'crdr' => 'DEBIT',
'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: addReversal
* - Adds a new charge reversal
*/
function addReversal1($data, $charge) {
$this->prEnter(compact('data', 'charge'));
if (count($data['Entry']) != 1)
$this->INTERNAL_ERROR("Should be one Entry for addReversal");
$data += array('control' => array());
/* 'amount' => $stats['Charge']['total'], */
/* 'account_id' => $charge['account_id'], */
// First, reverse the charge
$reversal = $data;
$reversal['control'] +=
array('assign' => false,
'include_ledger_entry' => true,
'include_statement_entry' => true,
);
//$reversal['Transaction']['type'] = 'CREDIT_NOTE';
$reversal['Entry'][0]['charge_entry_id'] = $charge_id;
//$reversal['Entry'][0]['type'] = 'REVERSAL';
$reversal['Entry'][0]['amount'] *= -1;
//$reversal['Transaction']['crdr'] = 'CREDIT';
//$reversal['Entry'][0]['crdr'] = 'DEBIT';
$ids['reversal'] = $this->addInvoice($reversal, $customer_id, $lease_id);
// Then issue a credit for the amount already paid, if any
if ($credit_amount > 0) {
$ids['credit'] = $this->addReceipt
(array('control' =>
array('include_ledger_entry' => false,
'include_statement_entry' => true,
),
'Transaction' =>
array('stamp' => $data['Transaction']['stamp'],
),
'Entry' =>
array(array('amount' => $credit_amount,
'charge_entry_id' => $charge_id,
// REVISIT <AP>: 20090814
// TEMPORARY. JUST NEED AN ACCOUNT AT THE MOMENT
'account_id' => $this->StatementEntry->Account->accountPayableAccountID(),
))),
$customer_id, $lease_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'],
'crdr' => strtoupper($this->Account->fundamentalOpposite($data['Transaction']['crdr'])),
'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',
'crdr' => 'CREDIT',
);
$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.
*
* $data
* - Transaction
* - [MANDATORY]
* - type (INVOICE, RECEIPT)
* - account_id
* - crdr
* - [OPTIONAL]
* - stamp
* (default: NOW)
* - comment
* - [AUTOMATICALLY SET] (if set, these items will be overwritten)
* - id
* - amount
* - customer_id
* - ledger_id
*
* - Entry (array)
* - [MANDATORY]
* - type (CHARGE, DISBURSEMENT)
* - account_id
* - crdr
* - amount
* - [OPTIONAL]
* - effective_date
* - through_date
* - due_date
* - comment (used for statement or ledger entry, based on context)
* - ledger_entry_comment
* - statement_entry_comment
* - Tender
* - [MANDATORY]
* - tender_type_id
* - [OPTIONAL]
* - name
* (default: Entry Account Name & data1)
* - data1, data2, data3, data4
* - comment
* - [AUTOMATICALLY SET] (if set, these items will be overwritten)
* - id
* - ledger_entry_id
* - deposit_transaction_id
* - nsf_transaction_id
* - [AUTOMATICALLY SET] (if set, these items will be overwritten)
* - id
* - transaction_id
* - ledger_id
*
*/
function addTransaction($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');
}
// Some transactions do not have their statement entries
// generated from this function, but they are instead
// 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;
}
// 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->fundamentalOpposite($le1['crdr']));
$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')));
}
}
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');
}
// If transaction amount is not already set, use the
// sum of the entry amounts.
$transaction += array('amount' => $transaction_amount);
/* // Add a summary ledger entry if requested */
/* if (!empty($control['summary_double_entry'])) { */
/* $entry = $control['summary_double_entry']; */
/* $le1 = */
/* array_intersect_key($entry, */
/* array_flip(array('ledger_id', 'account_id', 'crdr', 'comment'))) + */
/* array_intersect_key($transaction, */
/* array_flip(array('amount'))); */
/* $le1_tender = isset($entry['Tender']) ? $entry['Tender'] : null; */
/* $le2 = */
/* array_intersect_key($transaction, */
/* array_flip(array('ledger_id', 'account_id', 'crdr'))); */
/* array_unshift($entries, */
/* array('le1' => array('account_id' => $de['le1']['account_id'], */
/* 'crdr' => strtoupper($this->Account->fundamentalOpposite */
/* ($control['ar_ledger_entry'])), */
/* 'amount' => -1 * $transaction['amount']), */
/* 'le2' => array('account_id' => $this->Account->accountReceivableAccountID(), */
/* 'crdr' => $control['ar_ledger_entry'], */
/* 'amount' => -1 * $transaction['amount']) */
/* )); */
/* } */
$this->pr(20, compact('transaction', 'entries'));
// Move forward, verifying and saving everything.
$ret = array();
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);
$transaction_stamp = $this->field('stamp');
// Set up our return ids array
$ret['transaction_id'] = $this->id;
$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'] = $ret['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'] = $ret['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;
}
}
}
if (!empty($control['assign']) && !$ret['error']) {
$result = $this->StatementEntry->assignCredits
(null,
(empty($control['assign_receipt']) ? null
: $ret['transaction_id']),
null,
$assign_disbursement_type,
$transaction['customer_id'],
$transaction['lease_id']
);
$ret['assigned'] = $result;
if ($result['error'])
$ret['error'] = true;
}
$this->Customer->update($transaction['customer_id']);
return $this->prReturn($ret);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* 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(),
'crdr' => 'DEBIT',
'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(),
'crdr' => 'DEBIT',
);
// 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);
$ret['nsf_transaction_id'] = $ret['bounce']['transaction_id'];
if (!empty($ret['rollback']))
$ret['nsf_ledger_entry_id'] = $ret['rollback']['entries'][0]['DoubleEntry']['Entry1']['ledger_entry_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('assign' => true,
'assign_receipt' => true,
'include_ledger_entry' => false,
'include_statement_entry' => true,
),
'Transaction' =>
array('stamp' => $stamp,
//'type' => 'RECEIPT',
'type' => 'CREDIT_NOTE',
// The transaction amount will equal that of the charge
'amount' => $charge['StatementEntry']['amount'],
'account_id' => $charge['StatementEntry']['account_id'],
'crdr' => 'DEBIT',
'customer_id' => $charge['StatementEntry']['customer_id'],
'lease_id' => null,
'comment' => $comment,
),
'Entry' => array());
foreach ($disb_entries['DisbursementEntry'] AS $disbursement) {
$rollback['Entry'][] =
array('type' => $disbursement['type'],
'amount' => -1 * $disbursement['amount'],
'account_id' => $disbursement['account_id'],
'customer_id' => $disbursement['customer_id'],
'lease_id' => $disbursement['lease_id'],
'charge_entry_id' => $disbursement['charge_entry_id'],
);
}
// Add the sole ledger entry for this transaction
$rollback['Entry'][] =
array('include_ledger_entry' => true,
'include_statement_entry' => false,
'type' => 'REVERSAL',
'amount' => $rollback['Transaction']['amount'],
//'account_id' => $this->Account->accountPayableAccountID(),
'account_id' => $this->Account->accountReceivableAccountID(),
'crdr' => 'CREDIT',
);
// 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'] = $rollback_result;
if ($rollback_result['error'])
return $this->prReturn(array('error' => true) + $ret);
}
/* $ret['nsf_transaction_id'] = $ret['bounce']['transaction_id']; */
/* if (!empty($ret['rollback'])) */
/* $ret['nsf_ledger_entry_id'] = $ret['rollback']['entries'][0]['DoubleEntry']['Entry1']['ledger_entry_id']; */
$ret = $ret['rollback'];
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();
$stats = array();
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]);
}
return $this->prReturn($stats);
}
}
?>