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')); // Establish the transaction as an invoice $invoice =& $data['Transaction']; $invoice += 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['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')); // Establish the transaction as a receipt $receipt =& $data['Transaction']; $receipt += 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['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) INTERNAL_ERROR("Should be one Entry for addWaiver"); // Just make sure the disbursement(s) are marked as waivers // and that they go to cover the specific charge. $data['Transaction']['disbursement_type'] = 'WAIVER'; $data['Transaction']['assign_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 addReversal($data, $charge_id, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'charge_id', 'customer_id', 'lease_id')); if (count($data['Entry']) != 1) INTERNAL_ERROR("Should be one Entry for addReversal"); // Just make sure the disbursement(s) are marked as reversals // and that they go to cover the specific charge. $data['Transaction']['type'] = 'CREDIT_NOTE'; $data['Transaction']['disbursement_type'] = 'REVERSAL'; $data['Transaction']['assign_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['reversal_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')); // Establish the transaction as a deposit $deposit =& $data['Transaction']; $deposit += 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' => '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 $this->prReturn($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 += 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['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')); // 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. $refund =& $data['Transaction']; $refund += 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')); // Establish the transaction as an payment $payment =& $data['Transaction']; $payment += 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['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($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->id = $transaction['lease_id']; $transaction['customer_id'] = $L->field('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; // Really, data should be sanitized at the controller, // and not here. However, it's a one stop cleanup. $entry['amount'] = str_replace('$', '', $entry['amount']); // 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']; // (DISBURSEMENTS will have statement entries created below, when // assigning credits, and DEPOSITS don't have statement entries) if (empty($transaction['customer_id']) || ($transaction['account_id'] == $this->Account->accountReceivableAccountID() && $transaction['crdr'] == 'CREDIT')) $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. $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']; $se += array('effective_date' => $transaction_stamp); $result = $this->StatementEntry->addStatementEntry($se); $ret['entries'][$e_index]['StatementEntry'] = $result; if ($result['error']) { $ret['error'] = true; continue; } } } if ($transaction['account_id'] == $this->Account->accountReceivableAccountID() && !$ret['error']) { $result = $this->StatementEntry->assignCredits (null, ($transaction['crdr'] == 'CREDIT' ? $ret['transaction_id'] : null), ($transaction['crdr'] == 'CREDIT' && !empty($transaction['assign_charge_entry_id']) ? $transaction['assign_charge_entry_id'] : null), (!empty($transaction['disbursement_type']) ? $transaction['disbursement_type'] : null), $transaction['customer_id'], $transaction['lease_id'] ); $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, disbursements, 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 $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'], 'effective_date' => $stamp, ); } } // 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['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']; 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: 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 : 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); } } ?>