<?php
/**
* Created by simpson <simpsonwork@gmail.com>
* Date: 2019-06-26
* Time: 18:00
*/
namespace App\Entity\Sales;
use App\Entity\Location\City;
use App\Repository\PaidPlacementPriceRepository;
use Doctrine\ORM\Mapping as ORM;
use Money\Currency;
use Money\Money;
#[ORM\Table(name: 'paid_service_prices')]
#[ORM\Entity(repositoryClass: PaidPlacementPriceRepository::class)]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'placement_type', type: 'string', length: 32)]
#[ORM\DiscriminatorMap([
'standard_saloon' => Saloon\StandardPlacementPrice::class, 'vip_saloon' => Saloon\VipPlacementPrice::class,
'ultra_vip_saloon' => Saloon\UltraVipPlacementPrice::class, 'standard_masseur' => Profile\MasseurPlacementPrice::class,
'vip_masseur' => Profile\MasseurVipPlacementPrice::class, 'ultra_vip_masseur' => Profile\MasseurUltraVipPlacementPrice::class,
'standard_profile' => Profile\StandardPlacementPrice::class, 'vip_profile' => Profile\VipPlacementPrice::class,
'ultra_vip_profile' => Profile\UltraVipPlacementPrice::class, 'top_profile' => Profile\TopPlacementPrice::class,
'placement_hiding' => PlacementHidingPrice::class
])]
abstract class PaidPlacementPrice
{
const SECONDS_IN_HOUR = 3600;
const SECONDS_IN_DAY = 86400;
public const WEEKDAY_GROUP_MON_THU = 1;
public const WEEKDAY_GROUP_FRI = 2;
public const WEEKDAY_GROUP_SAT_SUN = 3;
public const TIME_GROUP_NIGHT = 1; // 00:00-02:59
public const TIME_GROUP_MORNING = 2; // 03:00-09:59
public const TIME_GROUP_DAY = 3; // 10:00-16:59
public const TIME_GROUP_EVENING = 4; // 17:00-23:59
#[ORM\Id]
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\GeneratedValue(strategy: 'AUTO')]
protected int $id;
#[ORM\JoinColumn(name: 'city_id', referencedColumnName: 'id')]
#[ORM\ManyToOne(targetEntity: City::class)]
protected ?City $city;
#[ORM\Column(name: 'price_amount', type: 'integer', nullable: true)]
protected ?int $priceAmount;
#[ORM\Column(name: 'currency', type: 'string', length: 3)]
protected string $currency;
/**
* Кол-во оплаченного времени по ценнику
*
* 3600 - цена в час
* 86400 - цена в сутки
*/
#[ORM\Column(name: 'duration', type: 'integer')]
protected int $duration;
#[ORM\Column(name: 'enabled', type: 'boolean')]
protected bool $enabled = false;
#[ORM\Column(name: 'city_price_category', type: 'smallint', nullable: true)]
protected ?int $cityPriceCategory = null;
#[ORM\Column(name: 'dynamic_price_matrix', type: 'json', nullable: true)]
protected ?array $dynamicPriceMatrix = null;
public static function createHourlyPrice(?City $city, Money $price): self
{
return new static($city, $price, self::SECONDS_IN_HOUR);
}
public static function createDailyPrice(?City $city, Money $price): self
{
return new static($city, $price, self::SECONDS_IN_DAY);
}
protected function __construct(?City $city, Money $basePrice, int $duration)
{
$this->city = $city;
$this->priceAmount = (int)$basePrice->getAmount();
$this->currency = $basePrice->getCurrency()->getCode();
$this->duration = $duration;
}
public function getId(): int
{
return $this->id;
}
public function getCity(): ?City
{
return $this->city;
}
protected function getBasePrice(): Money
{
if (null === $this->priceAmount) {
throw new \LogicException('Base price is not available for dynamic matrix price.');
}
return new Money($this->priceAmount, new Currency($this->currency));
}
public function getHourlyPrice(): Money
{
if ($this->isDynamicPrice()) {
return $this->resolveDynamicHourlyPriceAt(new \DateTimeImmutable('now'));
}
$basePrice = $this->getBasePrice();
return new Money(+$basePrice->getAmount() * self::SECONDS_IN_HOUR / $this->duration, $basePrice->getCurrency());
}
public function getDailyPrice(): Money
{
if ($this->isDynamicPrice()) {
$hourlyAmount = (int)$this->resolveDynamicHourlyPriceAt(new \DateTimeImmutable('now'))->getAmount();
return new Money($hourlyAmount * 24, new Currency($this->currency));
}
$basePrice = $this->getBasePrice();
return new Money(+$basePrice->getAmount() * self::SECONDS_IN_DAY / $this->duration, $basePrice->getCurrency());
}
public function calculatePriceByTimeRange(\DateTimeInterface $from, \DateTimeInterface $till, ?\DateTimeZone $timezone = null): Money
{
if ($this->isDynamicPrice()) {
return $this->calculateDynamicPriceByTimeRange($from, $till, $timezone);
}
$diffInSeconds = abs($till->getTimestamp() - $from->getTimestamp());
$basePrice = $this->getBasePrice();
return new Money(+$basePrice->getAmount() * $diffInSeconds / $this->duration, $basePrice->getCurrency());
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
}
public function getCityPriceCategory(): ?int
{
return $this->cityPriceCategory;
}
public function setDynamicPriceMatrix(int $cityPriceCategory, array $matrix): void
{
if ($cityPriceCategory < City::PRICE_CATEGORY_1 || $cityPriceCategory > City::PRICE_CATEGORY_5) {
throw new \InvalidArgumentException(sprintf('Unexpected city price category %d.', $cityPriceCategory));
}
$normalizedMatrix = [];
foreach ([self::WEEKDAY_GROUP_MON_THU, self::WEEKDAY_GROUP_FRI, self::WEEKDAY_GROUP_SAT_SUN] as $weekdayGroup) {
if (!isset($matrix[$weekdayGroup]) || !is_array($matrix[$weekdayGroup])) {
throw new \InvalidArgumentException(sprintf('Dynamic matrix weekday group %d is missing.', $weekdayGroup));
}
$normalizedMatrix[(string)$weekdayGroup] = [];
foreach ([self::TIME_GROUP_NIGHT, self::TIME_GROUP_MORNING, self::TIME_GROUP_DAY, self::TIME_GROUP_EVENING] as $timeGroup) {
if (!array_key_exists($timeGroup, $matrix[$weekdayGroup])) {
throw new \InvalidArgumentException(sprintf('Dynamic matrix slot %d/%d is missing.', $weekdayGroup, $timeGroup));
}
$amount = (int)$matrix[$weekdayGroup][$timeGroup];
if ($amount < 0) {
throw new \InvalidArgumentException(sprintf('Dynamic matrix slot %d/%d should not be negative.', $weekdayGroup, $timeGroup));
}
$normalizedMatrix[(string)$weekdayGroup][(string)$timeGroup] = $amount;
}
}
$this->city = null;
$this->cityPriceCategory = $cityPriceCategory;
$this->dynamicPriceMatrix = $normalizedMatrix;
$this->priceAmount = null;
$this->duration = self::SECONDS_IN_DAY;
}
public function isDynamicPrice(): bool
{
return null !== $this->dynamicPriceMatrix;
}
public function getDynamicPriceMatrix(): ?array
{
return $this->dynamicPriceMatrix;
}
public function resolveDynamicHourlyPriceAt(\DateTimeInterface $dateTime, ?\DateTimeZone $timezone = null): Money
{
if (!$this->isDynamicPrice()) {
return $this->getHourlyPrice();
}
$matrix = $this->dynamicPriceMatrix;
if (null === $matrix) {
throw new \LogicException('Dynamic price matrix is not set.');
}
$dateTimeInTimezone = \DateTimeImmutable::createFromInterface($dateTime)->setTimezone($this->resolveTimezone($timezone));
$weekdayGroup = self::resolveWeekdayGroup($dateTimeInTimezone);
$timeGroup = self::resolveTimeGroup($dateTimeInTimezone);
$amount = (int)($matrix[(string)$weekdayGroup][(string)$timeGroup] ?? 0);
return new Money($amount, new Currency($this->currency));
}
abstract static public function getType(): string;
public static function resolveWeekdayGroup(\DateTimeInterface $dateTime): int
{
$weekDay = (int)$dateTime->format('N');
if ($weekDay === 5) {
return self::WEEKDAY_GROUP_FRI;
}
if ($weekDay >= 6) {
return self::WEEKDAY_GROUP_SAT_SUN;
}
return self::WEEKDAY_GROUP_MON_THU;
}
public static function resolveTimeGroup(\DateTimeInterface $dateTime): int
{
$hour = (int)$dateTime->format('G');
if ($hour <= 2) {
return self::TIME_GROUP_NIGHT;
}
if ($hour <= 9) {
return self::TIME_GROUP_MORNING;
}
if ($hour <= 16) {
return self::TIME_GROUP_DAY;
}
return self::TIME_GROUP_EVENING;
}
private function calculateDynamicPriceByTimeRange(\DateTimeInterface $from, \DateTimeInterface $till, ?\DateTimeZone $timezone = null): Money
{
$fromDate = \DateTimeImmutable::createFromInterface($from);
$tillDate = \DateTimeImmutable::createFromInterface($till);
if ($tillDate <= $fromDate) {
return new Money(0, new Currency($this->currency));
}
$slotTimezone = $this->resolveTimezone($timezone);
$amountNumerator = 0;
$cursor = $fromDate;
while ($cursor < $tillDate) {
$nextHour = $cursor->modify('+1 hour');
if ($nextHour > $tillDate) {
$nextHour = $tillDate;
}
$hourlyPrice = $this->resolveDynamicHourlyPriceAt($cursor, $slotTimezone);
$seconds = $nextHour->getTimestamp() - $cursor->getTimestamp();
$amountNumerator += ((int)$hourlyPrice->getAmount() * $seconds);
$cursor = $nextHour;
}
return new Money((string)intdiv($amountNumerator, self::SECONDS_IN_HOUR), new Currency($this->currency));
}
private function resolveTimezone(?\DateTimeZone $timezone): \DateTimeZone
{
if (null !== $timezone) {
return $timezone;
}
if (null !== $this->city && null !== $this->city->getTimezone()) {
return new \DateTimeZone($this->city->getTimezone());
}
return new \DateTimeZone('UTC');
}
}