30, 'show' => 30); /************************************************************************** ************************************************************************** ************************************************************************** * function: securityDeposits * - Returns an array of security deposit entries */ function securityDeposits($id, $query = null) { $this->prEnter(compact('id', 'query')); $this->queryInit($query); $query['conditions'][] = array('StatementEntry.lease_id' => $id); $query['conditions'][] = array('StatementEntry.account_id' => $this->StatementEntry->Account->securityDepositAccountID()); $set = $this->StatementEntry->reconciledSet('CHARGE', $query, false, true); return $this->prReturn($set); } /************************************************************************** ************************************************************************** ************************************************************************** * function: securityDepositBalance * - Returns the balance of the lease security deposit(s) */ function securityDepositBalance($id, $query = null) { $this->prEnter(compact('id', 'query')); $this->queryInit($query); // REVISIT : 20090807 // Let's try simplifying the security deposit issue. // Presume that security deposits are NOT used at all, // until the customer moves out of the unit. At that // time, the ENTIRE deposit is converted to customer // credit. Piece of cake. // For more information, see file revision history, // including the revision just before this, r503. $this->id = $id; $moveout_date = $this->field('moveout_date'); if (!empty($moveout_date)) return $this->prReturn(0); $query['conditions'][] = array('StatementEntry.lease_id' => $id); $query['conditions'][] = array('StatementEntry.account_id' => $this->StatementEntry->Account->securityDepositAccountID()); $stats = $this->StatementEntry->stats(null, $query); return $this->prReturn($stats['Charge']['disbursement']); } /************************************************************************** ************************************************************************** ************************************************************************** * function: releaseSecurityDeposits * - Releases all security deposits associated with this lease. * That simply makes a disbursement out of them, which can be used * to pay outstanding customer charges, or simply to become * a customer surplus (customer credit). */ function releaseSecurityDeposits($id, $stamp = null, $query = null) { //$this->prFunctionLevel(30); $this->prEnter(compact('id', 'stamp', 'query')); $secdeps = $this->securityDeposits($id, $query); $secdeps = $secdeps['entries']; $this->pr(20, compact('secdeps')); // If there are no paid security deposits, then // we can consider all security deposits released. if (count($secdeps) == 0) return $this->prReturn(true); // Build a transaction $release = array('Transaction' => array(), 'Entry' => array()); $release['Transaction']['stamp'] = $stamp; $release['Transaction']['comment'] = "Security Deposit Release"; foreach ($secdeps AS $charge) { if ($charge['StatementEntry']['type'] !== 'CHARGE') die("INTERNAL ERROR: SECURITY DEPOSIT IS NOT CHARGE"); // Since security deposits are being released, this also means // any unpaid (or only partially paid) security deposit should // have the remaining balance reversed. if ($charge['StatementEntry']['balance'] > 0) $this->StatementEntry->reverse($charge['StatementEntry']['id'], true, $stamp); $release['Entry'][] = array('amount' => $charge['StatementEntry']['reconciled'], 'account_id' => $this->StatementEntry->Account->securityDepositAccountID(), 'comment' => "Released Security Deposit", ); } $customer_id = $secdeps[0]['StatementEntry']['customer_id']; $lease_id = $secdeps[0]['StatementEntry']['lease_id']; // Add receipt of the security deposit funds. Do NOT // flag them as part of the lease, as all received funds // are only associated with the customer, for future // (or present) disbursement on any lease. $result = $this->StatementEntry->Transaction->addReceipt ($release, $customer_id, null); return $this->prReturn($result); } /************************************************************************** ************************************************************************** ************************************************************************** * function: rentLastCharges * - Returns a list of rent charges from this lease that * do not have sequential followup charges. Under normal * circumstances, there would only be one entry, which is * the most recent rent charge. However, it's possible * that there are several, indicating a problem with lease. */ function rentLastCharges($id) { $this->prEnter(compact('id')); $rent_account_id = $this->StatementEntry->Account->rentAccountID(); $entries = $this->find ('all', array('link' => array(// Models 'StatementEntry', 'SEx' => array('class' => 'StatementEntry', 'fields' => array(), 'conditions' => array ('SEx.lease_id = StatementEntry.lease_id', 'SEx.type' => 'CHARGE', 'SEx.account_id' => $rent_account_id, 'SEx.reverse_transaction_id IS NULL', 'SEx.effective_date = DATE_ADD(StatementEntry.through_date, INTERVAL 1 day)', ), ), ), //'fields' => array('id', 'amount', 'effective_date', 'through_date'), 'fields' => array(), 'conditions' => array(array('Lease.id' => $id), array('StatementEntry.type' => 'CHARGE'), array('StatementEntry.account_id' => $rent_account_id), array('StatementEntry.reverse_transaction_id IS NULL'), array('SEx.id' => null), ), ) ); return $this->prReturn($entries); } /************************************************************************** ************************************************************************** ************************************************************************** * function: lateCharges * - Returns a list of late charges from this lease */ function lateCharges($id) { $this->prEnter(compact('id')); $late_account_id = $this->StatementEntry->Account->lateChargeAccountID(); $entries = $this->StatementEntry->find ('all', array('link' => array(// Models 'Lease', ), //'fields' => array('id', 'amount', 'effective_date', 'through_date'), 'conditions' => array(array('Lease.id' => $id), array('StatementEntry.type' => 'CHARGE'), array('StatementEntry.account_id' => $late_account_id), ), ) ); return $this->prReturn($entries); } /************************************************************************** ************************************************************************** ************************************************************************** * function: rentChargeGaps * - Checks for gaps in rent charges */ function rentChargeGaps($id) { $this->prEnter(compact('id')); $entries = $this->rentLastCharges($id); if ($entries && count($entries) > 1) return $this->prReturn(true); return $this->prReturn(false); } /************************************************************************** ************************************************************************** ************************************************************************** * function: rentChargeThrough * - Determines the date that rent has been charged through * Returns one of: * null: There are gaps in the charges * false: There are not yet any charges * date: The date rent has been charged through */ function rentChargeThrough($id) { $this->prEnter(compact('id')); $entries = $this->rentLastCharges($id); if (!$entries) return $this->prReturn(false); if (count($entries) != 1) return $this->prReturn(null); return $this->prReturn($entries[0]['StatementEntry']['through_date']); } /************************************************************************** ************************************************************************** ************************************************************************** * function: rentPaidThrough * - Determines the date of the first unpaid rent */ function rentPaidThrough($id) { $this->prEnter(compact('id')); $rent_account_id = $this->StatementEntry->Account->rentAccountID(); // First, see if we can find any unpaid entries. Of course, // the first unpaid entry gives us a very direct indication // of when the customer is paid up through, which is 1 day // prior to the effective date of that first unpaid charge. $rent = $this->StatementEntry->reconciledSet ('CHARGE', array('fields' => array('StatementEntry.*', 'DATE_SUB(StatementEntry.effective_date, INTERVAL 1 DAY) AS paid_through', ), 'conditions' => array(array('StatementEntry.lease_id' => $id), array('StatementEntry.account_id' => $rent_account_id), array('StatementEntry.reverse_transaction_id IS NULL'), ), 'order' => array('StatementEntry.effective_date'), ), true); $this->pr(20, $rent, "Unpaid rent"); if ($rent['entries']) return $this->prReturn($rent['entries'][0]['StatementEntry']['paid_through']); // If we don't have any unpaid charges (great!), then the // customer is paid up through the last day of the last // charge. So, search for paid charges, which already // have the paid through date saved as part of the entry. $rent = $this->StatementEntry->reconciledSet ('CHARGE', array('conditions' => array(array('StatementEntry.lease_id' => $id), array('StatementEntry.account_id' => $rent_account_id), array('StatementEntry.reverse_transaction_id IS NULL'), ), 'order' => array('StatementEntry.through_date DESC'), ), false); $this->pr(20, $rent, "Paid rent"); if ($rent['entries']) return $this->prReturn($rent['entries'][0]['StatementEntry']['through_date']); // After all that, having found that there are no unpaid // charges, and in fact, no paid charges either, we cannot // possibly say when the customer is paid through. return $this->prReturn(null); } /************************************************************************** ************************************************************************** ************************************************************************** * function: assessMonthlyRent * - Charges rent for the month, if not already charged. */ function assessMonthlyRent($id, $date = null) { $this->prEnter(compact('id', 'date')); $this->id = $id; if (empty($date)) $date = time(); if (is_string($date)) $date = strtotime($date); // REVISIT : 20090808 // Anniversary Billing not supported $anniversary = 0 && $this->field('anniversary_billing'); if (empty($anniversary)) { $date_parts = getdate($date); $date = mktime(0, 0, 0, $date_parts['mon'], 1, $date_parts['year']); } // Make sure we're not trying to assess rent on a closed lease $close_date = $this->field('close_date'); $this->pr(17, compact('close_date')); if (!empty($close_date)) return $this->prReturn(null); // Don't assess rent after customer has moved out $moveout_date = $this->field('moveout_date'); $this->pr(17, compact('moveout_date')); if (!empty($moveout_date) && strtotime($moveout_date) < $date) return $this->prReturn(null); // Determine when the customer has already been charged through // and, of course, don't charge them if they've already been. $charge_through_date = strtotime($this->rentChargeThrough($id)); $this->pr(17, compact('date', 'charge_through_date') + array('date_str' => date('Y-m-d', $date), 'charge_through_date_str' => date('Y-m-d', $charge_through_date))); if ($charge_through_date >= $date) return $this->prReturn(null); // OK, it seems we're going to go ahead and charge the customer // on this lease. Calculate the new charge through date, which // is 1 day shy of 1 month from $date. For example, if we're // charging for 8/1/09, charge through will be 8/31/09, and // charging for 8/15/09, charge through will be 9/14/09. $date_parts = getdate($date); $charge_through_date = mktime(0, 0, 0, $date_parts['mon']+1, $date_parts['mday']-1, $date_parts['year']); // Build the invoice transaction $invoice = array('Transaction' => array(), 'Entry' => array()); // REVISIT : 20090808 // Keeping Transaction.stamp until the existing facility // is up to date. Then we want the stamp to be now() // (and so can just delete the next line). $invoice['Transaction']['stamp'] = date('Y-m-d', $date); $invoice['Entry'][] = array('effective_date' => date('Y-m-d', $date), 'through_date' => date('Y-m-d', $charge_through_date), 'amount' => $this->field('rent'), 'account_id' => $this->StatementEntry->Account->rentAccountId(), ); // Record the invoice and return the result $this->pr(21, compact('invoice')); $result = $this->StatementEntry->Transaction->addInvoice ($invoice, null, $id); return $this->prReturn($result); } /************************************************************************** ************************************************************************** ************************************************************************** * function: assessMonthlyRentAll * - Ensures rent has been charged on all open leases */ function assessMonthlyRentAll($date = null) { $this->prEnter(compact('date')); $leases = $this->find ('all', array('contain' => false, 'conditions' => array('Lease.close_date' => null), )); $ret = array('Lease' => array()); foreach ($leases AS $lease) { $result = $this->assessMonthlyRent($lease['Lease']['id'], $date); $ret['Lease'][$lease['Lease']['id']] = $result; if ($result['error']) $ret['error'] = true; } return $this->prReturn($ret + array('error' => false)); } /************************************************************************** ************************************************************************** ************************************************************************** * function: assessMonthlyLate * - Assess late charges for the month, if not already charged. */ function assessMonthlyLate($id, $date = null) { $this->prEnter(compact('id', 'date')); $this->id = $id; if (empty($date)) $date = time(); if (is_string($date)) $date = strtotime($date); // REVISIT : 20090808 // Anniversary Billing not supported $anniversary = 0 && $this->field('anniversary_billing'); if (empty($anniversary)) { $date_parts = getdate($date); $date = mktime(0, 0, 0, $date_parts['mon'], 11, $date_parts['year']); } // Don't assess a late charge if the late charge date hasn't // even come yet. This is questionable whether we really // should restrict, since the user could know what they're // doing, and/or the server clock could be off (although that // would certainly have much larger ramifications). But, the // fact is that this check likely handles the vast majority // of the expected behavior, and presents an issue for very // few users, if any at all. if ($date > time()) return $this->prReturn(null); // Make sure we're not trying to assess late charges on a closed lease $close_date = $this->field('close_date'); $this->pr(17, compact('close_date')); if (!empty($close_date)) return $this->prReturn(null); // Don't assess late charges after customer has moved out $moveout_date = $this->field('moveout_date'); $this->pr(17, compact('moveout_date')); if (!empty($moveout_date) && strtotime($moveout_date) < $date) return $this->prReturn(null); // Determine when the customer has been charged through for rent // and don't mark them as late if they haven't even been charged rent $charge_through_date = strtotime($this->rentChargeThrough($id)); $this->pr(17, compact('date', 'charge_through_date') + array('date_str' => date('Y-m-d', $date), 'charge_through_date_str' => date('Y-m-d', $charge_through_date))); if ($charge_through_date <= $date) return $this->prReturn(null); // Determine if the customer is actually late. This is based on // when they've paid through, plus 10 days before they're late. // REVISIT : 20090813 // Of course, 10 days is a terrible hardcode. This should be // driven from the late schedule, saved as part of the lease // (when finally implemented). $paid_through_date = strtotime($this->rentPaidThrough($id)); $this->pr(17, compact('date', 'paid_through_date') + array('date_str' => date('Y-m-d', $date), 'paid_through_date_str' => date('Y-m-d', $paid_through_date))); $date_parts = getdate($paid_through_date); $paid_through_date = mktime(0, 0, 0, $date_parts['mon'], $date_parts['mday']+10, $date_parts['year']); if ($paid_through_date >= $date) return $this->prReturn(null); // Determine if the customer has already been charged a late fee // and, of course, don't charge them if they've already been. $late_charges = $this->lateCharges($id); foreach ($late_charges AS $late) { if (strtotime($late['StatementEntry']['effective_date']) == $date) return $this->prReturn(null); } // Build the invoice transaction $invoice = array('Transaction' => array(), 'Entry' => array()); // REVISIT : 20090808 // Keeping Transaction.stamp until the existing facility // is up to date. Then we want the stamp to be now() // (and so can just delete the next line). $invoice['Transaction']['stamp'] = date('Y-m-d', $date); $invoice['Entry'][] = array('effective_date' => date('Y-m-d', $date), 'amount' => 10, 'account_id' => $this->StatementEntry->Account->lateChargeAccountId(), ); // Record the invoice and return the result $this->pr(21, compact('invoice')); $result = $this->StatementEntry->Transaction->addInvoice ($invoice, null, $id); return $this->prReturn($result); } /************************************************************************** ************************************************************************** ************************************************************************** * function: assessMonthlyLateAll * - Ensures rent has been charged on all open leases */ function assessMonthlyLateAll($date = null) { $this->prEnter(compact('date')); $leases = $this->find ('all', array('contain' => false, 'conditions' => array('Lease.close_date' => null), )); $ret = array('Lease' => array()); foreach ($leases AS $lease) { $result = $this->assessMonthlyLate($lease['Lease']['id'], $date); $ret['Lease'][$lease['Lease']['id']] = $result; if ($result['error']) $ret['error'] = true; } return $this->prReturn($ret + array('error' => false)); } /************************************************************************** ************************************************************************** ************************************************************************** * functions: delinquency * - SQL fragments to determine whether a lease is delinquent */ function conditionDelinquent($table_name = 'Lease') { if (empty($table_name)) $t = ''; else $t = $table_name . '.'; return ("({$t}close_date IS NULL AND" . " NOW() > DATE_ADD({$t}paid_through_date, INTERVAL 1 DAY))"); } function delinquentDaysSQL($table_name = 'Lease') { if (empty($table_name)) $t = ''; else $t = $table_name . '.'; return ("IF(" . $this->conditionDelinquent($table_name) . "," . " DATEDIFF(NOW(), {$t}paid_through_date)-1," . " NULL)"); } function delinquentField($table_name = 'Lease') { return ($this->delinquentDaysSQL($table_name) . " AS 'delinquent'"); } /************************************************************************** ************************************************************************** ************************************************************************** * function: moveIn * - Moves the specified customer into the specified lease */ function moveIn($customer_id, $unit_id, $deposit = null, $rent = null, $stamp = null, $comment = null) { $this->prEnter(compact('customer_id', 'unit_id', 'deposit', 'rent', 'stamp', 'comment')); $lt = $this->LeaseType->find('first', array('conditions' => array('code' => 'SL'))); // Use NOW if not given a movein date if (!isset($stamp)) $stamp = date('Y-m-d G:i:s'); if (!$comment) $comment = null; if (!isset($deposit) || !isset($rent)) { $rates = $this->Unit->find ('first', array('contain' => array('UnitSize' => array('deposit', 'rent'), ), 'fields' => array('deposit', 'rent'), 'conditions' => array('Unit.id' => $unit_id), )); $deposit = (isset($deposit) ? $deposit : (isset($rates['Unit']['deposit']) ? $rates['Unit']['deposit'] : (isset($rates['UnitSize']['deposit']) ? $rates['UnitSize']['deposit'] : 0))); $rent = (isset($rent) ? $rent : (isset($rates['Unit']['rent']) ? $rates['Unit']['rent'] : (isset($rates['UnitSize']['rent']) ? $rates['UnitSize']['rent'] : 0))); } // Save this new lease. $this->create(); if (!$this->save(array('lease_type_id' => $lt['LeaseType']['id'], 'unit_id' => $unit_id, 'customer_id' => $customer_id, 'lease_date' => $stamp, 'movein_date' => $stamp, 'deposit' => $deposit, 'rent' => $rent, 'comment' => $comment), false)) { return $this->prReturn(null); } // Set the lease number to be the same as the lease ID $this->id; $this->saveField('number', $this->id); // Update the current lease count for the customer $this->Customer->updateLeaseCount($customer_id); // Update the unit status $this->Unit->updateStatus($unit_id, 'OCCUPIED'); // REVISIT : 20090702 // We need to assess the security deposit charge, // and probably rent as well. Rent, however, will // require user parameters to indicate whether it // was waived, pro-rated, etc. // Return the new lease ID return $this->prReturn($this->id); } /************************************************************************** ************************************************************************** ************************************************************************** * function: moveOut * - Moves the customer out of the specified lease */ function moveOut($id, $status = 'VACANT', $stamp = null, $close = true) { $this->prEnter(compact('id', 'status', 'stamp', 'close')); // Use NOW if not given a moveout date if (!isset($stamp)) $stamp = date('Y-m-d G:i:s'); // Reset the data $this->create(); $this->id = $id; // Set the customer move-out date $this->data['Lease']['moveout_date'] = $stamp; // Save it! $this->save($this->data, false); // Release the security deposit(s) $this->releaseSecurityDeposits($id, $stamp); // Close the lease, if so requested if ($close) $this->close($id, $stamp); // Update the current lease count for the customer $this->Customer->updateLeaseCount($this->field('customer_id')); // Finally, update the unit status $this->recursive = -1; $this->read(); $this->Unit->updateStatus($this->data['Lease']['unit_id'], $status); } /************************************************************************** ************************************************************************** ************************************************************************** * function: close * - Closes the lease to further action */ function close($id, $stamp = null) { $this->prEnter(compact('id', 'stamp')); if (!$this->closeable($id)) return $this->prReturn(false); // Reset the data $this->create(); $this->id = $id; // Use NOW if not given a moveout date if (!isset($stamp)) $stamp = date('Y-m-d G:i:s'); // Set the close date $this->data['Lease']['close_date'] = $stamp; // Save it! $this->save($this->data, false); // Update the current lease count for the customer $this->Customer->updateLeaseCount($this->field('customer_id')); return $this->prReturn(true); } /************************************************************************** ************************************************************************** ************************************************************************** * function: closeable * - Indicates whether or not the lease can be closed */ function closeable($id) { $this->prEnter(compact('id')); $this->recursive = -1; $this->read(null, $id); // We can't close a lease that's still in use if (!isset($this->data['Lease']['moveout_date'])) return $this->prReturn(false); // We can't close a lease that's already closed if (isset($this->data['Lease']['close_date'])) return $this->prReturn(false); // A lease can only be closed if there are no outstanding // security deposits ... if ($this->securityDepositBalance($id) != 0) return $this->prReturn(false); // ... and if the account balance is zero. if ($this->balance($id) != 0) return $this->prReturn(false); // Apparently this lease meets all the criteria! return $this->prReturn(true); } /************************************************************************** ************************************************************************** ************************************************************************** * function: refund * - Marks any lease balance as payable to the customer. */ function refund($id, $stamp = null) { $this->prEnter(compact('id')); $balance = $this->balance($id); if ($balance >= 0) return $this->prReturn(array('error' => true)); $balance *= -1; // Build a transaction $refund = array('Transaction' => array(), 'Entry' => array()); $refund['Transaction']['stamp'] = $stamp; $refund['Transaction']['comment'] = "Lease Refund"; $refund['Entry'][] = array('amount' => $balance); $result = $this->StatementEntry->Transaction->addRefund ($refund, null, $id); return $this->prReturn($result); } /************************************************************************** ************************************************************************** ************************************************************************** * function: update * - Update any cached or calculated fields */ function update($id) { $this->prEnter(compact('id')); $this->id = $id; $this->saveField('charge_through_date', $this->rentChargeThrough($id)); $this->saveField('paid_through_date', $this->rentPaidThrough($id)); $moveout = $this->field('moveout_date'); if (empty($moveout)) $this->Unit->update($this->field('unit_id')); } /************************************************************************** ************************************************************************** ************************************************************************** * function: balance * - Returns the balance of money owed on the lease */ function balance($id) { $this->prEnter(compact('id')); $stats = $this->stats($id); return $this->prReturn($stats['balance']); } /************************************************************************** ************************************************************************** ************************************************************************** * function: stats * - Returns summary data from the requested lease. */ function stats($id = null, $query = null) { $this->prEnter(compact('id', 'query')); if (!$id) return $this->prReturn(null); $find_stats = $this->StatementEntry->find ('first', array ('contain' => false, 'fields' => $this->StatementEntry->chargeDisbursementFields(true), 'conditions' => array('StatementEntry.lease_id' => $id), )); $find_stats = $find_stats[0]; return $this->prReturn($find_stats); } } ?>