diff --git a/Business/Marketing/CustomerValue.php b/Business/Marketing/CustomerValue.php index 42011e2e3..f0f6152f6 100644 --- a/Business/Marketing/CustomerValue.php +++ b/Business/Marketing/CustomerValue.php @@ -28,8 +28,8 @@ final class CustomerValue * Simple customer lifetime value * * @param float $margin Margin per period - * @param float $retentionRate Rate of remaining customers per period - * @param float $discountRate Cost of capital to discound future revenue + * @param float $retentionRate Rate of remaining customers per period (= average lifetime / (1 + average lifetime)) + * @param float $discountRate Cost of capital to discount future revenue * * @return float * @@ -43,30 +43,30 @@ final class CustomerValue /** * Normalized measure of recurring revenue * - * @param array $revenues Revenues - * @param int $periods Amount of revenue periods - * @param float $cutoff Normalization cutoff (which values should be ignored) + * @param array $revenues Revenues + * @param int $periods Amount of revenue periods + * @param float $lowerCutoff Normalization cutoff (which lower values should be ignored) + * @param float $upperCutoff Normalization cutoff (which upper values should be ignored) * * @return float * * @since 1.0.0 */ - public static function getMRR(array $revenues, int $periods = 12, float $cutoff = 0.1) : float + public static function getMRR(array $revenues, int $periods = 12, float $lowerCutoff = 0.1, float $upperCutoff = 0.0) : float { - if ($cutoff === 0.0) { + if ($lowerCutoff === 0.0 && $upperCutoff === 0.0) { return \array_sum($revenues) / $periods; } - $count = \count($revenues); - $offset = (int) \round($count * $cutoff, 0, \PHP_ROUND_HALF_UP); + \sort($revenues); - if ($offset * 2 >= $count) { - return 0.0; + $sum = 0.0; + foreach ($revenues as $revenue) { + if ($revenue >= $lowerCutoff && $revenue <= $upperCutoff) { + $sum += $revenue; + } } - \sort($revenues); - $normalized = \array_splice($revenues, $offset, $count - $offset); - - return \array_sum($normalized) / $periods; + return $sum / $periods; } } diff --git a/Business/Marketing/Metrics.php b/Business/Marketing/Metrics.php index f97ff12a3..4bef5e7a9 100644 --- a/Business/Marketing/Metrics.php +++ b/Business/Marketing/Metrics.php @@ -87,46 +87,6 @@ final class Metrics - $customers * $acquistionCost; } - /** - * Life time value of a customer - * - * @param \Closure $customerProfit Profit of a customer in year t - * @param float $discountRate Discount rate - * - * @return float - * - * @since 1.0.0 - */ - public static function lifeTimeValue(\Closure $customerProfit, float $discountRate) : float - { - $ltv = 0.0; - for ($i = 1; $i < 1000000; ++$i) { - $ltv += $customerProfit($i) / \pow(1 + $discountRate, $i - 1); - } - - return $ltv; - } - - /** - * Life time value of a customer - * - * @param \Closure $customerProfit Profit of a customer in year t - * @param float $discountRate Discount rate - * - * @return float - * - * @since 1.0.0 - */ - public static function simpleRetentionLifeTimeValue(\Closure $customerProfit, float $discountRate, float $retentionRate) : float - { - $ltv = 0.0; - for ($i = 1; $i < 1000000; ++$i) { - $ltv += $customerProfit($i) * \pow($retentionRate, $i - 1) / \pow(1 + $discountRate, $i - 1); - } - - return $ltv; - } - /** * Calculate the profitability of customers based on their purchase behaviour * @@ -140,7 +100,7 @@ final class Metrics * * @since 1.0.0 */ - public static function calclateMailingSuccess(float $discountRate, array $purchaseProbability, array $payoffs) : Matrix + public static function calculateMailingSuccess(float $discountRate, array $purchaseProbability, array $payoffs) : Matrix { $count = \count($purchaseProbability); $profit = new Matrix($count, $count); diff --git a/DataStorage/Database/DataMapperAbstract.php b/DataStorage/Database/DataMapperAbstract.php index 1428a53f3..77007c62b 100644 --- a/DataStorage/Database/DataMapperAbstract.php +++ b/DataStorage/Database/DataMapperAbstract.php @@ -1846,7 +1846,6 @@ class DataMapperAbstract implements DataMapperInterface } self::deleteModel($obj, $objId, $refClass); - self::clear(); return $objId; @@ -1872,7 +1871,9 @@ class DataMapperAbstract implements DataMapperInterface $obj = []; foreach ($result as $element) { - if (isset($element[static::$primaryField . '_' . $depth]) && self::isInitialized(static::class, $element[static::$primaryField . '_' . $depth], $depth)) { + if (isset($element[static::$primaryField . '_' . $depth]) + && self::isInitialized(static::class, $element[static::$primaryField . '_' . $depth], $depth) + ) { $obj[$element[static::$primaryField . '_' . $depth]] = self::$initObjects[static::class][$element[static::$primaryField . '_' . $depth]['obj']]; continue; @@ -1880,12 +1881,12 @@ class DataMapperAbstract implements DataMapperInterface $toFill = self::createBaseModel(); - if (isset($element[static::$primaryField . '_' . $depth])) { - $obj[$element[static::$primaryField . '_' . $depth]] = self::populateAbstract($element, $toFill, $depth); - self::addInitialized(static::class, $element[static::$primaryField . '_' . $depth], $obj[$element[static::$primaryField . '_' . $depth]], $depth); - } else { + if (!isset($element[static::$primaryField . '_' . $depth])) { throw new \Exception(); } + + $obj[$element[static::$primaryField . '_' . $depth]] = self::populateAbstract($element, $toFill, $depth); + self::addInitialized(static::class, $element[static::$primaryField . '_' . $depth], $obj[$element[static::$primaryField . '_' . $depth]], $depth); } return $obj; @@ -1906,18 +1907,20 @@ class DataMapperAbstract implements DataMapperInterface $obj = []; foreach ($result as $element) { - if (isset($element[static::$primaryField]) && self::isInitializedArray(static::class, $element[static::$primaryField], $depth)) { + if (isset($element[static::$primaryField]) + && self::isInitializedArray(static::class, $element[static::$primaryField], $depth) + ) { $obj[$element[static::$primaryField]] = self::$initArrays[static::class][$element[static::$primaryField]]['obj']; continue; } - if (isset($element[static::$primaryField])) { - $obj[$element[static::$primaryField]] = self::populateAbstractArray($element, [], $depth); - self::addInitializedArray(static::class, $element[static::$primaryField], $obj[$element[static::$primaryField]], $depth); - } else { + if (!isset($element[static::$primaryField])) { throw new \Exception(); } + + $obj[$element[static::$primaryField]] = self::populateAbstractArray($element, [], $depth); + self::addInitializedArray(static::class, $element[static::$primaryField], $obj[$element[static::$primaryField]], $depth); } return $obj; diff --git a/Log/FileLogger.php b/Log/FileLogger.php index f440739f4..1219d2b5c 100644 --- a/Log/FileLogger.php +++ b/Log/FileLogger.php @@ -104,7 +104,7 @@ final class FileLogger implements LoggerInterface $this->verbose = $verbose; if (\is_dir($lpath) || \strpos($lpath, '.') === false) { - $path = \rtrim($lpath, '/') . '/' . \date('Y-m-d') . '.log'; + $path = \rtrim($path !== false ? $path : $lpath, '/') . '/' . \date('Y-m-d') . '.log'; } else { $path = $lpath; } diff --git a/tests/Business/Marketing/CustomerValueTest.php b/tests/Business/Marketing/CustomerValueTest.php index bcd8e914c..220ab89a9 100644 --- a/tests/Business/Marketing/CustomerValueTest.php +++ b/tests/Business/Marketing/CustomerValueTest.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace phpOMS\tests\Business\Marketing; +use phpOMS\Business\Marketing\CustomerValue; + /** * @testdox phpOMS\tests\Business\Marketing\CustomerValueTest: Customer value * @@ -22,18 +24,28 @@ namespace phpOMS\tests\Business\Marketing; class CustomerValueTest extends \PHPUnit\Framework\TestCase { /** + * @testdox The simple customer life time value is correctly calculated * @group framework */ public function testSimpleCLV() : void { - self::markTestIncomplete(); + $margin = 3000; + $years = 10; + $retention = $years / (1 + $years); + self::assertEqualsWithDelta(30000, CustomerValue::getSimpleCLV($margin, $retention, 0.0), 0.1); } /** + * @testdox The monthly recurring revenue (MRR) is correctly calculated * @group framework */ public function testMRR() : void { - self::markTestIncomplete(); + $revenues = [ + 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 + ]; + + self::assertEqualsWithDelta(77.53846, CustomerValue::getMRR($revenues, 13, 10, 1000), 0.01); + self::assertEqualsWithDelta(630.07692307, CustomerValue::getMRR($revenues, 13, 0.0, 0.0), 0.01); } } diff --git a/tests/DataStorage/Cache/Connection/ConnectionFactoryTest.php b/tests/DataStorage/Cache/Connection/ConnectionFactoryTest.php index e466a9adb..680b32a8b 100644 --- a/tests/DataStorage/Cache/Connection/ConnectionFactoryTest.php +++ b/tests/DataStorage/Cache/Connection/ConnectionFactoryTest.php @@ -73,7 +73,7 @@ class ConnectionFactoryTest extends \PHPUnit\Framework\TestCase } /** - * @testdox An invalid cache type results in an exception + * @testdox A invalid cache type results in an exception * @group framework */ public function testInvalidCacheType() : void diff --git a/tests/Module/ModuleManagerTest.php b/tests/Module/ModuleManagerTest.php index 25726dfd6..a37f0043e 100644 --- a/tests/Module/ModuleManagerTest.php +++ b/tests/Module/ModuleManagerTest.php @@ -37,9 +37,10 @@ class ModuleManagerTest extends \PHPUnit\Framework\TestCase protected function setUp() : void { - $this->app = new class() extends ApplicationAbstract { - protected string $appName = 'Api'; - }; + $this->app = new class() extends ApplicationAbstract { + protected string $appName = 'Api'; + }; + $this->app->appName = 'Api'; $this->app->dbPool = $GLOBALS['dbpool']; $this->app->router = new WebRouter(); diff --git a/tests/Module/NullModuleTest.php b/tests/Module/NullModuleTest.php index 2b09896df..f119e5b5b 100644 --- a/tests/Module/NullModuleTest.php +++ b/tests/Module/NullModuleTest.php @@ -17,19 +17,46 @@ namespace phpOMS\tests\Module; require_once __DIR__ . '/../Autoloader.php'; use phpOMS\Application\ApplicationAbstract; +use phpOMS\Log\FileLogger; use phpOMS\Module\NullModule; +use phpOMS\Utils\TestUtils; /** + * @testdox phpOMS\tests\Module\NullModuleTest: Basic module functionality + * * @internal */ final class NullModuleTest extends \PHPUnit\Framework\TestCase { - public function testModule() : void + protected NullModule $module; + + protected function setUp() : void { $app = new class() extends ApplicationAbstract { }; - self::assertInstanceOf('\phpOMS\Module\ModuleAbstract', new NullModule($app)); + $this->module = new NullModule($app); + } + + /** + * @group framework + */ + public function testModule() : void + { + self::assertInstanceOf('\phpOMS\Module\ModuleAbstract', $this->module); + } + + /** + * @testdox A invalid module method call will create an error log + * @covers phpOMS\Module\NullModule + * @group framework + */ + public function testInvalidModuleMethodCalls() : void + { + $this->module->invalidMethodCall(); + + $path = TestUtils::getMember(FileLogger::getInstance(), 'path'); + self::assertStringContainsString('Expected module/controller but got NullModule.', \file_get_contents($path)); } }