ApiService.php
11 months ago
ElementService.php
11 months ago
EmailService.php
11 months ago
FormService.php
11 months ago
LevelOrderService.php
11 months ago
LevelService.php
11 months ago
MembershipService.php
11 months ago
RedirectService.php
11 months ago
SanitizationService.php
11 months ago
StatisticsService.php
11 months ago
UserService.php
11 months ago
StatisticsService.php
427 lines
| 1 | <?php |
| 2 | |
| 3 | namespace FapiMember\Service; |
| 4 | |
| 5 | use DateTimeImmutable; |
| 6 | use FapiMember\Container\Container; |
| 7 | use FapiMember\Model\Enums\Format; |
| 8 | use FapiMember\Model\Enums\Types\MembershipChangeType; |
| 9 | use FapiMember\Model\Membership; |
| 10 | use FapiMember\Model\MembershipChange; |
| 11 | use FapiMember\Repository\LevelRepository; |
| 12 | use FapiMember\Repository\MemberActivityRepository; |
| 13 | use FapiMember\Repository\MembershipChangeRepository; |
| 14 | use FapiMember\Repository\UserRepository; |
| 15 | use FapiMember\Utils\DateTimeHelper; |
| 16 | |
| 17 | class StatisticsService |
| 18 | { |
| 19 | private MembershipChangeRepository $membershipChangeRepository; |
| 20 | private MemberActivityRepository $memberActivityRepository; |
| 21 | private LevelRepository $levelRepository; |
| 22 | private UserRepository $userRepository; |
| 23 | |
| 24 | public function __construct() |
| 25 | { |
| 26 | $this->membershipChangeRepository = Container::get(MembershipChangeRepository::class); |
| 27 | $this->memberActivityRepository = Container::get(MemberActivityRepository::class); |
| 28 | $this->levelRepository = Container::get(LevelRepository::class); |
| 29 | $this->userRepository = Container::get(UserRepository::class); |
| 30 | } |
| 31 | |
| 32 | /** |
| 33 | * @param array<Membership> $oldMemberships |
| 34 | * @param array<Membership> $newMemberships |
| 35 | */ |
| 36 | public function saveChanges(array $oldMemberships, array $newMemberships): void |
| 37 | { |
| 38 | $missingMemberships = $oldMemberships; |
| 39 | $createdMemberships = $newMemberships; |
| 40 | |
| 41 | foreach ($oldMemberships as $oldKey => $oldMembership) { |
| 42 | foreach ($newMemberships as $newKey => $newMembership) { |
| 43 | if ($oldMembership->getLevelId() === $newMembership->getLevelId()) { |
| 44 | unset($missingMemberships[$oldKey]); |
| 45 | unset($createdMemberships[$newKey]); |
| 46 | |
| 47 | if ($oldMembership->getUntil() !== null && |
| 48 | ($newMembership->getUntil() === null || |
| 49 | $oldMembership->getUntil() < $newMembership->getUntil()) |
| 50 | ) { |
| 51 | $this->membershipChangeRepository->addChange( |
| 52 | $newMembership->toMembershipChange(MembershipChangeType::EXTENDED), |
| 53 | ); |
| 54 | } elseif ( |
| 55 | $oldMembership->getUntil()?->format(Format::DATE_TIME) !== $newMembership->getUntil()?->format(Format::DATE_TIME) || |
| 56 | $oldMembership->getRegistered()->format(Format::DATE_TIME) !== $newMembership->getRegistered()->format(Format::DATE_TIME) |
| 57 | ) { |
| 58 | $this->membershipChangeRepository->addChange( |
| 59 | $newMembership->toMembershipChange(MembershipChangeType::UPDATED), |
| 60 | ); |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | foreach ($missingMemberships as $membership) { |
| 67 | if ($membership->getUntil() !== null && $membership->getUntil() < DateTimeHelper::getNow()) { |
| 68 | $this->membershipChangeRepository->addChange( |
| 69 | $membership->toMembershipChange(MembershipChangeType::EXPIRED), |
| 70 | ); |
| 71 | } else { |
| 72 | $this->membershipChangeRepository->addChange( |
| 73 | $membership->toMembershipChange(MembershipChangeType::DELETED), |
| 74 | ); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | foreach ($createdMemberships as $membership) { |
| 79 | $this->membershipChangeRepository->addChange( |
| 80 | $membership->toMembershipChange(MembershipChangeType::CREATED), |
| 81 | ); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | public function getMemberCountsForPeriod( |
| 86 | DateTimeImmutable $dateFrom, |
| 87 | DateTimeImmutable $dateTo, |
| 88 | array $levelIds, |
| 89 | bool $groupLevels, |
| 90 | ): array |
| 91 | { |
| 92 | $periods = DateTimeHelper::calculateGraphPeriods($dateFrom, $dateTo); |
| 93 | $data = []; |
| 94 | |
| 95 | foreach ($periods as $period) { |
| 96 | $dateString = $period['date_to']->format(Format::DATE_CZECH); |
| 97 | |
| 98 | if ($groupLevels) { |
| 99 | $data[$dateString]['Počet'] = 0; |
| 100 | } |
| 101 | |
| 102 | $lastChanges = $this->membershipChangeRepository->getLastChangesForLevels( |
| 103 | $levelIds, |
| 104 | $period['date_to'], |
| 105 | ); |
| 106 | |
| 107 | $counts = $this->calculateActiveMembershipCounts($lastChanges, $levelIds); |
| 108 | |
| 109 | foreach ($counts as $levelId => $count) { |
| 110 | if ($groupLevels) { |
| 111 | $data[$dateString]['Počet'] += $count; |
| 112 | } else { |
| 113 | $data[$dateString][$this->levelRepository->getLevelById($levelId)->getName()] |
| 114 | = $count; |
| 115 | } |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | return $data; |
| 120 | } |
| 121 | |
| 122 | public function getMemberCountChangesForPeriod( |
| 123 | DateTimeImmutable $dateFrom, |
| 124 | DateTimeImmutable $dateTo, |
| 125 | array $levelIds, |
| 126 | bool $groupLevels, |
| 127 | ): array |
| 128 | { |
| 129 | $periods = DateTimeHelper::calculateGraphPeriods($dateFrom, $dateTo); |
| 130 | $data = []; |
| 131 | |
| 132 | foreach ($periods as $period) { |
| 133 | $dateString = $period['date_to']->format(Format::DATE_CZECH); |
| 134 | |
| 135 | if ($groupLevels) { |
| 136 | $data[$dateString]['+'] = 0; |
| 137 | $data[$dateString]['-'] = 0; |
| 138 | } |
| 139 | |
| 140 | $changesBefore = $this->membershipChangeRepository->getLastChangesForLevels( |
| 141 | $levelIds, |
| 142 | $period['date_from'], |
| 143 | ); |
| 144 | |
| 145 | $changesAfter = $this->membershipChangeRepository->getLastChangesForLevels( |
| 146 | $levelIds, |
| 147 | $period['date_to'], |
| 148 | ); |
| 149 | |
| 150 | $lostCounts = $this->calculateActiveMembershipChangedCounts( |
| 151 | $changesBefore, |
| 152 | $changesAfter, |
| 153 | $levelIds, |
| 154 | true, |
| 155 | ); |
| 156 | |
| 157 | $gainedCounts = $this->calculateActiveMembershipChangedCounts( |
| 158 | $changesBefore, |
| 159 | $changesAfter, |
| 160 | $levelIds, |
| 161 | ); |
| 162 | |
| 163 | foreach ($lostCounts as $levelId => $count) { |
| 164 | if ($groupLevels) { |
| 165 | $data[$dateString]['-'] += $count; |
| 166 | } else { |
| 167 | $data |
| 168 | [$dateString] |
| 169 | ['- ' . $this->levelRepository->getLevelById($levelId)->getName()] |
| 170 | = $count; |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | foreach ($gainedCounts as $levelId => $count) { |
| 175 | if ($groupLevels) { |
| 176 | $data[$dateString]['+'] += $count; |
| 177 | } else { |
| 178 | $data |
| 179 | [$dateString] |
| 180 | ['+ ' . $this->levelRepository->getLevelById($levelId)->getName()] |
| 181 | = $count; |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | } |
| 186 | |
| 187 | return $data; |
| 188 | } |
| 189 | |
| 190 | public function getAcquisitionOrChurnRate( |
| 191 | DateTimeImmutable $dateFrom, |
| 192 | DateTimeImmutable $dateTo, |
| 193 | array $levelIds, |
| 194 | bool $groupLevels, |
| 195 | bool $calculateChurn = true, |
| 196 | ): array |
| 197 | { |
| 198 | $data = []; |
| 199 | $arrayKey = $calculateChurn ? 'Churn rate' : 'Acquisition rate'; |
| 200 | |
| 201 | $changesBefore = $this->membershipChangeRepository->getLastChangesForLevels( |
| 202 | $levelIds, |
| 203 | $dateFrom, |
| 204 | ); |
| 205 | |
| 206 | $changesAfter = $this->membershipChangeRepository->getLastChangesForLevels( |
| 207 | $levelIds, |
| 208 | $dateTo, |
| 209 | ); |
| 210 | |
| 211 | $changedCounts = $this->calculateActiveMembershipChangedCounts( |
| 212 | $changesBefore, |
| 213 | $changesAfter, |
| 214 | $levelIds, |
| 215 | $calculateChurn, |
| 216 | ); |
| 217 | |
| 218 | $activeCounts = $this->calculateActiveMembershipCounts($changesBefore, $levelIds); |
| 219 | |
| 220 | $totalActiveCount = 0; |
| 221 | $totalChangedCount = 0; |
| 222 | |
| 223 | foreach ($changedCounts as $levelId => $changedCount) { |
| 224 | if (!isset($activeCounts[$levelId]) || $activeCounts[$levelId] === 0) { |
| 225 | continue; |
| 226 | } |
| 227 | |
| 228 | if ($groupLevels) { |
| 229 | $totalActiveCount += $activeCounts[$levelId]; |
| 230 | $totalChangedCount += abs($changedCount); |
| 231 | } else { |
| 232 | $levelName = $this->levelRepository->getLevelById($levelId)->getName(); |
| 233 | |
| 234 | $data[$levelName] = [ |
| 235 | $levelName => number_format((abs($changedCount) / $activeCounts[$levelId]) * 100, 2), |
| 236 | ]; |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | if ($groupLevels && $totalActiveCount !== 0) { |
| 241 | $data[$arrayKey][$arrayKey] = number_format((abs($totalChangedCount) / $totalActiveCount) * 100, 2); |
| 242 | } elseif ($groupLevels) { |
| 243 | $data[$arrayKey][$arrayKey] = 0; |
| 244 | } |
| 245 | |
| 246 | return $data; |
| 247 | } |
| 248 | |
| 249 | public function getActiveCountsForPeriod( |
| 250 | DateTimeImmutable $dateFrom, |
| 251 | DateTimeImmutable $dateTo, |
| 252 | ): array |
| 253 | { |
| 254 | $periods = DateTimeHelper::calculateGraphPeriods($dateFrom, $dateTo); |
| 255 | $data = []; |
| 256 | |
| 257 | foreach ($periods as $period) { |
| 258 | $dateString = $period['date_to']->format(Format::DATE_CZECH); |
| 259 | |
| 260 | $data[$dateString]['Aktivních'] = count($this->memberActivityRepository->getAllForPeriod($period['date_from'], $period['date_to'])); |
| 261 | } |
| 262 | |
| 263 | return $data; |
| 264 | } |
| 265 | |
| 266 | public function getAverageChurnRatePeriodsForLevels( |
| 267 | bool $groupLevels, |
| 268 | array $levelIds, |
| 269 | ): array |
| 270 | { |
| 271 | $firstCreatedChanges = $this->membershipChangeRepository |
| 272 | ->getFirstCreatedForLevels($levelIds); |
| 273 | |
| 274 | $totalCount = count($firstCreatedChanges); |
| 275 | |
| 276 | if ($totalCount === 0) { |
| 277 | return []; |
| 278 | } |
| 279 | |
| 280 | $graphColumns = [ |
| 281 | 'Měsíc' => '+1 month', |
| 282 | '2 Měsíce' => '+2 months', |
| 283 | '3 Měsíce' => '+3 months', |
| 284 | '4 Měsíce' => '+4 months', |
| 285 | '5 Měsíců' => '+5 months', |
| 286 | 'Půl roku' => '+6 months', |
| 287 | 'Rok' => '+1 year', |
| 288 | '2 Roky' => '+2 years', |
| 289 | ]; |
| 290 | |
| 291 | $churnedOutCounts = []; |
| 292 | |
| 293 | foreach ($firstCreatedChanges as $firstCreatedChange) { |
| 294 | $startDate = $firstCreatedChange->getTimestamp(); |
| 295 | $levelKey = $groupLevels |
| 296 | ? 'Churn rate' |
| 297 | : $this->levelRepository |
| 298 | ->getLevelById($firstCreatedChange->getLevelId()) |
| 299 | ?->getName(); |
| 300 | |
| 301 | foreach ($graphColumns as $key => $value) { |
| 302 | if (!isset($churnedOutCounts[$key][$levelKey])) { |
| 303 | $churnedOutCounts[$key][$levelKey] = 0; |
| 304 | } |
| 305 | |
| 306 | $endDate = $startDate->modify($value)->modify('-1 day'); |
| 307 | |
| 308 | if ($endDate > DateTimeHelper::getNow()) { |
| 309 | break; |
| 310 | } |
| 311 | |
| 312 | $lastChange = $this->membershipChangeRepository->findLastChange( |
| 313 | $firstCreatedChange->getUserId(), |
| 314 | $firstCreatedChange->getLevelId(), |
| 315 | $endDate, |
| 316 | ); |
| 317 | |
| 318 | if ($firstCreatedChange->isActive() && !$lastChange->isActive()) { |
| 319 | $churnedOutCounts[$key][$levelKey]++; |
| 320 | break; |
| 321 | } |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | $churnedOutRates = []; |
| 326 | |
| 327 | foreach ($churnedOutCounts as $key => $churnedOutPeriod) { |
| 328 | foreach ($churnedOutPeriod as $levelKey => $churnedOutLevel) { |
| 329 | $churnedOutRates[$key][$levelKey] = number_format(($churnedOutLevel) / $totalCount * 100, 2); |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | return $churnedOutRates; |
| 334 | } |
| 335 | |
| 336 | /** @param array<MembershipChange> $changes */ |
| 337 | private function calculateActiveMembershipCounts(array $changes, array $levelIds): array |
| 338 | { |
| 339 | $data = []; |
| 340 | |
| 341 | $levels = $this->levelRepository->getAllAsLevels(); |
| 342 | |
| 343 | foreach ($levels as $level) { |
| 344 | if (in_array($level->getId(), $levelIds) || $levelIds === []) { |
| 345 | $data[$level->getId()] = 0; |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | foreach ($changes as $change) { |
| 350 | if (!isset($data[$change->getLevelId()])) { |
| 351 | $data[$change->getLevelId()] = 0; |
| 352 | } |
| 353 | |
| 354 | if ($change->isActive()) { |
| 355 | $data[$change->getLevelId()]++; |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | return $data; |
| 360 | } |
| 361 | |
| 362 | /** |
| 363 | * @param array<MembershipChange> $changesBefore |
| 364 | * @param array<MembershipChange> $changesAfter |
| 365 | */ |
| 366 | private function calculateActiveMembershipChangedCounts( |
| 367 | array $changesBefore, |
| 368 | array $changesAfter, |
| 369 | array $levelIds, |
| 370 | bool $countLost = false, |
| 371 | ): array |
| 372 | { |
| 373 | $data = []; |
| 374 | |
| 375 | $levels = $this->levelRepository->getAllAsLevels(); |
| 376 | |
| 377 | foreach ($levels as $level) { |
| 378 | if (in_array($level->getId(), $levelIds) || $levelIds === []) { |
| 379 | $data[$level->getId()] = 0; |
| 380 | } |
| 381 | } |
| 382 | |
| 383 | foreach ($changesAfter as $index => $change) { |
| 384 | $activeBefore = isset($changesBefore[$index]) && $changesBefore[$index]->isActive(); |
| 385 | |
| 386 | $activeAfter = $change->isActive(); |
| 387 | |
| 388 | $changed = $countLost |
| 389 | ? ($activeBefore && !$activeAfter) |
| 390 | : (!$activeBefore && $activeAfter); |
| 391 | |
| 392 | |
| 393 | if ($changed) { |
| 394 | $data[$change->getLevelId()] += $countLost ? -1 : 1; |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | return $data; |
| 399 | } |
| 400 | |
| 401 | public function handleUserActive(): void |
| 402 | { |
| 403 | $user = $this->userRepository->getCurrentUser(); |
| 404 | if ($user === null || !$user->isMemberOrSubscriber() || !$this->memberActivityRepository->tableExists()) { |
| 405 | return; |
| 406 | } |
| 407 | |
| 408 | $now = DateTimeHelper::getNow(); |
| 409 | |
| 410 | $dayStart = $now->setTime(0, 0, 0); |
| 411 | $dayEnd = $now->setTime(23, 59, 59); |
| 412 | |
| 413 | $activityToday = $this->memberActivityRepository->getOneForPeriod( |
| 414 | $user->getId(), |
| 415 | $dayStart, |
| 416 | $dayEnd, |
| 417 | ); |
| 418 | |
| 419 | if (count($activityToday) < 1) { |
| 420 | $this->memberActivityRepository->addActivity($user->getId()); |
| 421 | } else { |
| 422 | $this->memberActivityRepository->updateActivity($user->getId()); |
| 423 | } |
| 424 | } |
| 425 | |
| 426 | } |
| 427 |