src/Entity/Sales/PaidPlacementPrice.php line 28

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by simpson <simpsonwork@gmail.com>
  4.  * Date: 2019-06-26
  5.  * Time: 18:00
  6.  */
  7. namespace App\Entity\Sales;
  8. use App\Entity\Location\City;
  9. use App\Repository\PaidPlacementPriceRepository;
  10. use Doctrine\ORM\Mapping as ORM;
  11. use Money\Currency;
  12. use Money\Money;
  13. #[ORM\Table(name'paid_service_prices')]
  14. #[ORM\Entity(repositoryClassPaidPlacementPriceRepository::class)]
  15. #[ORM\InheritanceType('SINGLE_TABLE')]
  16. #[ORM\DiscriminatorColumn(name'placement_type'type'string'length32)]
  17. #[ORM\DiscriminatorMap([
  18.     'standard_saloon' => Saloon\StandardPlacementPrice::class, 'vip_saloon' => Saloon\VipPlacementPrice::class,
  19.     'ultra_vip_saloon' => Saloon\UltraVipPlacementPrice::class, 'standard_masseur' => Profile\MasseurPlacementPrice::class,
  20.     'vip_masseur' => Profile\MasseurVipPlacementPrice::class, 'ultra_vip_masseur' => Profile\MasseurUltraVipPlacementPrice::class,
  21.     'standard_profile' => Profile\StandardPlacementPrice::class, 'vip_profile' => Profile\VipPlacementPrice::class,
  22.     'ultra_vip_profile' => Profile\UltraVipPlacementPrice::class, 'top_profile' => Profile\TopPlacementPrice::class,
  23.     'placement_hiding' => PlacementHidingPrice::class
  24. ])]
  25. abstract class PaidPlacementPrice
  26. {
  27.     const SECONDS_IN_HOUR 3600;
  28.     const SECONDS_IN_DAY 86400;
  29.     public const WEEKDAY_GROUP_MON_THU 1;
  30.     public const WEEKDAY_GROUP_FRI 2;
  31.     public const WEEKDAY_GROUP_SAT_SUN 3;
  32.     public const TIME_GROUP_NIGHT 1// 00:00-02:59
  33.     public const TIME_GROUP_MORNING 2// 03:00-09:59
  34.     public const TIME_GROUP_DAY 3// 10:00-16:59
  35.     public const TIME_GROUP_EVENING 4// 17:00-23:59
  36.     #[ORM\Id]
  37.     #[ORM\Column(name'id'type'integer')]
  38.     #[ORM\GeneratedValue(strategy'AUTO')]
  39.     protected int $id;
  40.     #[ORM\JoinColumn(name'city_id'referencedColumnName'id')]
  41.     #[ORM\ManyToOne(targetEntityCity::class)]
  42.     protected ?City $city;
  43.     #[ORM\Column(name'price_amount'type'integer'nullabletrue)]
  44.     protected ?int $priceAmount;
  45.     #[ORM\Column(name'currency'type'string'length3)]
  46.     protected string $currency;
  47.     /**
  48.      * Кол-во оплаченного времени по ценнику
  49.      *
  50.      * 3600 - цена в час
  51.      * 86400 - цена в сутки
  52.      */
  53.     #[ORM\Column(name'duration'type'integer')]
  54.     protected int $duration;
  55.     #[ORM\Column(name'enabled'type'boolean')]
  56.     protected bool $enabled false;
  57.     #[ORM\Column(name'city_price_category'type'smallint'nullabletrue)]
  58.     protected ?int $cityPriceCategory null;
  59.     #[ORM\Column(name'dynamic_price_matrix'type'json'nullabletrue)]
  60.     protected ?array $dynamicPriceMatrix null;
  61.     public static function createHourlyPrice(?City $cityMoney $price): self
  62.     {
  63.         return new static($city$priceself::SECONDS_IN_HOUR);
  64.     }
  65.     public static function createDailyPrice(?City $cityMoney $price): self
  66.     {
  67.         return new static($city$priceself::SECONDS_IN_DAY);
  68.     }
  69.     protected function __construct(?City $cityMoney $basePriceint $duration)
  70.     {
  71.         $this->city $city;
  72.         $this->priceAmount = (int)$basePrice->getAmount();
  73.         $this->currency $basePrice->getCurrency()->getCode();
  74.         $this->duration $duration;
  75.     }
  76.     public function getId(): int
  77.     {
  78.         return $this->id;
  79.     }
  80.     public function getCity(): ?City
  81.     {
  82.         return $this->city;
  83.     }
  84.     protected function getBasePrice(): Money
  85.     {
  86.         if (null === $this->priceAmount) {
  87.             throw new \LogicException('Base price is not available for dynamic matrix price.');
  88.         }
  89.         return new Money($this->priceAmount, new Currency($this->currency));
  90.     }
  91.     public function getHourlyPrice(): Money
  92.     {
  93.         if ($this->isDynamicPrice()) {
  94.             return $this->resolveDynamicHourlyPriceAt(new \DateTimeImmutable('now'));
  95.         }
  96.         $basePrice $this->getBasePrice();
  97.         return new Money(+$basePrice->getAmount() * self::SECONDS_IN_HOUR $this->duration$basePrice->getCurrency());
  98.     }
  99.     public function getDailyPrice(): Money
  100.     {
  101.         if ($this->isDynamicPrice()) {
  102.             $hourlyAmount = (int)$this->resolveDynamicHourlyPriceAt(new \DateTimeImmutable('now'))->getAmount();
  103.             return new Money($hourlyAmount 24, new Currency($this->currency));
  104.         }
  105.         $basePrice $this->getBasePrice();
  106.         return new Money(+$basePrice->getAmount() * self::SECONDS_IN_DAY $this->duration$basePrice->getCurrency());
  107.     }
  108.     public function calculatePriceByTimeRange(\DateTimeInterface $from\DateTimeInterface $till, ?\DateTimeZone $timezone null): Money
  109.     {
  110.         if ($this->isDynamicPrice()) {
  111.             return $this->calculateDynamicPriceByTimeRange($from$till$timezone);
  112.         }
  113.         $diffInSeconds abs($till->getTimestamp() - $from->getTimestamp());
  114.         $basePrice $this->getBasePrice();
  115.         return new Money(+$basePrice->getAmount() * $diffInSeconds $this->duration$basePrice->getCurrency());
  116.     }
  117.     public function isEnabled(): bool
  118.     {
  119.         return $this->enabled;
  120.     }
  121.     public function setEnabled(bool $enabled): void
  122.     {
  123.         $this->enabled $enabled;
  124.     }
  125.     public function getCityPriceCategory(): ?int
  126.     {
  127.         return $this->cityPriceCategory;
  128.     }
  129.     public function setDynamicPriceMatrix(int $cityPriceCategory, array $matrix): void
  130.     {
  131.         if ($cityPriceCategory City::PRICE_CATEGORY_1 || $cityPriceCategory City::PRICE_CATEGORY_5) {
  132.             throw new \InvalidArgumentException(sprintf('Unexpected city price category %d.'$cityPriceCategory));
  133.         }
  134.         $normalizedMatrix = [];
  135.         foreach ([self::WEEKDAY_GROUP_MON_THUself::WEEKDAY_GROUP_FRIself::WEEKDAY_GROUP_SAT_SUN] as $weekdayGroup) {
  136.             if (!isset($matrix[$weekdayGroup]) || !is_array($matrix[$weekdayGroup])) {
  137.                 throw new \InvalidArgumentException(sprintf('Dynamic matrix weekday group %d is missing.'$weekdayGroup));
  138.             }
  139.             $normalizedMatrix[(string)$weekdayGroup] = [];
  140.             foreach ([self::TIME_GROUP_NIGHTself::TIME_GROUP_MORNINGself::TIME_GROUP_DAYself::TIME_GROUP_EVENING] as $timeGroup) {
  141.                 if (!array_key_exists($timeGroup$matrix[$weekdayGroup])) {
  142.                     throw new \InvalidArgumentException(sprintf('Dynamic matrix slot %d/%d is missing.'$weekdayGroup$timeGroup));
  143.                 }
  144.                 $amount = (int)$matrix[$weekdayGroup][$timeGroup];
  145.                 if ($amount 0) {
  146.                     throw new \InvalidArgumentException(sprintf('Dynamic matrix slot %d/%d should not be negative.'$weekdayGroup$timeGroup));
  147.                 }
  148.                 $normalizedMatrix[(string)$weekdayGroup][(string)$timeGroup] = $amount;
  149.             }
  150.         }
  151.         $this->city null;
  152.         $this->cityPriceCategory $cityPriceCategory;
  153.         $this->dynamicPriceMatrix $normalizedMatrix;
  154.         $this->priceAmount null;
  155.         $this->duration self::SECONDS_IN_DAY;
  156.     }
  157.     public function isDynamicPrice(): bool
  158.     {
  159.         return null !== $this->dynamicPriceMatrix;
  160.     }
  161.     public function getDynamicPriceMatrix(): ?array
  162.     {
  163.         return $this->dynamicPriceMatrix;
  164.     }
  165.     public function resolveDynamicHourlyPriceAt(\DateTimeInterface $dateTime, ?\DateTimeZone $timezone null): Money
  166.     {
  167.         if (!$this->isDynamicPrice()) {
  168.             return $this->getHourlyPrice();
  169.         }
  170.         $matrix $this->dynamicPriceMatrix;
  171.         if (null === $matrix) {
  172.             throw new \LogicException('Dynamic price matrix is not set.');
  173.         }
  174.         $dateTimeInTimezone \DateTimeImmutable::createFromInterface($dateTime)->setTimezone($this->resolveTimezone($timezone));
  175.         $weekdayGroup self::resolveWeekdayGroup($dateTimeInTimezone);
  176.         $timeGroup self::resolveTimeGroup($dateTimeInTimezone);
  177.         $amount = (int)($matrix[(string)$weekdayGroup][(string)$timeGroup] ?? 0);
  178.         return new Money($amount, new Currency($this->currency));
  179.     }
  180.     abstract static public function getType(): string;
  181.     public static function resolveWeekdayGroup(\DateTimeInterface $dateTime): int
  182.     {
  183.         $weekDay = (int)$dateTime->format('N');
  184.         if ($weekDay === 5) {
  185.             return self::WEEKDAY_GROUP_FRI;
  186.         }
  187.         if ($weekDay >= 6) {
  188.             return self::WEEKDAY_GROUP_SAT_SUN;
  189.         }
  190.         return self::WEEKDAY_GROUP_MON_THU;
  191.     }
  192.     public static function resolveTimeGroup(\DateTimeInterface $dateTime): int
  193.     {
  194.         $hour = (int)$dateTime->format('G');
  195.         if ($hour <= 2) {
  196.             return self::TIME_GROUP_NIGHT;
  197.         }
  198.         if ($hour <= 9) {
  199.             return self::TIME_GROUP_MORNING;
  200.         }
  201.         if ($hour <= 16) {
  202.             return self::TIME_GROUP_DAY;
  203.         }
  204.         return self::TIME_GROUP_EVENING;
  205.     }
  206.     private function calculateDynamicPriceByTimeRange(\DateTimeInterface $from\DateTimeInterface $till, ?\DateTimeZone $timezone null): Money
  207.     {
  208.         $fromDate \DateTimeImmutable::createFromInterface($from);
  209.         $tillDate \DateTimeImmutable::createFromInterface($till);
  210.         if ($tillDate <= $fromDate) {
  211.             return new Money(0, new Currency($this->currency));
  212.         }
  213.         $slotTimezone $this->resolveTimezone($timezone);
  214.         $amountNumerator 0;
  215.         $cursor $fromDate;
  216.         while ($cursor $tillDate) {
  217.             $nextHour $cursor->modify('+1 hour');
  218.             if ($nextHour $tillDate) {
  219.                 $nextHour $tillDate;
  220.             }
  221.             $hourlyPrice $this->resolveDynamicHourlyPriceAt($cursor$slotTimezone);
  222.             $seconds $nextHour->getTimestamp() - $cursor->getTimestamp();
  223.             $amountNumerator += ((int)$hourlyPrice->getAmount() * $seconds);
  224.             $cursor $nextHour;
  225.         }
  226.         return new Money((string)intdiv($amountNumeratorself::SECONDS_IN_HOUR), new Currency($this->currency));
  227.     }
  228.     private function resolveTimezone(?\DateTimeZone $timezone): \DateTimeZone
  229.     {
  230.         if (null !== $timezone) {
  231.             return $timezone;
  232.         }
  233.         if (null !== $this->city && null !== $this->city->getTimezone()) {
  234.             return new \DateTimeZone($this->city->getTimezone());
  235.         }
  236.         return new \DateTimeZone('UTC');
  237.     }
  238. }