Projekt

Obecné

Profil

« Předchozí | Další » 

Revize cd149dd5

Přidáno uživatelem Ondřej Fibich před téměř 7 roky(ů)

Refs #1076: member expiration date calculation fix. The calc functionality was separate from Members controller to a new Expiration calc service. The new service comes with unit tests which tests the #1076. Additionally this patch comes with some fixes and improvements in already existing unit and integration tests.

Zobrazit rozdíly:

application/controllers/members.php
}
// finds date of expiration of member fee
$expiration_date = '';
$expiration_info = '';
if (Settings::get('finance_enabled') &&
isset($account) && !$is_applicant && !$is_former)
{
$expiration_date = self::get_expiration_date($account);
$expiration_info = $this->services->injectMemberExpirationCalc()
->get_expiration_info($account);
}
// finds total traffic of member
......
$view->content->contacts = $contacts;
$view->content->contact_types = $contact_types;
$view->content->variable_symbols = (isset($variable_symbols)) ? $variable_symbols : NULL;
$view->content->expiration_date = $expiration_date;
$view->content->expiration_info = $expiration_info;
$view->content->entrance_fee_paid = (isset($entrance_fee_paid)) ? $entrance_fee_paid : NULL;
$view->content->entrance_fee_left = (isset($entrance_fee_left)) ? $entrance_fee_left : NULL;
$view->content->account = (isset($account)) ? $account : NULL;
......
$view->render(TRUE);
} // end of show function
/**
* Gets expiration date of member's payments.
*
* @author Michal Kliment, Ondrej Fibich
* @param object $account
* @return string
*/
public static function get_expiration_date($account)
{
// member's actual balance
$balance = $account->balance;
$transfer_model = new Transfer_Model();
$close_date = date_parse(
date::get_closses_deduct_date_to(
$transfer_model->get_last_transfer_datetime_of_account($account->id)
)
);
// date
$day = $close_date['day'];
$month = $close_date['month'];
$year = $close_date['year'];
// balance is in positive, we will go to the future
if ($balance > 0)
{
$sign = 1;
}
// balance is in negative, we will go to the past
else
{
$sign = -1;
}
// ttl = time to live - it is count how many ending conditions
// will have to happen to end cycle
// negative balance needs one extra more
$ttl = ($balance < 0) ? 2 : 1;
// negative balance will drawn by red color, else balance will drawn by green color
$color = ($balance < 0) ? 'red' : 'green';
$payments = array();
// finds entrance date of member
$entrance_date_str = date::get_closses_deduct_date_to($account->member->entrance_date);
$entrance_date = date_parse($entrance_date_str);
// finds debt payment rate of entrance fee
$debt_payment_rate = ($account->member->debt_payment_rate > 0)
? $account->member->debt_payment_rate : $account->member->entrance_fee;
// finds all debt payments of entrance fee
self::find_debt_payments(
$payments, $entrance_date['month'], $entrance_date['year'],
$account->member->entrance_fee, $debt_payment_rate
);
// finds all member's devices with debt payments
$devices = ORM::factory('device')->get_member_devices_with_debt_payments($account->member_id);
foreach ($devices as $device)
{
// finds buy date of this device
$buy_date = date_parse(date::get_closses_deduct_date_to($device->buy_date));
// finds all debt payments of this device
self::find_debt_payments(
$payments, $buy_date['month'], $buy_date['year'],
$device->price, $device->payment_rate
);
}
$fee_model = new Fee_Model();
// protection from unending loop
$too_long = FALSE;
// finds min and max date = due to prevent before unending loop
$min_fee_date = $fee_model->get_min_fromdate_fee_by_type ('regular member fee');
$max_fee_date = $fee_model->get_max_todate_fee_by_type ('regular member fee');
while (true)
{
$date = date::create(date::get_deduct_day_to($month, $year), $month, $year);
// date is bigger/smaller than max/min fee date, ends it (prevent before unending loop)
if (($sign == 1 && $date > $max_fee_date) || ($sign == -1 && $date < $min_fee_date))
break;
// finds regular member fee for this month
$fee = $fee_model->get_regular_member_fee_by_member_date($account->member_id, $date);
// if exist payment for this month, adds it to the fee
if (isset($payments[$year][$month]))
$fee += $payments[$year][$month];
// attributed / deduct fee to / from balance
$balance -= $sign * $fee;
if ($sign == -1 && $balance == 0)
$ttl--;
if ($balance * $sign < 0)
$ttl--;
if ($ttl == 0)
break;
$month += $sign;
if ($month == 0 OR $month == 13)
{
$month = ($month == 13) ? 1 : 12;
$year += $sign;
}
// if we are 5 years in future, there is no point of counting more
if (date('Y') + 10 < $year)
{
$too_long = TRUE;
break;
}
}
$month--;
if ($month == 0)
{
$month = 12;
$year--;
}
$date = date::create(date::days_of_month($month), $month, $year);
if (strtotime($date) < strtotime($entrance_date_str))
$date = $entrance_date_str;
return '<span style="color: '.$color.'">'
. ($too_long ? '&gt; ' : '')
. $date. '</span>';
}
/**
* It stores debt payments into double-dimensional array (indexes year, month)
*
* @author Michal Kliment
* @param array $payments
* @param int $month
* @param int $year
* @param float $payment_left
* @param float $payment_rate
*/
protected static function find_debt_payments(
&$payments, $month, $year, $payment_left, $payment_rate)
{
while ($payment_left > 0)
{
if ($payment_left > $payment_rate)
$payment = $payment_rate;
else
$payment = $payment_left;
if (isset($payments[$year][$month]))
$payments[$year][$month] += $payment;
else
$payments[$year][$month] = $payment;
$month++;
if ($month > 12)
{
$year++;
$month = 1;
}
$payment_left -= $payment;
}
}
/**
* Function adds new member to database.
* Creates user of type member assigned to this member.
application/controllers/transfers.php
if ($account->member->type != Member_Model::TYPE_APPLICANT &&
$account->member->type != Member_Model::TYPE_FORMER)
{
$view->content->expiration_date = Members_Controller::get_expiration_date($account);
$view->content->expiration_info = $this->services
->injectMemberExpirationCalc()
->get_expiration_info($account);
}
$view->content->transfers_grid = $transfers_grid;
application/libraries/MY_Controller.php
}
catch (InvalidArgumentException $iaex)
{
self::error(WRITABLE, server::base_dir() . 'upload');
self::error(WRITABLE, server::base_dir() . '/upload');
}
catch (NotEnabledDbUpgradeException $neu)
{
application/services/core/AclService.php
{
parent::__construct($factory);
// singleton resolver instance
if (empty($this->resolver))
if (empty(self::$resolver))
{
$this->resolver = new \Groups_aro_map_Model();
self::$resolver = new \Groups_aro_map_Model();
}
}
......
if (($member_id == $_SESSION['member_id']) || $force_own)
{
// check own access
if ($this->resolver->has_access(
if (self::$resolver->has_access(
$_SESSION['user_id'], $aco_type . '_own',
$axo_section, $axo_value
))
......
}
// check all
return $this->resolver->has_access(
return self::$resolver->has_access(
$_SESSION['user_id'], $aco_type . '_all',
$axo_section, $axo_value
);
......
*/
public function is_user_in_group($aro_group_id, $aro_id)
{
return $this->resolver->groups_aro_map_exists($aro_group_id, $aro_id);
return self::$resolver->groups_aro_map_exists($aro_group_id, $aro_id);
}
/**
application/services/member/ExpirationCalcService.php
<?php
/*
* This file is part of open source system FreenetIS
* and it is release under GPLv3 licence.
*
* More info about licence can be found:
* http://www.gnu.org/licenses/gpl-3.0.html
*
* More info about project can be found:
* http://www.freenetis.org/
*/
namespace freenetis\service\member;
use date;
use Transfer_Model;
use Fee_Model;
use Device_Model;
/**
* Service that allows to calculate member payment expiration date.
*
* @author Ondřej Fibich <fibich@freenetis.org>
* @since 1.2
*/
class ExpirationCalcService extends \AbstractService
{
/**
* @var Transfer_Model
*/
protected $transfer_model;
/**
* @var Fee_Model
*/
protected $fee_model;
/**
* @var Device_Model
*/
protected $device_model;
/**
* Creates service.
*
* @param \ServiceFactory $factory
*/
public function __construct(\ServiceFactory $factory)
{
parent::__construct($factory);
$this->transfer_model = new Transfer_Model;
$this->fee_model = new Fee_Model;
$this->device_model = new Device_Model;
}
/**
* Gets expiration date of member's payments.
*
* @author Michal Kliment, Ondrej Fibich
* @param object $account
* @param int $shortened_on_year year to shortened expiration date from
* (10 years from now by default)
* @return ExpirationCalcResult
*/
public function get_expiration_info($account, $shortened_on_year = NULL)
{
if (!is_numeric($shortened_on_year))
{
$shortened_on_year = date('Y') + 10; // 10 year shortened by default
}
// member's actual balance
$balance = $account->balance;
$last_deduct_date = date_parse(
date::get_closses_deduct_date_to(
$this->transfer_model->get_last_transfer_datetime_of_account($account->id)
)
);
// date
$day = $last_deduct_date['day'];
$month = $last_deduct_date['month'];
$year = $last_deduct_date['year'];
// set algoritm firection by current balance
if ($balance > 0)
{
$sign = 1; // balance is in positive, we will go to the future
}
else
{
$sign = -1; // balance is in negative, we will go to the past
}
$payments = array();
// finds entrance date of member
$entrance_date_str = date::get_closses_deduct_date_to($account->member->entrance_date);
$entrance_date = date_parse($entrance_date_str);
// finds debt payment rate of entrance fee
$debt_payment_rate = ($account->member->debt_payment_rate > 0)
? $account->member->debt_payment_rate : $account->member->entrance_fee;
// finds all debt payments of entrance fee
self::find_debt_payments(
$payments, $entrance_date['month'], $entrance_date['year'],
$account->member->entrance_fee, $debt_payment_rate
);
// finds all member's devices with debt payments
$devices = $this->device_model->get_member_devices_with_debt_payments($account->member_id);
foreach ($devices as $device)
{
// finds buy date of this device
$buy_date = date_parse(date::get_closses_deduct_date_to($device->buy_date));
// finds all debt payments of this device
self::find_debt_payments(
$payments, $buy_date['month'], $buy_date['year'],
$device->price, $device->payment_rate
);
}
// protection from unending loop
$shortened = FALSE;
// finds min and max date = due to prevent before unending loop
$min_fee_date = $this->fee_model->get_min_fromdate_fee_by_type('regular member fee');
$max_fee_date = $this->fee_model->get_max_todate_fee_by_type('regular member fee');
while (true)
{
$date = date::create(date::get_deduct_day_to($month, $year), $month, $year);
// date is bigger/smaller than max/min fee date, ends it (prevent before unending loop)
if (($sign == 1 && $date > $max_fee_date) || ($sign == -1 && $date < $min_fee_date))
{
break;
}
// finds regular member fee for this month
$fee = $this->fee_model->get_regular_member_fee_by_member_date($account->member_id, $date);
// if exist payment for this month, adds it to the fee
if (isset($payments[$year][$month]))
$fee += $payments[$year][$month];
// attributed / deduct fee to / from balance
$balance -= $sign * $fee;
// break if we crossed dept border from any direction
if ($balance * $sign < 0)
{
break;
}
$month += $sign;
if ($month == 0 OR $month == 13)
{
$month = ($month == 13) ? 1 : 12;
$year += $sign;
}
// if we are X years in future, there is no point of counting more
if ($shortened_on_year < $year)
{
$shortened = TRUE;
break;
}
}
$month--;
if ($month == 0)
{
$month = 12;
$year--;
}
$date = date::create(date::days_of_month($month), $month, $year);
// never exceed entrace day with expiration data
if (strtotime($date) < strtotime($entrance_date_str))
{
$date = $entrance_date_str;
}
return new ExpirationCalcResult($date, $shortened);
}
/**
* It stores debt payments into double-dimensional array (indexes year, month)
*
* @author Michal Kliment
* @param array $payments
* @param int $month
* @param int $year
* @param float $payment_left
* @param float $payment_rate
*/
protected static function find_debt_payments(
&$payments, $month, $year, $payment_left, $payment_rate)
{
while ($payment_left > 0)
{
if ($payment_left > $payment_rate)
{
$payment = $payment_rate;
}
else
{
$payment = $payment_left;
}
if (isset($payments[$year][$month]))
{
$payments[$year][$month] += $payment;
}
else
{
$payments[$year][$month] = $payment;
}
$month++;
if ($month > 12)
{
$month = 1;
$year++;
}
$payment_left -= $payment;
}
}
}
/**
* Holds expiration calculation result information.
*/
class ExpirationCalcResult
{
/**
* Calculated expiration in format YYYY-MM-DD.
*
* @var string
*/
public $expiration_date;
/**
* Flag whether the expiration was too long and was shortened.
*
* @var boolean
*/
public $shortened;
public function __construct($expiration_date, $shortened)
{
$this->expiration_date = $expiration_date;
$this->shortened = $shortened;
}
}
application/views/members/show.php
</td>
</tr>
<?php if ($member->type != Member_Model::TYPE_APPLICANT) { ?>
<?php if (isset($expiration_date) && ($entrance_fee_paid == $member->entrance_fee)) { ?>
<?php if (isset($expiration_info) && ($entrance_fee_paid == $member->entrance_fee)) { ?>
<tr>
<th><?php echo __('Payed to').'&nbsp;'.help::hint('payed_to') ?></th>
<td><?php echo $expiration_date ?></td>
<td><span style="color: <?php echo ($account->balance < 0) ? 'red' : 'green' ?>"><?php if ($expiration_info->shortened): ?>&gt; <?php endif; ?><?php echo $expiration_info->expiration_date ?></span></td>
</tr>
<?php } ?>
<?php if (isset($fee)) { ?>
application/views/transfers/show_by_account.php
<?php endforeach; ?>
</td>
</tr>
<?php if (isset($expiration_date)) { ?>
<?php if (isset($expiration_info)) { ?>
<tr>
<th><?php echo __('Payed to')?></th>
<td><?php echo $expiration_date ?></td>
<td><span style="color: <?php echo ($balance < 0) ? 'red' : 'green' ?>"><?php if ($expiration_info->shortened): ?>&gt; <?php endif; ?><?php echo $expiration_info->expiration_date ?></span></td>
</tr>
<?php } ?>
<?php } ?>
system/libraries/ServiceFactory.php
return $this->inject('core\SetupService');
}
/**
* @return \freenetis\service\member\ExpirationCalcService
*/
public function injectMemberExpirationCalc()
{
return $this->inject('member\ExpirationCalcService');
}
}
tests/AbstractItCase.php
{
self::reset_url_settings_to_current();
});
unlink($lck_file);
// get DB connection
self::$connection = Database::instance();
}
tests/application/controllers/api_endpoints/AbstractEndPointTestCase.php
protected $api_account;
/**
* Defines authentification type that is used for connecting to API.
* Defines authentication type that is used for connecting to API.
*
* @var string
*/
protected $auth_method;
/**
* Holds state of settings "api_enabled" during test.
*
* @var bool
*/
private static $old_api_enabled = TRUE;
/**
* Enable API and save old state before tests.
*/
public static function setUpBeforeClass() {
parent::setUpBeforeClass();
self::$old_api_enabled = module::e('api');
Settings::set('api_enabled', TRUE);
}
/**
* Restore old API state after all tests done.
*/
public static function tearDownAfterClass() {
parent::tearDownAfterClass();
Settings::set('api_enabled', self::$old_api_enabled);
}
/**
* Prepare base path and add API account.
*/
tests/application/helpers/dateTest.php
*/
class dateTest extends AbstractItCase
{
/**
* @covers date::create
*/
public function test_create()
{
$this->assertEquals('2017-01-01', date::create(1, 1, 2017));
$this->assertEquals('2017-12-01', date::create(1, 12, 2017));
$this->assertEquals('2017-01-31', date::create(31, 1, 2017));
}
/**
* @covers date::days_of_month
*/
public function test_days_of_month()
{
$this->assertEquals(31, date::days_of_month(1, 2017));
$this->assertEquals(28, date::days_of_month(2, 2014));
$this->assertEquals(28, date::days_of_month(2, 2015));
$this->assertEquals(29, date::days_of_month(2, 2016));
$this->assertEquals(28, date::days_of_month(2, 2017));
$this->assertEquals(31, date::days_of_month(3, 2017));
$this->assertEquals(30, date::days_of_month(4, 2017));
$this->assertEquals(31, date::days_of_month(5, 2017));
$this->assertEquals(30, date::days_of_month(6, 2017));
$this->assertEquals(31, date::days_of_month(7, 2017));
$this->assertEquals(31, date::days_of_month(8, 2017));
$this->assertEquals(30, date::days_of_month(9, 2017));
$this->assertEquals(31, date::days_of_month(10, 2017));
$this->assertEquals(30, date::days_of_month(11, 2017));
$this->assertEquals(31, date::days_of_month(12, 2017));
}
/**
* @covers date::get_next_deduct_date_to
tests/application/libraries/importers/importers.sql
TRUNCATE TABLE `address_points`;
INSERT INTO `address_points` (`id`, `name`, `country_id`, `town_id`, `street_id`, `street_number`, `gps`) VALUES
(1, NULL, 55, 1, 1, '594', NULL),
(2, NULL, 55, 1, 2, '784', ''),
(3, NULL, 55, 2, NULL, '78', ''),
(4, NULL, 55, 3, 3, '454/8a', '');
(2, NULL, 55, 1, 2, '784', NULL),
(3, NULL, 55, 2, NULL, '78', NULL),
(4, NULL, 55, 3, 3, '454/8a', NULL);
TRUNCATE TABLE `allowed_subnets`;
TRUNCATE TABLE `allowed_subnets_counts`;
......
(3, 3, 1, '2004-01-15', '9999-12-31', 1, ''),
(4, 4, 1, '2004-01-15', '9999-12-31', 1, '');
TRUNCATE TABLE `members_traffics_daily`;
TRUNCATE TABLE `members_traffics_monthly`;
TRUNCATE TABLE `members_traffics_yearly`;
TRUNCATE TABLE `members_whitelists`;
INSERT INTO `members_whitelists` (`id`, `member_id`, `permanent`, `since`, `until`, `comment`) VALUES
(1, 1, 1, '2015-01-15', '9999-12-31', NULL);
tests/application/services/member/ExpirationCalcServiceTest.php
<?php
/*
* This file is part of open source system FreenetIS
* and it is release under GPLv3 licence.
*
* More info about licence can be found:
* http://www.gnu.org/licenses/gpl-3.0.html
*
* More info about project can be found:
* http://www.freenetis.org/
*/
namespace freenetis\service\member;
use AbstractItCase;
use Settings;
require_once APPPATH . '/services/member/ExpirationCalcService' . EXT;
/**
* Test case for ExpirationCalcService class.
*
* @author Ondřej Fibich <fibich@freenetis.org>
* @since 1.2
*/
class ExpirationCalcServiceTest extends AbstractItCase
{
/**
* @var ExpirationCalcService
*/
private $object;
/**
* Hold deduct day during test for recover.
*
* @var int day number
*/
private $old_deduct_day;
protected function setUp()
{
$this->object = new ConfigurableTestExpirationCalcService(self::$services);
$this->old_deduct_day = Settings::get('deduct_day');
}
protected function tearDown()
{
Settings::set('deduct_day', $this->old_deduct_day);
}
/**
* @see https://dev.freenetis.org/issues/1076
*/
public function test_get_expiration_info__issue1076()
{
Settings::set('deduct_day', 28);
$this->object->set_fee_model(new TestStaticFeeModel(2, 12.65));
$this->object->set_transfer_model(new TestStaticTransferModel(11, '2017-02-28'));
$account = new \stdClass();
$account->id = 11;
$account->balance = -37.95;
$account->member_id = 2;
$account->member = new \stdClass();
$account->member->entrance_date = '2016-06-01';
$account->member->entrance_fee = 9.9;
$account->member->debt_payment_rate = 0;
$res = $this->object->get_expiration_info($account);
$this->assertFalse($res->shortened);
$this->assertEquals($res->expiration_date, '2016-11-30', $res->expiration_date);
}
public function test_get_expiration_info__no_entrance_fee()
{
Settings::set('deduct_day', 15);
$this->object->set_fee_model(new TestStaticFeeModel(3, 150));
$this->object->set_transfer_model(new TestStaticTransferModel(12, '2017-10-15'));
$account = new \stdClass();
$account->id = 12;
$account->balance = 1500.00;
$account->member_id = 3;
$account->member = new \stdClass();
$account->member->entrance_date = '2017-09-01';
$account->member->entrance_fee = 0;
$account->member->debt_payment_rate = 0;
$res = $this->object->get_expiration_info($account);
$this->assertFalse($res->shortened);
$this->assertEquals('2018-08-31', $res->expiration_date);
// Shortened test
$res_shortened = $this->object->get_expiration_info($account, 2017);
$this->assertTrue($res_shortened->shortened);
$this->assertEquals('2017-12-31', $res_shortened->expiration_date);
}
public function test_get_expiration_info__debt_month()
{
Settings::set('deduct_day', 15);
$this->object->set_fee_model(new TestStaticFeeModel(4, 150));
$this->object->set_transfer_model(new TestStaticTransferModel(13, '2017-10-15'));
$account = new \stdClass();
$account->id = 13;
$account->balance = -150.00;
$account->member_id = 4;
$account->member = new \stdClass();
$account->member->entrance_date = '2017-09-01';
$account->member->entrance_fee = 0;
$account->member->debt_payment_rate = 0;
$res = $this->object->get_expiration_info($account);
$this->assertFalse($res->shortened);
$this->assertEquals('2017-09-30', $res->expiration_date);
}
public function test_get_expiration_info__entrance_fee()
{
Settings::set('deduct_day', 1);
$this->object->set_fee_model(new TestStaticFeeModel(10, 100));
$this->object->set_transfer_model(new TestStaticTransferModel(20, '2017-10-01'));
$account = new \stdClass();
$account->id = 20;
$account->balance = 600.00;
$account->member_id = 10;
$account->member = new \stdClass();
$account->member->entrance_date = '2017-09-01';
$account->member->entrance_fee = 1000;
$account->member->debt_payment_rate = 500;
$res = $this->object->get_expiration_info($account);
$this->assertFalse($res->shortened);
$this->assertEquals('2017-11-30', $res->expiration_date);
}
}
/**
* Special ExpirationCalcService that allows to replace models for testing
* purposes.
*/
class ConfigurableTestExpirationCalcService extends ExpirationCalcService
{
public function set_transfer_model($transfer_model)
{
$this->transfer_model = $transfer_model;
}
public function set_fee_model($fee_model)
{
$this->fee_model = $fee_model;
}
public function set_device_model($device_model)
{
$this->device_model = $device_model;
}
}
class TestStaticTransferModel
{
private $account_id;
private $last_transfer_date;
function __construct($account_id, $last_transfer_date)
{
$this->account_id = $account_id;
$this->last_transfer_date = $last_transfer_date;
}
public function get_last_transfer_datetime_of_account($account_id)
{
return $this->account_id == $account_id ? $this->last_transfer_date : NULL;
}
}
class TestStaticFeeModel
{
private $member_id;
private $fee;
function __construct($member_id, $fee)
{
$this->member_id = $member_id;
$this->fee = $fee;
}
public function get_min_fromdate_fee_by_type($fee_name)
{
return '0000-00-00';
}
public function get_max_todate_fee_by_type($fee_name)
{
return '9999-12-31';
}
public function get_regular_member_fee_by_member_date($member_id, $date)
{
return ($this->member_id == $member_id) ? $this->fee : 0;
}
}
tests/config.sample.ini
; Database configuration
[db]
type = mysql
type = mysqli
user = root
pass =
host = localhost

Také k dispozici: Unified diff