array( 'className' => 'Tender', 'foreignKey' => 'deposit_transaction_id', ), 'Charge' => array( 'className' => 'StatementEntry', 'conditions' => array('Charge.type' => 'CHARGE') ), 'Payment' => array( 'className' => 'StatementEntry', 'conditions' => array('Payment.type' => 'PAYMENT') ), 'Debit' => array( 'className' => 'LedgerEntry', 'conditions' => array('Debit.crdr' => 'DEBIT') ), 'Credit' => array( 'className' => 'LedgerEntry', 'conditions' => array('Credit.crdr' => 'CREDIT') ), ); var $default_log_level = 30; /************************************************************************** ************************************************************************** ************************************************************************** * function: addInvoice * - Adds a new invoice invoice */ function addInvoice($data, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'customer_id', 'lease_id')); // Establish the transaction as an invoice $invoice =& $data['Transaction']; $invoice['type'] = 'INVOICE'; $invoice['crdr'] = 'DEBIT'; $invoice['account_id'] = $this->Account->accountReceivableAccountID(); $invoice['customer_id'] = $customer_id; $invoice['lease_id'] = $lease_id; // Go through the statement entries and flag as charges foreach ($data['Entry'] AS &$entry) { $entry['type'] = 'CHARGE'; $entry['crdr'] = 'CREDIT'; } $ids = $this->addTransaction($data['Transaction'], $data['Entry']); if (isset($ids['transaction_id'])) $ids['invoice_id'] = $ids['transaction_id']; return $ids; } /************************************************************************** ************************************************************************** ************************************************************************** * function: addReceipt * - Adds a new receipt */ function addReceipt($data, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'customer_id', 'lease_id')); // Establish the transaction as a receipt $receipt =& $data['Transaction']; $receipt['type'] = 'RECEIPT'; $receipt['crdr'] = 'CREDIT'; $receipt['account_id'] = $this->Account->accountReceivableAccountID(); $receipt['customer_id'] = $customer_id; $receipt['lease_id'] = $lease_id; // Go through the statement entries and flag as payments foreach ($data['Entry'] AS &$entry) { $entry['crdr'] = 'DEBIT'; if (empty($entry['account_id']) && isset($entry['Tender']['tender_type_id'])) { $entry['account_id'] = $this->LedgerEntry->Tender->TenderType-> accountID($entry['Tender']['tender_type_id']); } } $ids = $this->addTransaction($data['Transaction'], $data['Entry']); if (isset($ids['transaction_id'])) $ids['receipt_id'] = $ids['transaction_id']; return $ids; } /************************************************************************** ************************************************************************** ************************************************************************** * function: addDeposit * - Adds a new bank deposit */ function addDeposit($data, $account_id) { $this->prEnter(compact('data', 'account_id')); // Establish the transaction as a deposit $deposit =& $data['Transaction']; $deposit['type'] = 'DEPOSIT'; $deposit['crdr'] = 'DEBIT'; $deposit['account_id'] = $account_id; $deposit['customer_id'] = null; $deposit['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' => 'CREDIT', 'amount' => 0); $group[$entry['account_id']]['amount'] += $entry['amount']; } $data['Entry'] = $group; $ids = $this->addTransaction($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 $ids; } /************************************************************************** ************************************************************************** ************************************************************************** * function: addClose * - Adds a new transaction for closing ledgers */ function addClose($data) { $this->prEnter(compact('data')); // Establish the transaction as a close $close =& $data['Transaction']; $close['type'] = 'CLOSE'; $close['account_id'] = null; $close['customer_id'] = null; $close['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['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 $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, PAYMENT) * - 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($transaction, $entries, $subtype = null) { $this->prEnter(compact('transaction', 'entries', 'subtype')); // 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->recursive = -1; $lease = $L->read(null, $transaction['lease_id']); $transaction['customer_id'] = $lease['Lease']['customer_id']; } // 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; // Add entry amount into the transaction total $transaction['amount'] += $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; } // 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'))); } // 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']; // (PAYMENTS will have statement entries created below, when // assigning credits, and DEPOSITS don't have statement entries) if ($transaction['type'] != 'INVOICE' && $subtype !== 'NSF') $se = null; // NSF transactions don't use LedgerEntries // REVISIT : 20090731 // Doesn't seem right... probably doing this because of the // single A/R entry we add below for NSF if ($subtype === 'NSF') $le1 = $le1_tender = $le2 = null; // Replace combined entry with our new individual entries $entry = compact('le1', 'le1_tender', 'le2', 'se'); } if ($subtype === 'NSF') { // REVISIT : 20090731 // Should we be doing this, or just doing individual ledger entries // that were created above before we nulled them out array_unshift($entries, array('le1' => array('account_id' => $transaction['account_id'], 'crdr' => 'DEBIT', 'amount' => $transaction['amount']), 'le2' => array('account_id' => $this->Account->accountReceivableAccountID(), 'crdr' => 'CREDIT', 'amount' => $transaction['amount']) )); } $this->pr(20, compact('transaction', 'entries')); // Move forward, verifying and saving everything. // REVISIT : 20090731 NSF was not verifying... let's find out why $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); // 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']; $result = $this->StatementEntry->addStatementEntry($se); $ret['entries'][$e_index]['StatementEntry'] = $result; if ($result['error']) { $ret['error'] = true; continue; } } } if (($transaction['type'] == 'INVOICE' || $transaction['type'] == 'RECEIPT') && $subtype !== 'NSF' && !$ret['error']) { $result = $this->StatementEntry->assignCredits (array('link' => array('Customer'), 'conditions' => array('Customer.id' => $transaction['customer_id'])), ($transaction['type'] == 'RECEIPT' ? $ret['transaction_id'] : null)); $ret['assigned'] = $result; if ($result['error']) $ret['error'] = true; } return $this->prReturn($ret); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addNsf * - Adds NSF transaction */ function addNsf($tender, $stamp) { $this->prEnter(compact('tender', 'stamp')); $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, payments, credits, etc). $bounce_result = $this->addDeposit (array('Transaction' => array(), 'Entry' => array(array('tender_id' => null, 'account_id' => $this->Account->nsfAccountID(), 'amount' => -1 * $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('Transaction' => array(), 'Entry' => array()); $rollback['Transaction']['stamp'] = $stamp; $rollback['Transaction']['type'] = 'RECEIPT'; $rollback['Transaction']['crdr'] = 'DEBIT'; // Unused... keeps verifyTx happy $rollback['Transaction']['account_id'] = $this->Account->nsfAccountID(); $rollback['Transaction']['customer_id'] = $tender['Tender']['customer_id']; $rollback['Transaction']['amount'] = -1 * $tender['LedgerEntry']['amount']; foreach ($nsf_ledger_entry['Transaction']['StatementEntry'] AS $payment) { if ($payment['type'] === 'SURPLUS') { $payment['type'] = 'VOID'; $this->StatementEntry->id = $payment['id']; $this->StatementEntry->saveField('type', $payment['type']); } else { $rollback['Entry'][] = array('type' => $payment['type'], 'amount' => -1 * $payment['amount'], 'account_id' => $this->Account->nsfAccountID(), 'customer_id' => $payment['customer_id'], 'lease_id' => $payment['lease_id'], 'charge_entry_id' => $payment['charge_entry_id'], 'effective_date' => $stamp, ); } } // Record the transaction, which will un-pay previously paid // charges, void any credits, and other similar work. $rollback_result = $this->addTransaction($rollback['Transaction'], $rollback['Entry'], 'NSF'); $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 : 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']; $ret['nsf_ledger_entry_id'] = $ret['rollback']['entries'][0]['DoubleEntry']['Entry1']['ledger_entry_id']; return $this->prReturn($ret + array('error' => false)); } /************************************************************************** ************************************************************************** ************************************************************************** * 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->chargePaymentFields(true)); } else { $squery['fields'][] = "SUM({$table}.amount) AS total"; $squery['fields'][] = "COUNT({$table}.id) AS entries"; } $stats[$table] = $this->find('first', $squery); // REVISIT : 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); } } ?>