openstackid-resources/app/Models/Foundation/Summit/Registration/SummitOrder.php

1077 lines
27 KiB
PHP

<?php namespace models\summit;
/**
* Copyright 2019 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Events\PaymentSummitRegistrationOrderConfirmed;
use App\Events\RequestedSummitOrderRefund;
use App\Events\SummitOrderCanceled;
use App\Events\SummitOrderRefundAccepted;
use Doctrine\Common\Collections\Criteria;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use models\exceptions\ValidationException;
use models\main\Company;
use models\main\Member;
use models\utils\SilverstripeBaseModel;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping AS ORM;
/**
* @ORM\Entity(repositoryClass="App\Repositories\Summit\DoctrineSummitOrderRepository")
* @ORM\AssociationOverrides({
* @ORM\AssociationOverride(
* name="summit",
* inversedBy="orders"
* )
* })
* @ORM\Table(name="SummitOrder")
* Class SummitOrder
* @package models\summit
*/
class SummitOrder extends SilverstripeBaseModel implements IQREntity
{
use SummitOwned;
/**
* @ORM\Column(name="Number", type="string")
* @var string
*/
private $number;
/**
* @ORM\Column(name="ExternalId", type="string")
* @var string
*/
private $external_id;
/**
* @ORM\Column(name="Status", type="string")
* @var string
*/
private $status;
/**
* @ORM\Column(name="PaymentMethod", type="string")
* @var string
*/
private $payment_method;
/**
* @ORM\Column(name="QRCode", type="string", nullable=true)
* @var string
*/
private $qr_code;
/**
* @ORM\Column(name="OwnerFirstName", type="string")
* @var string
*/
private $owner_first_name;
/**
* @ORM\Column(name="OwnerSurname", type="string")
* @var string
*/
private $owner_surname;
/**
* @ORM\Column(name="OwnerEmail", type="string")
* @var string
*/
private $owner_email;
/**
* @ORM\Column(name="OwnerCompany", type="string")
* @var string
*/
private $owner_company;
/**
* @ORM\ManyToOne(targetEntity="models\main\Company")
* @ORM\JoinColumn(name="CompanyID", referencedColumnName="ID", nullable=true)
* @var Company
*/
private $company;
/**
* @ORM\ManyToOne(targetEntity="models\main\Member", inversedBy="summit_registration_orders")
* @ORM\JoinColumn(name="OwnerID", referencedColumnName="ID", nullable=true)
* @var Member
*/
private $owner;
/**
* @ORM\Column(name="BillingAddress1", type="string")
* @var string
*/
private $billing_address_1;
/**
* @ORM\Column(name="BillingAddress2", type="string")
* @var string
*/
private $billing_address_2;
/**
* @ORM\Column(name="BillingAddressZipCode", type="string")
* @var string
*/
private $billing_address_zip_code;
/**
* @ORM\Column(name="BillingAddressCity", type="string")
* @var string
*/
private $billing_address_city;
/**
* @ORM\Column(name="BillingAddressState", type="string")
* @var string
*/
private $billing_address_state;
/**
* @ORM\Column(name="BillingAddressCountryISOCode", type="string")
* @var string
*/
private $billing_address_country_iso_code;
/**
* @ORM\Column(name="ApprovedPaymentDate", type="datetime")
* @var \DateTime
*/
private $approved_payment_date;
/**
* @ORM\Column(name="LastError", type="string")
* @var string
*/
private $last_error;
/**
* @ORM\Column(name="PaymentGatewayClientToken", type="string")
* @var string
*/
private $payment_gateway_client_token;
/**
* @ORM\Column(name="PaymentGatewayCartId", type="string")
* @var string
*/
private $payment_gateway_cart_id;
/**
* @ORM\Column(name="Hash", type="string")
* @var string
*/
private $hash;
/**
* @ORM\Column(name="HashCreationDate", type="datetime")
* @var \DateTime
*/
private $hash_creation_date;
/**
* @ORM\OneToMany(targetEntity="SummitAttendeeTicket", mappedBy="order", cascade={"persist","remove"}, orphanRemoval=true)
* @var SummitAttendeeTicket[]
*/
private $tickets;
/**
* @ORM\OneToMany(targetEntity="SummitOrderExtraQuestionAnswer", mappedBy="order", cascade={"persist","remove"}, orphanRemoval=true)
* @var SummitOrderExtraQuestionAnswer[]
*/
private $extra_question_answers;
/**
* @ORM\Column(name="RefundedAmount", type="float")
* @var float
*/
private $refunded_amount;
/**
* @var \DateTime
*/
private $disclaimer_accepted_date;
/**
* @ORM\Column(name="LastReminderEmailSentDate", type="datetime")
* @var \DateTime
*/
private $last_reminder_email_sent_date;
/**
* SummitOrder constructor.
*/
public function __construct()
{
parent::__construct();
$this->tickets = new ArrayCollection();
$this->extra_question_answers = new ArrayCollection();
$this->status = IOrderConstants::ReservedStatus;
$this->payment_method = IOrderConstants::OnlinePaymentMethod;
$this->refunded_amount = 0.0;
}
public function setPaymentMethodOffline(){
$this->payment_method = IOrderConstants::OfflinePaymentMethod;
}
public function generateHash(){
$email = $this->getOwnerEmail();
if(empty($email))
throw new ValidationException("owner email is null");
$fname = $this->getOwnerFirstName();
if(empty($fname))
throw new ValidationException("owner first name is null");
$lname = $this->getOwnerSurname();
if(empty($lname))
throw new ValidationException("owner last name is null");
$token = $this->number.'.'.$email.'.'.$fname.".".$lname;
$token = $token . random_bytes(16).time();
$this->hash = hash('sha256', $token);
$this->hash_creation_date = new \DateTime('now', new \DateTimeZone('UTC'));
}
/**
* @return bool
* @throws \Exception
*/
public function canPubliclyEdit():bool {
if(empty($this->hash) || is_null($this->hash_creation_date)) return false;
$ttl_minutes = Config::get("registration.order_public_edit_ttl", 10);
$eol = new \DateTime('now', new \DateTimeZone('UTC'));
$eol->sub(new \DateInterval('PT'.$ttl_minutes.'M'));
if($this->hash_creation_date <= $eol)
return false;
return true;
}
/**
* @return string
*/
public function generateNumber():string{
$this->number = strtoupper(str_replace(".","", uniqid($this->summit->getOrderQRPrefix().'_', true)));
$this->generateQRCode();
return $this->number;
}
/**
* @return string
*/
public function getNumber(): string
{
return $this->number;
}
/**
* @param string $number
*/
public function setNumber(string $number): void
{
$this->number = $number;
}
/**
* @return string
*/
public function getStatus(): string
{
return $this->status;
}
public function setPaidStatus(){
$this->status = IOrderConstants::PaidStatus;
$this->approved_payment_date = new \DateTime('now', new \DateTimeZone('UTC'));
}
public function setPaid(){
Log::debug(sprintf("SummitOrder::setPaid order %s", $this->id));
if($this->isPaid()){
Log::warning(sprintf("SummitOrder %s is already Paid.", $this->getId()));
return;
}
$this->setPaidStatus();
foreach($this->tickets as $ticket){
$ticket->setPaid();
}
Event::dispatch(new PaymentSummitRegistrationOrderConfirmed($this->getId()));
}
/**
* @param null|string $error
*/
public function setPaymentError(?string $error):void{
if(empty($error)) return;
$this->status = IOrderConstants::ErrorStatus;
$this->last_error = $error;
}
public function setConfirmed(){
if($this->status == IOrderConstants::ReservedStatus)
$this->status = IOrderConstants::ConfirmedStatus;
}
/**
* @param bool $sendMail
*/
public function setCancelled(bool $sendMail = true):void {
$ignore_statuses = [ IOrderConstants::PaidStatus, IOrderConstants::CancelledStatus];
if(in_array($this->status, $ignore_statuses)) return;
$this->status = IOrderConstants::CancelledStatus;
list($tickets_to_return, $promo_codes_to_return) = $this->calculateTicketsAndPromoCodesToReturn();
foreach ($this->getTickets() as $ticket){
$ticket->setCancelled();
}
Event::dispatch(new SummitOrderCanceled($this->id, $sendMail, $tickets_to_return, $promo_codes_to_return));
}
/**
* @return array
*/
public function calculateTicketsAndPromoCodesToReturn():array {
$tickets_to_return = [];
$promo_codes_to_return = [];
foreach($this->tickets as $ticket){
if($ticket->isCancelled()) continue;
if($ticket->isRefunded()) continue;
if(!isset($tickets_to_return[$ticket->getTicketTypeId()]))
$tickets_to_return[$ticket->getTicketTypeId()] = 0;
$tickets_to_return[$ticket->getTicketTypeId()] += 1;
if($ticket->hasPromoCode()){
if(!isset($promo_codes_to_return[$ticket->getPromoCode()->getCode()]))
$promo_codes_to_return[$ticket->getPromoCode()->getCode()] = 0;
$promo_codes_to_return[$ticket->getPromoCode()->getCode()] +=1;
}
}
return [$tickets_to_return, $promo_codes_to_return];
}
/**
* @throws ValidationException
*/
public function requestRefund():void{
$summit = $this->getSummit();
$begin_date = $summit->getBeginDate();
if(is_null($begin_date)) return;
// check tickets badge printings
if($this->getRawAmount() == 0 )
foreach ($this->tickets as $ticket){
if($ticket->isBadgePrinted()){
throw new ValidationException(sprintf( "You can not request a refund for this ticket %s (badge already printed).", $ticket->getNumber()));
}
}
$now = new \DateTime('now', new \DateTimeZone('UTC'));
if($now > $begin_date){
Log::debug("SummitOrder::requestRefund: now is greater than Summit.BeginDate");
throw new ValidationException("You can not request a refund after summit started.");
}
$interval = $begin_date->diff($now);
$days_before_event_starts = intval($interval->format('%a'));
Log::debug(sprintf("SummitOrder::requestRefund: days_before_event_starts %s", $days_before_event_starts));
if($this->status != IOrderConstants::PaidStatus){
throw new ValidationException("You can not request a refund on this order.");
}
$this->status = IOrderConstants::RefundRequestedStatus;
foreach ($this->tickets as $ticket){
$ticket->setRefundRequests();
}
Event::dispatch(new RequestedSummitOrderRefund($this->getId(), $days_before_event_starts));
}
function cancelRefundRequest():void {
if(!$this->isRefundRequested())
throw new ValidationException(sprintf("You can not cancel any refund on this order"));
$this->status = IOrderConstants::PaidStatus;
foreach ($this->tickets as $ticket){
$ticket->setPaid(false);
}
}
/**
* @return string
*/
public function getPaymentMethod(): string
{
return $this->payment_method;
}
/**
* @param string $payment_method
*/
public function setPaymentMethod(string $payment_method): void
{
$this->payment_method = $payment_method;
}
/**
* @return string
*/
public function getQRCode(): ?string
{
return $this->qr_code;
}
/**
* @return string
*/
public function getOwnerFirstName(): ?string
{
if($this->hasOwner()){
return $this->owner->getFirstName();
}
return $this->owner_first_name;
}
/**
* @param string $owner_first_name
*/
public function setOwnerFirstName(string $owner_first_name): void
{
$this->owner_first_name = $owner_first_name;
}
/**
* @return string
*/
public function getOwnerSurname(): ?string
{
if($this->hasOwner()){
return $this->owner->getLastName();
}
return $this->owner_surname;
}
/**
* @param string $owner_surname
*/
public function setOwnerSurname(string $owner_surname): void
{
$this->owner_surname = $owner_surname;
}
/**
* @return string
*/
public function getOwnerEmail(): ?string
{
if(!is_null($this->owner)){
return $this->owner->getEmail();
}
return $this->owner_email;
}
/**
* @param string $owner_email
*/
public function setOwnerEmail(string $owner_email): void
{
$this->owner_email = strtolower($owner_email);
}
/**
* @return string
*/
public function getOwnerCompany(): ?string
{
if($this->hasCompany())
return $this->company->getName();
return $this->owner_company;
}
/**
* @param string $owner_company
*/
public function setOwnerCompany(string $owner_company): void
{
$this->owner_company = $owner_company;
}
/**
* @return Company
*/
public function getCompany(): Company
{
return $this->company;
}
/**
* @param Company $company
*/
public function setCompany(Company $company): void
{
$this->company = $company;
}
/**
* @return Member
*/
public function getOwner(): ?Member
{
return $this->owner;
}
/**
* @param Member $owner
*/
public function setOwner(Member $owner): void
{
$this->owner = $owner;
}
/**
* @return string
*/
public function getBillingAddress1(): ?string
{
return $this->billing_address_1;
}
/**
* @param string $billing_address_1
*/
public function setBillingAddress1(string $billing_address_1): void
{
$this->billing_address_1 = $billing_address_1;
}
/**
* @return string
*/
public function getBillingAddress2(): ?string
{
return $this->billing_address_2;
}
/**
* @param string $billing_address_2
*/
public function setBillingAddress2(string $billing_address_2): void
{
$this->billing_address_2 = $billing_address_2;
}
/**
* @return string
*/
public function getBillingAddressZipCode(): ?string
{
return $this->billing_address_zip_code;
}
/**
* @param string $billing_address_zip_code
*/
public function setBillingAddressZipCode(string $billing_address_zip_code): void
{
$this->billing_address_zip_code = $billing_address_zip_code;
}
/**
* @return string
*/
public function getBillingAddressCity(): ?string
{
return $this->billing_address_city;
}
/**
* @param string $billing_address_city
*/
public function setBillingAddressCity(string $billing_address_city): void
{
$this->billing_address_city = $billing_address_city;
}
/**
* @return string
*/
public function getBillingAddressState(): ?string
{
return $this->billing_address_state;
}
/**
* @param string $billing_address_state
*/
public function setBillingAddressState(string $billing_address_state): void
{
$this->billing_address_state = $billing_address_state;
}
/**
* @return string
*/
public function getBillingAddressCountryIsoCode(): ?string
{
return $this->billing_address_country_iso_code;
}
/**
* @param string $billing_address_country_iso_code
*/
public function setBillingAddressCountryIsoCode(string $billing_address_country_iso_code): void
{
$this->billing_address_country_iso_code = $billing_address_country_iso_code;
}
/**
* @return \DateTime
*/
public function getApprovedPaymentDate(): \DateTime
{
return $this->approved_payment_date;
}
/**
* @param \DateTime $approved_payment_date
*/
public function setApprovedPaymentDate(\DateTime $approved_payment_date): void
{
$this->approved_payment_date = $approved_payment_date;
}
/**
* @return string
*/
public function getLastError(): ?string
{
return $this->last_error;
}
/**
* @param string $last_error
*/
public function setLastError(string $last_error): void
{
$this->last_error = $last_error;
}
/**
* @return string
*/
public function getPaymentGatewayClientToken(): ?string
{
return $this->payment_gateway_client_token;
}
/**
* @param string $payment_gateway_client_token
*/
public function setPaymentGatewayClientToken(string $payment_gateway_client_token): void
{
$this->payment_gateway_client_token = $payment_gateway_client_token;
}
/**
* @return string
*/
public function getPaymentGatewayCartId(): ?string
{
return $this->payment_gateway_cart_id;
}
/**
* @param string $payment_gateway_cart_id
*/
public function setPaymentGatewayCartId(string $payment_gateway_cart_id): void
{
$this->payment_gateway_cart_id = $payment_gateway_cart_id;
}
/**
* @return string
*/
public function getHash(): ?string
{
return $this->hash;
}
/**
* @return \DateTime
*/
public function getHashCreationDate(): ?\DateTime
{
return $this->hash_creation_date;
}
/**
* @return ArrayCollection|SummitAttendeeTicket[]
*/
public function getTickets()
{
return $this->tickets;
}
/**
* @param SummitAttendeeTicket $ticket
*/
public function addTicket(SummitAttendeeTicket $ticket){
if($this->tickets->contains($ticket)) return;
$this->tickets->add($ticket);
$ticket->setOrder($this);
}
/**
* @param int $ticket_id
* @return SummitAttendeeTicket|null
*/
public function getTicketById(int $ticket_id):?SummitAttendeeTicket{
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('id', intval($ticket_id)));
$ticket = $this->tickets->matching($criteria)->first();
return $ticket === false ? null : $ticket;
}
/**
* @return SummitOrderExtraQuestionAnswer[]
*/
public function getExtraQuestionAnswers()
{
return $this->extra_question_answers;
}
public function clearExtraQuestionAnswers(){
$this->extra_question_answers->clear();
}
public function addExtraQuestionAnswer(SummitOrderExtraQuestionAnswer $answer){
if($this->extra_question_answers->contains($answer)) return;
$this->extra_question_answers->add($answer);
$answer->setOrder($this);
}
public function removeExtraQuestionAnswer(SummitOrderExtraQuestionAnswer $answer){
if(!$this->extra_question_answers->contains($answer)) return;
$this->extra_question_answers->removeElement($answer);
$answer->clearOrder();
}
use QRGeneratorTrait;
public function generateQRCode(): string
{
$this->qr_code = $this->generateQRFromFields([
$this->summit->getOrderQRPrefix(),
$this->number
]);
return $this->qr_code;
}
/**
* @return int
*/
public function getOwnerId(){
try {
return is_null($this->owner) ? 0 : $this->owner->getId();
}
catch(\Exception $ex){
return 0;
}
}
/**
* @return bool
*/
public function hasCompany():bool{
return $this->getCompanyId() > 0;
}
/**
* @return int
*/
public function getCompanyId(){
try {
return is_null($this->company) ? 0 : $this->company->getId();
}
catch(\Exception $ex){
return 0;
}
}
/**
* @return bool
*/
public function hasOwner():bool{
return $this->getOwnerId() > 0;
}
/**
* @return float
*/
public function getRawAmount():float{
$amount = 0.0;
foreach ($this->tickets as $ticket){
$amount += $ticket->getRawCost();
}
return $amount;
}
/**
* @return float
*/
public function getFinalAmount():float {
$amount = 0.0;
foreach ($this->tickets as $ticket){
$amount += $ticket->getFinalAmount();
}
return $amount;
}
/**
* @return bool
*/
public function isFree():bool {
return $this->getFinalAmount() == 0;
}
/**
* @return bool
*/
public function hasPaymentInfo():bool{
return empty($this->payment_gateway_cart_id) || empty($this->payment_gateway_client_token);
}
/**
* @return float
*/
public function getTaxesAmount(): float{
$amount = 0.0;
foreach ($this->tickets as $ticket){
foreach($ticket->getAppliedTaxes() as $appliedTax){
$amount += $appliedTax->getAmount();
}
}
return $amount;
}
/**
* @return float
*/
public function getDiscountAmount(): float{
$amount = 0.0;
foreach ($this->tickets as $ticket){
$amount += $ticket->getDiscount();
}
return $amount;
}
/**
* @return string
*/
public function getCurrency():string{
$ticket = $this->tickets->first();
return $ticket->getCurrency();
}
/**
* @return string
*/
public function getOwnerFullName():string {
if($this->hasOwner()){
return $this->owner->getFullName();
}
return sprintf("%s %s", $this->owner_first_name, $this->owner_surname);
}
/**
* @return bool
*/
public function isPaid():bool {
return $this->status == IOrderConstants::PaidStatus;
}
/**
* @return bool
*/
public function canRefund():bool{
$validStatuses = [IOrderConstants::RefundRequestedStatus, IOrderConstants::PaidStatus];
if(!in_array($this->status, $validStatuses)){
return false;
}
if($this->isFree()){
return false;
}
return true;
}
/**
* @param float $amount
* @throws ValidationException
*/
public function refund(float $amount)
{
if (!$this->canRefund())
throw new ValidationException
(
sprintf
(
"can not request a refund on a %s order",
$this->status
)
);
$this->status = IOrderConstants::RefundedStatus;
$this->refunded_amount = $amount;
list($tickets_to_return, $promo_codes_to_return) = $this->calculateTicketsAndPromoCodesToReturn();
foreach ($this->tickets as $ticket){
$ticket->setRefunded();
}
Event::dispatch(new SummitOrderRefundAccepted($this->getId(), $tickets_to_return, $promo_codes_to_return));
}
/**
* @return string
* - if tixs in an order are all in a combination of status refund requested or refunded, show order as refund requested.
* - if all tix in an order are in status refund requested, show order as refund requested.
* - if all tix in an order are in status refunded, show order as refunded.
*/
public function recalculateOrderStatus():string {
Log::debug(sprintf("SummitOrder::recalculateOrderStatus current status %s", $this->status));
$request_refund_count = 0;
$refund_count = 0;
foreach ($this->tickets as $ticket){
$ticket_status = $ticket->getStatus();
Log::debug(sprintf("SummitOrder::recalculateOrderStatus ticket_id %s ticket_status %s", $ticket->getId(), $ticket_status));
if($ticket_status == IOrderConstants::RefundRequestedStatus)
++$request_refund_count;
if($ticket_status == IOrderConstants::RefundedStatus)
++$refund_count;
}
$tickets_count = $this->tickets->count();
Log::debug(sprintf("SummitOrder::recalculateOrderStatus tickets_count %s request_refund_count %s refund_count %s", $tickets_count, $request_refund_count, $refund_count));
if(($request_refund_count == $tickets_count || ( $refund_count > 0 && $request_refund_count > 0 && ($refund_count + $request_refund_count) == $tickets_count)))
$this->status = IOrderConstants::RefundRequestedStatus;
if($refund_count == $tickets_count)
$this->status = IOrderConstants::RefundedStatus;
Log::debug(sprintf("SummitOrder::recalculateOrderStatus recalculated status %s", $this->status));
return $this->status;
}
/**
* @return bool
*/
public function isRefundRequested():bool {
return $this->status == IOrderConstants::RefundRequestedStatus;
}
/**
* @return float
*/
public function getRefundedAmount(): float
{
return $this->refunded_amount;
}
/**
* @return \DateTime
*/
public function getLastReminderEmailSentDate(): ?\DateTime
{
$last_action_date = $this->last_reminder_email_sent_date;
if (is_null($last_action_date)) {
$last_action_date = $this->getCreatedUTC();
}
return $last_action_date;
}
/**
* @param \DateTime $last_reminder_email_sent_date
*/
public function setLastReminderEmailSentDate(\DateTime $last_reminder_email_sent_date): void
{
$this->last_reminder_email_sent_date = $last_reminder_email_sent_date;
}
/**
* @return bool
*/
public function isSingleOrder():bool{
if($this->tickets->count() > 1){
return false;
}
$ticket = $this->tickets->first();
if(!$ticket instanceof SummitAttendeeTicket) return false;
if($ticket->getOwnerEmail() != $this->getOwnerEmail()) return false;
return true;
}
/**
* @return SummitAttendeeTicket|null
*/
public function getFirstTicket():?SummitAttendeeTicket{
if(is_null($this->tickets)) return null;
if($this->tickets->count() == 0) return null;
return $this->tickets->first();
}
/*
* @return string
*/
public function getExternalId(): ?string
{
return $this->external_id;
}
/**
* @param string $external_id
*/
public function setExternalId(string $external_id): void
{
$this->external_id = $external_id;
}
}