/** * Order.php * * Orders are the bookings which purchasers make in the system * * @class Order * @package Order * @copyright 2010 - 2015 BookingLive Software Limited */ class Order extends DataObject implements NotificationInterface,PrerequisiteInterface { static $currentOrder = null; static $type = PermissionsExtension::CONDITIONAL; static $db = array( 'Code' => 'Varchar', 'Reference' => 'Varchar', 'SessionID' => 'Varchar', 'IPAddress' => 'Varchar', 'DateRegistered' => 'SS_Datetime', 'OriginalDateRegistered' => 'SS_Datetime', 'Status' => 'Varchar', 'WaitingList' => 'Boolean', 'MainStatus' => "Enum(array( 'Completed' 'Pending', 'Aborted', 'Cancelled', 'Provisional', 'WaitingList' ),'Pending')", 'Source' => "Enum(array( 'Online', 'Staff', 'EPOS' ),'Online')", 'Occasion' => 'Varchar', 'HowDidYouHearAboutUs' => 'Varchar(255)', 'Actioned' => 'Boolean', //Override 'TotalCostOverride' => 'BLDecimal', 'DepositOverride' => 'BLDecimal', //Refund (on after write hack) 'PerformRefund' => 'Boolean', 'CreditsToRefund' => 'Int', 'AmountToRefund' => 'BLDecimal', 'RefundLog' => 'Text', //Cancellation (on after write hack) 'Cancel' => 'Boolean', 'CancelCode' => 'Varchar(5)', 'CancelText' => 'Text', 'CancelDate' => 'SS_Datetime', 'CancelMemberID' => 'Int', //(calced) 'CheckHash' => 'Text', 'EventCheckHash' => 'Text', 'Summary' => 'Text', 'BillingForm' => 'Text', 'Deposit' => 'BLDecimal', // total of each order item deposit + tax 'TotalCost' => 'BLDecimal', // total of all items excluding discounts and charges and promotions (net) 'TotalTax' => 'BLDecimal', 'FinalTotalCost' => 'BLDecimal', // total of all items including discounts and charges and promotions (gross) 'ChargesTotal' => 'BLDecimal', // amount of charges on that total 'TotalTransactionValue' => 'BLDecimal', 'TotalTransactionRefund' => 'BLDecimal', 'UnverifiedTransactionValue'=> 'BLDecimal', 'AmountDue' => 'BLDecimal', // the amount that is currently required to be paid (gross) 'TotalAmountDue' => 'BLDecimal', // total amount due for whole order (include not yet submitted invoices) 'TotalAmountOverdue' => 'BLDecimal', // total amount due that missed the payment date 'InvoiceAmountDue' => 'BLDecimal', // amount due to be paid for next not yet submitted invoice 'AllParticipantInformation' => 'Boolean', 'EPOS' => 'Boolean', 'API' => 'Boolean', //this indicates it is a booking to "link" people together //these should be hidden from all normal views (they will be "Cancelled" anyway) //but are used so members can be selected on billing page 'FakeOrder' => 'Boolean', 'CedArRefInvoiced' => 'Boolean', 'Invoiced' => 'Boolean', //The value of this field show will SMS message be send if there are changes in the order 'SMSNotification' => 'Boolean', //The value of this field show will Email message be send if there are changes in the order //This field is different than $order->param('NotificationByEmail'). $order->param('NotificationByEmail') //should be used to determine do I need to send email for this change. 'EmailNotification' => 'Boolean', // Resource Management TODO consider those fields //'DamagedCount' => 'Int', //'MissingCount' => 'Int', 'SimulatedDate' => 'SS_Datetime', 'EditAttempts' => 'Int', // Indicate the initial date time when order was registered 'OriginalCheckoutDate' => 'SS_Datetime', 'CancellationCode' => 'Varchar(255)', 'CancellationReason' => 'Text', 'EmailSent' => 'Int', ); static $has_one = array( 'PendingPurchaser' => 'PendingMember', 'Staff' => 'Member', 'Purchaser' => 'Member', 'Organisation' => 'Organisation' ); static $bl_icon = 'icon-cart3'; static $has_many = array( 'Transactions' => 'Transaction', 'OrderItems' => 'OrderItem', 'OrderNotes' => 'OrderNote', 'OrderParams' => 'OrderParam', 'OrderSummaries' => 'OrderSummary',//TODO depreciate 'BillingPageSubmissions'=> 'BillingPageSubmission', 'Charges' => 'ChargeItem' ); static $many_many = array( 'Tags' => 'OrderTag', 'Prerequisites' => 'Prerequisite', 'MemberDocuments' => 'MemberDocument' ); static $default_sort = 'DateRegistered DESC'; public static $indexes = array( 'SessionID' => true, 'MainStatus' => true, 'DateRegistered' => true ); public static $summary_fields = array( 'Reference', 'OriginalDateRegistered', 'StartDate', 'EndDate', 'PurchaserFullName', 'FinalTotalCostAdminFormat', 'MainStatus' ); public static $searchable_fields = array( 'Status' => array('filter'=> 'OrderCustomStatusFilter'), 'Reference', 'OrderItems.ProductID' => array('title' => 'Products in order'), ); static $field_labels = array( 'MainStatus' => 'Status' ); static $extensions = [ ZonalOrderExtension::class, ]; static $default_search_statuses = array('Completed', 'Provisional'); public $OriginalID = 0; // Original order id of the duplicated order public function getTagsSummary() { return implode(', ', $this->Tags()->column('Title')); } public function getTitle() { return $this->Reference; } public function canDelete($member = null) { return false; } public function getDependentRelationsForDelete() { if($this->MainStatus == 'Aborted') return array(); return array('Transactions','OrderItems','Invoices'); } public static function getDefaultSearchValues() { $value = array(); $StatusCustomLabel = StatusCustomLabel::get(); if (!isset($_POST) || (isset($_POST) && !in_array(['action_save','action_doSaveAndQuit'],$_POST))) { $StatusCustomLabel = $StatusCustomLabel->filter('DefaultSearch',true); } foreach ($StatusCustomLabel->sort('Label ASC, IsSystemStatus DESC') as $customStatus) { $value[] = $customStatus->Label; } return $value; } /** * scaffold the form fields for model admins * @param null $_params * @return FieldList */ function scaffoldSearchFields($_params = null){ $fields = parent::scaffoldSearchFields($_params); // TODO make a static varible with this //$value = self::$default_search_statuses; $value = self::getDefaultSearchValues(); $orderItemValue = array(); if (isset($_REQUEST['q'])) { $value = isset($_REQUEST['q']['MainStatus']) ? $_REQUEST['q']['MainStatus'] : array(); $orderItemValue = isset($_REQUEST['q']['OrderItems__ProductID']) ? $_REQUEST['q']['OrderItems__ProductID'] : array(); } $arrSelections = CustomStatusesField::CustomLabels()->map('Label','Label')->toArray(); if (isset($arrSelections['Waiting List'])) { $arrSelections['WaitingList'] = $arrSelections['Waiting List']; unset($arrSelections['Waiting List']); } $fields->replaceField( 'Status', ListboxField::create('MainStatus', 'Status', $arrSelections) ->setMultiple(true) ->setValue($value) ); $fields->replaceField( 'OrderItems__ProductID', ListboxField::create('OrderItems__ProductID', 'Products in orders',Product::get()->map('ID','Name')->toArray()) ->setMultiple(true) ->setValue($orderItemValue) ); $this->extend('updateScaffoldSearchFields', $fields); return $fields; } public function OrderCMSFields() { $fActions = ActionsDropdownField::create('ActionsDropdown'); if (in_array($this->MainStatus, array('Completed','Provisional','WaitingList'))) { if ($this->HasResourceManagementProduct()) { if ($this->CanCheckIn()) $fActions->addAction('Check In', array( 'class' => 'checkinout-popup', 'data-link' => 'OrderAdminController/CheckInResources/'.$this->ID )); if ($this->CanCheckOut()) $fActions->addAction('Check Out',array( 'class' => 'checkinout-popup', 'data-link' => 'OrderAdminController/CheckOutResources/'.$this->ID )); $fActions->addAction('Edit Collection & Return',array( 'class' => 'collection-return-edit', 'data-link' => 'OrderAdminController/EditCollectionReturnDates/'.$this->ID )); } $fActions->addActions(array( 'Add to Order' => array( 'id' => 'action_AddToORder', 'data-link' => '#' ), 'Edit Details' => array( 'class' => 'EditPurchaserDetails', 'data-link' => 'admin/eventadmin/EditBooking/'.$this->ID.'?EditPurchaserOnly=1' ), 'Resend Confirmation' => array( 'id' => 'action_ResendConfirmationEmail', 'data-link' => 'EventAdmin/ResendConfirmationEmail/'.$this->ID ), 'Cancel Order' => array( 'id' => 'action_CancelOrder', 'data-id' => $this->ID ), 'Order Timeline' => array( 'id' => 'action_Log', 'data-link' => 'OrderAdminController/showTimeLine/'.$this->ID ), 'Send Balance Reminder Email' => array( 'id' => 'action_SendReminderEmail', 'data-link' => 'OrderAdminController/SendReminderEmail/'.$this->ID ) )); if ($this->IfOutstandingPayment()) $fActions->addAction('Add Payment',array( 'id' => 'action_AddPayment', 'data-orderid' => $this->ID )); } if (in_array($this->MainStatus,array('Cancelled','Provisional'))) $fActions->addAction('Complete Order',array( 'id' => 'action_CompleteOrder', 'data-id' => $this->ID )); return $fActions->forTemplate(); } public static function OrderCMSTableRow($strLabel,$stValue,$bStrong=false) { $stValue = $bStrong ? ''.$stValue.'' : $stValue; return ''.$strLabel.':'.$stValue.''; } public function OrderCMSTable() { $strTableRows = self::OrderCMSTableRow('Reference',$this->Reference,true); $strTableRows .= self::OrderCMSTableRow('Date',DateUtils::PublicDateTimeFormat($this->DateRegistered)); if ($this->MainStatus == 'Cancelled') $strTableRows .= self::OrderCMSTableRow('Cancellation',DateUtils::PublicDateTimeFormat($this->CancelDate)); if (SiteConfigOverride::CurrentSiteConfig()->SystemTakesPayments) { $currency = $this->CurrencySymbol(); $strTableRows .= self::OrderCMSTableRow('Total',CurrencyUtils::getCurrencyWithFormat($this->FinalTotalCost)); $strTableRows .= self::OrderCMSTableRow('Total Payable Now',CurrencyUtils::getCurrencyWithFormat($this->AmountDue + $this->UnverifiedTransactionValue)); } return ''.$strTableRows.'
'; } private function GetTaglistForTagField() { $q = DatabaseUtils::ArrayFromDBQuery('SELECT ID FROM `OrderTag` GROUP BY "Title"'); $currentOrderTags = $this->Tags()->map('ID','ID')->toArray(); foreach ($q as $value) { $currentOrderTags[$value['ID']] = $value['ID']; } return $currentOrderTags; } public function getCMSFields() { $fields = parent::getCMSFields(); PresentationUtils::AddRequirements(); Requirements::javascript('mysite/javascript/CustomersAdmin.js'); Requirements::javascript('mysite/javascript/OrderAdminEvents.js'); Requirements::javascript('mysite/javascript/CustomStatuses.js'); $iPhysicalItemCount = Product::get()->filter(array( 'Status' => 'On', 'ClassName' => 'ProductPhysicalItem' ))->count(); $iUpsellCount = 0; //This will be enabled whit next release and when investigation on how Add Upsell to order works. // foreach ($this->OrderItems() as $orderItem) // if ($orderItem->Product()->ProductUpsells()->count() > 0) // $iUpsellCount = 1; $fields->removeByName(array( 'AllParticipantInformation', 'AmountToRefund', 'BillingForm', 'BillingPageSubmissions', 'CheckHash', 'CreditsToRefund', 'DepositOverride', 'FakeOrder', 'OrderSummaries', 'PerformRefund', 'RefundLog', 'SessionID', 'Summary', 'TotalCostOverride', 'Invoices', 'Tags', )); if (!$this->Charges()->first()) $fields->removeByName('Charges'); $siteConfig = SiteConfigOverride::CurrentSiteConfig(); if(!SiteConfigCategoryZonal::getCurrent()->Enabled) { $fields->removeByName('ZonalMessages'); } $fields->findOrMakeTab('Root.Main') ->setTitle('Main') ->setChildren( FieldList::create( LiteralField::create('BookingSummaryBegin','

Order

'.$this->CompanyName.'

'.$this->PurchaserOrganisationName().'

'.htmlspecialchars($this->PurchaserFullName()).'

'.$this->PurchaserAddressHTML().'

'.$this->PurchaserTelephone().'

'.$this->PurchaserEmail().'

'.$this->OrderCMSFields().' '.$this->OrderEmailIcon().' '.$this->OrderSMSIcon().' '.$this->OrderCMSTable() ), TagField::create('Tags', 'Tags', OrderTag::get()->filter('ID',$this->GetTaglistForTagField()), $this->Tags()) ->setShouldLazyLoad(false) ->setCanCreate(true), LiteralField::create('BookingSummaryEnd','
'.$this->CustomStatusesActionDropDown().'
') ) ); if ($fOrderItems = $fields->dataFieldByName('OrderItems')) { $orderItems = $this->OrderItems() ->LeftJoin('OrderItem_Events','OrderItem_Events.OrderItemID = OrderItem.ID') ->LeftJoin('Event','Event.ID = OrderItem_Events.EventID') ->Sort('Event.StartDateTime ASC'); $fields->addFieldToTab( 'Root.Main', $grid = FormUtils::GridFieldEdit('Products', 'Products', $orderItems, $siteConfig->SystemTakesPayments ? array( 'AdminSummaryDescription' => 'Description', 'AddedByName' => 'Added By', 'LastEditedByName' => 'Last Edited By', 'Cost' => _t('BookingLive.Net', 'Net'), 'Tax' => _t('BookingLive.Tax', 'Tax'), 'GrossAmount' => _t('BookingLive.Gross', 'Gross') ) : array( 'AdminSummaryDescription' => 'Description', 'AddedByName' => 'Added By', 'LastEditedByName' => 'Last Edited By', ), array( 'StartDateTime' => function($value, &$item) { return date(ADMINGRIDFIELDDATETIME, strtotime($value)); }, 'EndDateTime' => function($value, &$item) { return date(ADMINGRIDFIELDDATETIME, strtotime($value)); } ))->addExtraClass('remove-click-state')->addExtraClass('remove-hover-state') ); $config = $grid->getConfig(); $config->removeComponentsByType('GridFieldAddNewButton') ->removeComponentsByType('GridFieldPaginator') ->removeComponentsByType('GridFieldAddExistingAutocompleter') ->removeComponentsByType('GridFieldDeleteAction') ->removeComponentsByType('GridFieldFilterHeader') ->removeComponentsByType('GridFieldEditButton') ->addComponent(new GridFieldOrderItemActions()) ->addComponent(new GridFieldOrderSummaryRow(), 'GridFieldPaginator') ->addComponent($pagination = new GridFieldPaginator(10)) ->getComponentByType('GridFieldDataColumns'); $grid->getConfig() ->getComponentByType('GridFieldDataColumns') ->setFieldFormatting(array( 'Tax' => function($value, &$item) { return $item->CurrencySymbol().' '.$value; }, 'Cost' => function($value, &$item) { return $item->CurrencySymbol().' '.$value; }, 'GrossAmount' => function($value, &$item) { return $item->CurrencySymbol().' '.number_format($value, 2,'.',''); } )); $fields->removeByName('OrderItems'); } $fields->addFieldToTab('Root.Main',LiteralField::create('', PresentationUtils::ParseTemplateWithArray(array( 'ID' => $this->ID, 'PhysicalItemCount' => $iPhysicalItemCount, 'UpsellCount' => $iUpsellCount ),'AddToOrder') )); if (!$siteConfig->SystemTakesPayments) { $fields->removeByName('Transactions'); } else { if ($fTransactions = $fields->dataFieldByName('Transactions')) { $fTransactions->addExtraClass('remove-click-state'); $config = $fTransactions->getConfig() ->removeComponentsByType('GridFieldAddNewButton') ->removeComponentsByType('GridFieldPaginator') ->removeComponentsByType('GridFieldFilterHeader') ->removeComponentsByType('GridFieldAddExistingAutocompleter') ->removeComponentsByType('GridFieldViewButton') ->removeComponentsByType('GridFieldEditButton') ->removeComponentsByType('GridFieldDeleteAction') ->addComponent($pagination = new GridFieldPaginator(5)) ->addComponent(GridFieldViewTransaction::create()) ->addComponent(new GridFieldOrderSummaryRow(), 'GridFieldPaginator'); $config->getComponentByType('GridFieldDataColumns') ->setDisplayFields(array( 'Date' => 'Date', 'Status' => 'Status', 'TypeDescriptive' => 'Type', 'Source' => 'Source', 'Amount' => 'Amount', )); $config->getComponentByType('GridFieldDataColumns') ->setFieldFormatting(array( 'TypeDescriptive' => function ($value, &$item) { if ($value == 'Voucher') { return $value . ' ' . ($item->Voucher()->Status != 'Verification' ? $item->Voucher()->Status : 'Awaiting Verification'); } return $value; }, 'Date' => function ($value, &$item) { return date(ADMINGRIDFIELDDATETIME, strtotime($value)); }, 'Amount' => function ($value, &$item) { return $item->CurrencySymbol() . ' ' . $value; } )); $fields->removeByName('Transactions'); $fields->addFieldToTab('Root.Main', $fTransactions); } } if ($notes = $fields->dataFieldByName('OrderNotes')) { $config = $notes->setTitle('Notes') ->getConfig() ->removeComponentsByType('GridFieldPaginator') ->removeComponentsByType('GridFieldDeleteAction') ->addComponent($pagination = new GridFieldPaginator(5)); $config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array( 'Created' => 'Date', 'Type' => 'Type', 'Staff.FirstName' => 'Name', 'Note' => 'Note' )); $config->getComponentByType('GridFieldDataColumns')->setFieldFormatting(array( 'Created' => function($value, &$item) { return date(ADMINGRIDFIELDDATETIME, strtotime($value)); } )); $fields->removeByName('OrderNotes'); $fields->addFieldToTab('Root.Main',$notes); } if (!$siteConfig->SystemTakesPayments) $fields->removeByName('Transactions'); if ($grid = $fields->dataFieldByName('OrderNotes')) $grid->getConfig() ->removeComponentsByType('GridFieldAddExistingAutocompleter') ->removeComponentsByType('GridFieldFilterHeader'); $arrSiteConfigOrderParams = array(); foreach (OrderParamPattern::get()->filter('OrderProminent',1) as $strOrderParam) $arrSiteConfigOrderParams[] = $strOrderParam->AutoAttachNames; if (empty($_POST['IsInXLSExport2016'])) { $orderParams = $this->OrderParams() ->filterAny(array( 'IsTemplate' => true, 'Name' => $arrSiteConfigOrderParams )) ->exclude('Name','template'); if ($orderParams->exists()) { if ($grid = $fields->dataFieldByName('OrderParams')) { $grid->getConfig() ->removeComponentsByType('GridFieldAddNewButton') ->removeComponentsByType('GridFieldFilterHeader') ->removeComponentsByType('GridFieldAddExistingAutocompleter') ->removeComponentsByType('GridFieldDeleteAction') ->removeComponentsByType('GridFieldViewButton'); if (empty($arrSiteConfigOrderParams)) $grid->getConfig()->removeComponentsByType('GridFieldEditButton'); $grid->setList($orderParams); $grid->setTitle('Additional Order Information'); $grid->getConfig() ->getComponentByType('GridFieldDataColumns') ->setDisplayFields(array( 'Name' => 'Description', 'getDescribedValue' => 'Value', )); $fields->removeByName('OrderParams'); $fields->insertBefore($grid,'OrderNotes'); } } else $fields->removeByName('OrderParams'); } if ($memberDocuments = $fields->dataFieldByName('MemberDocuments')) { $config = $memberDocuments->setTitle('Documents') ->getConfig() ->removeComponentsByType('GridFieldPaginator') ->removeComponentsByType('GridFieldAddNewButton') ->removeComponentsByType('GridFieldDeleteAction') ->addComponent(new GridfieldDownloadMemberDocument()) ->addComponent(new GridfieldChangeDocumentOwner()) ->addComponent(new GridFieldDeleteAction()) ->addComponent($pagination = new GridFieldPaginator(5)); $config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array( 'File.Name' => 'Document Name', 'OrderItem.Product.Name' => 'Product', 'Member.FullName' => 'User' )); $fields->removeByName('MemberDocuments'); $fields->addFieldToTab('Root.Main',$memberDocuments); } $this->extend('overrideCMSFields', $fields); return $fields; } function onBeforeWrite(){ parent::onBeforeWrite(); if(!$this->IsInDB()) $this->SkipOnAfterWriteRecalc = true; else { if ($this->isChanged('MainStatus', 2)) { $arrChanges = $this->getChangedFields(true,2); if (in_array($arrChanges['MainStatus']['after'],array('Completed','Provisional'))) { foreach ($this->OrderItems('DateRegistered IS NULL') as $orderItem) { $orderItem->DateCompleted = SS_Datetime::now()->getValue(); $orderItem->write(); } } } } } function onAfterWrite(){ parent::onAfterWrite(); $bWriteAtEnd = false; if($this->Cancel){ $siteConfig = SiteConfigOverride::CurrentSiteConfig(); $this->MainStatus = 'Cancelled'; $this->UpdateStatus(); $this->CancelDate = SS_Datetime::now()->getValue(); $this->CancelMemberID = Member::CurrentUserID(); $this->Cancel = false; $bWriteAtEnd = true; foreach($this->OrderItems() as $orderItem) $orderItem->Cancel(); WebHook::process($this, 'OrderCancelled'); if ($siteConfig->SendOrderCancelEmail) { Message::PrepareAndSendByType('Cancellation', $this); SMSMessage::PrepareAndSendByType('Cancellation', $this); } } if(!$this->Reference) { $siteConfig = SiteConfigOverride::CurrentSiteConfig(); $this->Reference = ($siteConfig->DefaultReferencePrefix ? $siteConfig->DefaultReferencePrefix.'-' : '') .(200000 + $this->ID) .'-'.RandomData::StringAlpha(3); $bWriteAtEnd = true; } if($bWriteAtEnd) { $this->write(); } if(!$this->SkipOnAfterWriteRecalc && in_array($this->MainStatus, array('Aborted', 'Pending', 'Cancelled'))) $this->ReCalculate(true); $this->SkipOnAfterWriteRecalc = false; } /** * Gets the current order in payment mode * * @return mixed */ public static function GetPaymentProcessingOrder() { $orderParam = OrderParam::get()->where(" Name = 'PaymentProcessing' AND Unset = 0 AND OrderID IN ( SELECT ID FROM `Order` WHERE SessionID='" . session_id() . "' )")->first(); if ($orderParam) return $orderParam->Order(); } public function CancelWithReason($sCode, $sReason) { $array = array( 'Type' => 'CancellationNote', 'Note' => $sCode."
".$sReason, 'OrderID' => $this->ID, MemberExtension::IsStaff() ? 'StaffID' : 'MemberID' => Member::currentUserID() ); $orderNote = OrderNote::create($array); $orderNote->write(); $this->CancellationCode = $sCode; $this->CancellationReason = $sReason; $this->Cancel = 1; $this->write(); } /** * Get current order. * * If there is order to edit in the session (CurrentlyEditingOrder) it will be the returned order * only if current user is Staff or it's the purchaser or OrganisationId is set and it's equal for * the user and the order. * This can give more access to some users if Organisation is used, but user must not has privileges * to edit. * * If the current order is completed or has any problem with purchaser the order is aborted. * Then take any pending order or let to create a new order * @param string $strSessionID, the session id * @param boolean $bCreateOrder ,if it is true a new order is created, and no otherwise * @param boolean $bRecoverAbortedIfNoItems * @return Order */ public static function GetCurrentOrder($strSessionID='',$bCreateOrder=true, $bRecoverAbortedIfNoItems=true) { //JSONFeed does not need order and this is big speed improvement if (get_class(Controller::curr()) == 'JSONFeed_Controller') return null; if(empty($strSessionID)) $strSessionID = ServerUtils::GetSessionID(); $iCurrentlyEditingOrder = (int)Session::get('CurrentlyEditingOrder'); //Reuse cached order if it's still Pending and the sessionID is the same. Also if Session::get('CurrentlyEditingOrder') is set then we do not need the current order if (!is_null(self::$currentOrder) && self::$currentOrder !== false && !$iCurrentlyEditingOrder) if (self::$currentOrder->SessionID == $strSessionID && self::$currentOrder->MainStatus == 'Pending') return self::$currentOrder; if(!$bCreateOrder && self::$currentOrder === false) return null; self::$currentOrder = $order = null; if($iSelectedOrderID = (int)Session::get('CurrentlyEditingOrder')){ if($order = Order::get()->byId($iSelectedOrderID)){ if ($member = MemberExtension::currentUser()) if (!MemberExtension::IsStaff($member) && $member->ID != $order->PurchaserID && (!$member->OrganisationID || $member->OrganisationID != $order->OrganisationID) ) $order = null; if($order && !$order->allowEdit()) $order = null; } } if (!$order) { if ($order = Order::GetPaymentProcessingOrder()) { $order->OrderInPayment = true; return $order; } } if(empty($strSessionID)) $strSessionID = ServerUtils::GetSessionID(); if(!$order){ StartOrder_Controller::EndEditSession(); $order = $newPendingOrder = Order::get()->filter(array( 'SessionID' => $strSessionID, 'MainStatus' => 'Pending' ))->first(); } if($order && !$order->getOrderParam('NoRecover')) { if ($bRecoverAbortedIfNoItems && (!$order || !$order->OrderItems()->exists()) && ($order && !$order->getOrderParam('RebookPurchaser')) && !defined('BUSKERS_PATH') //not the best hack ) { if($abortedOrder = Order::get()->filter(array( 'SessionID' => $strSessionID, 'MainStatus' => 'Aborted', 'PurchaserID' => Member::currentUserID() ))->first()) { $order = $abortedOrder; } } if($order && $order->MainStatus == 'Aborted' && !defined('BUSKERS_PATH')) { //not the best hack if($order->getOrderParam('Recovering')) return $order; $order->setOrderParam('Recovering', true); if($order->canRecover()) { LogEntry::log('Recover '.$order->Reference); $order->SessionID = ServerUtils::RegenerateSessionID(); $order->MainStatus = 'Pending'; $order->write(); $order->unsetOrderParam('Recovering'); } else { $order->unsetOrderParam('Recovering'); $order = isset($newPendingOrder) ? $newPendingOrder : null; } } } if (!$order && $bCreateOrder) $order = Order::NewOrder($strSessionID); //If Session::get('CurrentlyEditingOrder') is set then this is order editing, so do not cache it as current order if(!$iCurrentlyEditingOrder) self::$currentOrder = $order; if(!$order) self::$currentOrder = false; return $order; } /** * canRecover * Checks if all the Orders Events are still available. * @return bool */ public function canRecover() { foreach($this->OrderItems() as $orderItem) if (!$orderItem->AreEventsAvailable()) return false; return true; } /** * Get order items of currently editing order one at a time * @return int, the id of that order item */ public function CurrentlyEditingOrderItem(){ return $this->OrderItems() ->filter('ID',$this->getOrderParam('CurrentlyEditingOrderItem')) ->first(); } public static function IsEventRelatedToCurrentlyEditingOrderItem($event){ return self::EventRelatedToCurrentlyEditingOrderItem($event) ? true : false; } public static function EventRelatedToCurrentlyEditingOrderItem($event){ $order = self::GetCurrentOrder(); if(!$order->IsInEdit()) return false; $alEvents = DatabaseUtils::ArrayListFromDBQuery(' SELECT EventID FROM OrderItem_Events WHERE OrderItemID ='.intval($order->getOrderParam('CurrentlyEditingOrderItem')) ); $arrEventIDs = explode(',',$event->EventIDs); foreach ($arrEventIDs as $iEventID) if($alEvents->find('EventID',$iEventID)) return Event::get()->byId($iEventID); return false; } /** * Check whether the order is in the edit mode * @return boolean, true if the order in edit mode, false otherwise */ public static function IsOrderEditMode($order = null){ if ($order == null) $order = self::GetCurrentOrder('',false); if ($order == null) return false; return $order->IsInEdit(); } public function IsInEdit(){ return $this->allowEdit() && intval(Session::get('CurrentlyEditingOrder')) == $this->ID; } public function allowEdit() { if($this->isInDB()) { $arrAllowedOrderStatus = array('Completed', 'Provisional'); if($this->HasResourceManagementProduct()) $arrAllowedOrderStatus[] = 'Pending'; return in_array($this->MainStatus, $arrAllowedOrderStatus); } } public function IsBasketEdit() { return $this->getOrderParam('BasketOrderItemEdit'); } public function CurrentlyEditingBasketOrderItem() { if($iOrderItemId = $this->IsBasketEdit()) { $orderItem = $this->OrderItems()->filter('ID', $iOrderItemId)->first(); return $orderItem; } } public function editOrderItem($orderItem) { if($orderItem->Order()->ID == $this->ID) { LogEntry::Log('Currently editing order '.$this->ID); if (Session::get('CurrentlyEditingOrder')) { return false; } Session::set('CurrentlyEditingOrder', $this->ID); $this->SessionID = ServerUtils::RegenerateSessionID(); $this->write(); $this->setOrderParam('CurrentlyEditingOrderItem', $orderItem->ID); return true; } } public static function OrderAlredyInEditMesage() : string { return '

' . _t('OrderAlreadyInEdit', 'Please finish editing your order before proceeding') . '

'; } public function editOrderItemDetails($orderItem) { if($this->editOrderItem($orderItem) !== false) { $this->setOrderParam('EditOrderItemDetails', 1); $orderItem->AddParticipantsAsPendingParticipants($orderItem); return true; } return false; } /** * Check whether the order is empty * @return boolean, true if the order has no order items, false otherwise */ public function IsEmpty(){ return !$this->OrderItems()->first(); } public function HasHiddenTemplate(){ $bIsStaff = MemberExtension::IsStaff(); foreach ($this->OrderItems() as $orderItem) { if ($templateSet = $orderItem->GetTemplateSet()) { foreach ($templateSet->Templates() as $template) { if ((!$bIsStaff && $template->HiddenFromPurchaser) || ($bIsStaff && $template->HiddenFromAdmin)) { return true; } } } } } public function PendingParticipantCount(){ $iCount = 0; foreach ($this->OrderItems() as $orderItem) $iCount += $orderItem->PendingParticipants()->Count(); return $iCount; } /** * Create a new order, set the values for its attributes and enter the values to the database * @param string $strSessionID is the current session id * @param boolean $bWrite, if true, the data is written to the database,no otherwise * @return Order, the details of the order */ public static function NewOrder($strSessionID,$bWrite=true,$bNewPlayerOnly=false,$pendingMember=null) { $siteConfig = SiteConfigOverride::CurrentSiteConfig(); // Abort any orders with same session id to avoid having > 1 orders with pending statuses Order::AbortPending($strSessionID); $order = Order::create(array( 'MainStatus' => 'Pending', 'IPAddress' => ServerUtils::IPAddress(), 'SessionID' => $strSessionID, )); if ($member = MemberExtension::currentUser()) { if ($member->IsStaff()) { $purchaser = Member::get()->byID($member->BookingOnBehalfOfMemberID); $order->update(array( 'StaffID' => $member->ID, 'Source' => 'Staff', 'PurchaserID' => $member->BookingOnBehalfOfMemberID, 'OrganisationID' => $purchaser && $purchaser->Organisation()->Status == 'On' ? $purchaser->OrganisationID : 0, )); } else { $order->PurchaserID = $member->ID; $order->OrganisationID = $member->Organisation()->Status == 'On' ? $member->OrganisationID : 0; } } if (!$order->PurchaserID) { $pendingMember = PendingMember::create(); if(!$bNewPlayerOnly && $siteConfig->AutoFillData) $pendingMember = RandomData::PopulateMember($pendingMember, Template::GetPurchaserTemplate()); } if ($bWrite) { if (!$order->PurchaserID) { $pendingMember->OrganisationID = $order->Organisation()->Status == 'On' ? $order->OrganisationID : 0; $order->PendingPurchaserID = $pendingMember->write(); } $order->write(); } if(Queue::isQueueITEnabled()) { $order->setOrderParam('NoRecover', true); } return $order; } /** * Abort the pending ordersParticipants needed for the order items * @param string $strSessionID, the session id */ public static function AbortPending($strSessionID='',$iAgeInMinutes = 0) { $orders = Order::get()->filter('MainStatus','Pending'); if(empty($strSessionID)) { $strSessionID = session_id(); if(empty($strSessionID)) return;//Just incase still empty return instantly to avoid aborting all pending orders } if($strSessionID != 'All') $orders = $orders->filter('SessionID', $strSessionID); if($iAgeInMinutes) $orders = $orders->filter('LastEdited:LessThan',date(MYSQLDATETIME, DateUtils::MinusMinutesFromNow($iAgeInMinutes))); LogEntry::log('Orders to abort: ' . $orders->count()); foreach ($orders->limit(40) as $order) { // avoid memory issues and timeouts - limited per request if ($order->getOrderParam('BillingPage') == 1) { $siteConfig = SiteConfigOverride::CurrentSiteConfig(); if ($siteConfig->SendEmailToAdminOnOrderAbort) { Message::PrepareAndSendByType('OrderAbortedAdminNotification',$order); } if ($siteConfig->SendEmailToPurchaserOnOrderAbort) { if ($order->PendingPurchaser()) { Message::PrepareAndSendByType('OrderAbortedPurchaserNotification', $order); } } } $order->Abort(); } } public static function DeleteAbortedOrders() { //delete empty orders that are old more than 24 hours $lastEditedTime = date(MYSQLDATETIME, DateUtils::MinusDaysFromDate(strtotime(SS_Datetime::now()->getValue()),1)); $orders = DB::query("SELECT o.ID FROM `Order` o LEFT JOIN `OrderParam` op ON op.OrderID = o.ID AND op.Name = 'BillingPage' WHERE o.MainStatus = 'Aborted' AND o.LastEdited <= '$lastEditedTime' AND IFNULL(op.Value, 0) != 1 ORDER BY o.ID DESC LIMIT 10")->column('ID'); foreach($orders as $orderID) { $order = Order::get()->byID($orderID); $order->delete(); } //delete empty orders that are old more than 90 days $lastEditedTime = date(MYSQLDATETIME, DateUtils::MinusDaysFromDate(strtotime(SS_Datetime::now()->getValue()),90)); $orders = DB::query(" SELECT o.ID FROM `Order` o JOIN OrderParam op on op.OrderID = o.ID and op.Name = 'BillingPage' WHERE `MainStatus` = 'Aborted' and o.LastEdited <= '$lastEditedTime' AND op.Value = 1 ORDER BY o.ID DESC LIMIT 10")->column('ID'); foreach($orders as $orderID) { $order = Order::get()->byID($orderID); $order->delete(); } } /** * Abort the orders and update the status in the database */ public function Abort() { LogEntry::log('Abort ' . $this->Reference); $this->MainStatus = 'Aborted'; $this->SessionID = ''; //clear session id so not resotre orders after they get aborted. $this->write(); $this->RemoveMemberDocuments(); DatabaseUtils::UpdateField($this->Transactions(),'Status','Aborted'); } public function RemoveMemberDocuments() { if (!$this->ID) return; $memberDocuments = $this->MemberDocuments(); foreach ($memberDocuments as $memberDocument) { if ($file = $memberDocument->File()) { if ($file->exists()) { $file->delete(); } } $memberDocument->delete(); } } //TODO use polymorphism on below function public function IsOrderItemsAvailableWhileAborted(){ $bAvailable = true; $arrPost = array(); $dlOrderItems =$this->OrderItems(); foreach($dlOrderItems as $orderItem){ $product = $orderItem->Product(); $arrPost['ProductID'] = $product->ID; $arrPost['Quantity'] = $orderItem->Quantity; $event = $orderItem->Events()->First(); switch ($product->Type){ case 'FixedEvent' : $arrPost['EventID'] = ($event) ? $event->ID:'0'; break; case 'TemplateEvents' : $dtStartDateTime = strtotime($event->StartDateTime); $arrPost['Availability']['StartDate'] = date(UKJSDATE,$dtStartDateTime); $arrPost['Availability']['StartTime'] = date(MYSQLTIME,$dtStartDateTime); break; case 'EventSeries': $arrPost['EventGroupID'] = $event->EventGroupID; $dlProductGroups = $product->ProductGroups(); $arrPost['ProductGroupID'] = ($dlProductGroups->count())?$dlProductGroups->sort('Priority DESC')->ID:0; break; case 'PhysicalItem': if($product->PhysicalItem()->StockControlled){ $arrPost['RelatedProduct_'.$product->ID] = $orderItem->Quantity; $arrPost['RelatedProduct_' . $product->ID . '_ItemType'] = $orderItem->PhysicalItemTypeID; } } $bAvailable = $bAvailable && $product->IsAvailable($orderItem->Quantity,strtotime($event->StartDateTime),'',$event->ID,'',false,$arrPost) == 100; } return $bAvailable; } /** * PerformOneOrderOnly * if the site config is set to one order only remove any other OrderItems, called from the * AddItems function */ private function PerformOneOrderOnly(){ if(SiteConfigOverride::CurrentSiteConfig()->OneItemOnly) foreach($this->OrderItems() as $orderItem) self::RemoveFromBasket($orderItem); } private function PerformProductGroupOneOrderOnly($iSequence) { $preItems = $this->OrderItems()->exclude('Sequence', $iSequence); $postItems = $this->OrderItems()->filter('Sequence', $iSequence); $bItemsRemoved = false; foreach($preItems->filter([ 'Product.ProductGroups.ID' => ProductGroup::get()->filter('Products.OrderItems.ID', $postItems->column('ID'))->column('ID'), 'Product.ProductGroups.OneOrderOnly' => true, ]) as $orderItem) { self::RemoveFromBasket($orderItem); $bItemsRemoved = true; } if ($bItemsRemoved) { $this->ReCalculate(true); } } public static function RemoveFromBasket($orderItem) { $orderItem->Order()->removeOrderItem($orderItem); } /** * Add Items to the order and recalculate * Calls the function of the relavant products to add them to the basket. * Makes the OrderItems, Events and the PendingParticipant objects * @param array $arrPost * @param boolean $bWrite, if true, the data is written to the database,no otherwise * @param int $iSeq, sequence number * @param boolean $bUseAnonymousMembers, true if anonymous members are considered, false otherwise */ public function AddItems($arrPost,$bWrite = true, $iSeq = 0, $bCheckAvailability = true) { FileUtils::BuskerLog('AddItems - '.StringUtils::Obj2Str($arrPost).' - '.StringUtils::Obj2Str($bWrite)); LogEntry::log('AddItems - '.StringUtils::Obj2Str($arrPost).' - '.StringUtils::Obj2Str($bWrite),999); $this->ErrorCode = ''; $this->Error = ''; $this->setOrderParam("BillingPage", '0'); if ($bWrite) { if(!$this->IsInEdit()) $this->PerformOneOrderOnly(); } $this->PerformWaitingListOrderCheck(); if ($iSeq == 0) $iSeq = $this->GetNextSequence(); $iIsAvailable = ''; if (ArrayUtils::IsSetAndNotEqual($arrPost, 'ProductGroupID', 0)) { $events = Event::get()->byIDs(explode(',', $arrPost['EventSeriesID'])); if($events) { $glEvents = new GroupedList($events); foreach ($glEvents->groupedBy("ProductID") as $eventGroup) { $arrPostforGroup = $arrPost; $arrPostforGroup['ProductID'] = $eventGroup->ProductID; $arrPostforGroup['EventSeriesID'] = implode(',', $eventGroup->Children->Column('ID')); $this->AddProductItems($arrPostforGroup, $bWrite, $iSeq, $bCheckAvailability, $strErrorMessage, $iIsAvailable); if($productGroup = ProductGroup::get()->byID($arrPost['ProductGroupID'])) foreach($this->OrderItems() as $orderItem) if($orderItem->Sequence == $iSeq) { $orderItem->ProductGroupID = $productGroup->ID; if($bWrite) $orderItem->write(); } } } } else { $this->AddProductItems($arrPost, $bWrite, $iSeq, $bCheckAvailability, $strErrorMessage, $iIsAvailable); } if($bWrite) { $this->PerformProductGroupOneOrderOnly($iSeq); } return $this; } function AddProductItems($arrPost, $bWrite, $iSeq, $bCheckAvailability, &$strErrorMessage, &$iIsAvailable) { FileUtils::BuskerLog(' AddProductItems'); $bEditMode = $this->isInDB() ? $this->IsInEdit() : self::IsOrderEditMode(); $product = DataObject::get_by_id($arrPost['ProductClassName'], $arrPost['ProductID']); if (isset($arrPost['EventID'])) { self::CheckForOverBooking($arrPost['EventID']); } OrderItemLock::LockByProduct($product, $arrPost); $iQuantity = isset($arrPost['Quantity']) ? abs(intval($arrPost['Quantity'])) : 1; if(isset($arrPost['ProductGroupProduct']) && !empty($arrPost['ProductGroupProduct'])) $arrPost['ProductID'] = $arrPost['ProductGroupProduct']; $alRelatedProducts = new ArrayList(); $strErrorMessage = ''; if ($product && ArrayUtils::IsAllSetAndNotEmpty($arrPost, array('ProductID','ProductClassName'))) { if($iQuantity == 0) $iIsAvailable = 115; else{ $iIsAvailable = 100; if ($bCheckAvailability) { FileUtils::BuskerLog(' Run is Available check'); $iIsAvailable = $product->IsAvailable($arrPost, $strErrorMessage, $bEditMode); FileUtils::BuskerLog(' End Available check - '.$iIsAvailable); if (isset($arrPost['EventID'])) { self::CheckForOverBooking($arrPost['EventID']); } } if($iIsAvailable == 100){ $arrRequiredProducts = []; if ($product->HasEnabledRequiredUpsells()) { foreach($product->ProductUpsells() as $upsell) { if(!$upsell instanceof ProductUpsellBookingPageRequiredOne) { continue; } if (!$upsell->HasEnabledUpsells()) { continue; } if($arrRequiredProductsData = ArrayUtils::IsSetAndNotEmpty($arrPost, "RequiredProduct_{$upsell->ID}")) { if($iRequiredProduct = ArrayUtils::IsSetAndNotEmpty($arrRequiredProductsData, 'Product')) { if($requiredProduct = $upsell->Upsells()->byID($iRequiredProduct)) { $arrRequiredData = ArrayUtils::IsSetAndNotEmpty($arrRequiredProductsData, $iRequiredProduct, []); if($requiredProduct->IsAvailable($arrRequiredData,$strErrorMessage,$bEditMode) != 100) { $iIsAvailable = 206; break; } else { $arrRequiredProducts[] = [ 'Product' => $requiredProduct, 'Data' => $arrRequiredData, ]; } } else { $iIsAvailable = 111; } } else { $iIsAvailable = 111; } } else { $iIsAvailable = 111; } } } $alRelatedProducts = $this->SelectedRelatedProducts($arrPost); if(!$this->NewPlayerOnly && $alRelatedProducts->first()) { foreach ($alRelatedProducts as $relatedProduct) { if ($relatedProduct->IsAvailable($arrPost, $strErrorMessage, $bEditMode) != 100) { $iIsAvailable = 206; } } } //Check for protected events - user can book the same quantity as in the previous event //Those protected events are only for products that have fixed events. if (in_array($product->ClassName,array('ProductFixedEvent','ProductEventSeries')) && $events = $product->GetEventsFromPost($arrPost)) { foreach(ProductEventOrder::GetCurrentProductEventOrdersForMember(Member::get()->byID(Order::getRealPurchaserID(null, false)))->filter(array( 'NextEventID' => DatabaseUtils::QuickMap($events, 'Event.ID','Event.ID'), 'NextProductID' => $product->ID, )) as $productEventOrder) if (($iProtectedQuantity = $productEventOrder->ProtectedQuantity()) < $product->AddItemQuantity($arrPost)) { $iIsAvailable = $product->AddItemQuantity($arrPost) == 1 || $iProtectedQuantity == 0 ? 212 : 213; break; } } if($iIsAvailable == 100){ if($member = MemberExtension::currentUser()) { if($member->ID == SiteConfigOverride::CurrentSiteConfig()->SlowDownAddingOrderItemForUser) { if($iSlowDown = SiteConfigOverride::CurrentSiteConfig()->SlowDownAddingOrderItem) { sleep($iSlowDown); } } } FileUtils::BuskerLog(' Run new orderitems'); $newOrderItem = $product->AddItemsToOrder($this,$iSeq, $arrPost, $bWrite)->first(); $requiredOrderItems = new ArrayList; foreach($arrRequiredProducts as $arrProduct) { $requiredOrderItems->merge($arrProduct['Product']->AddItemsToOrder($this,$iSeq, $arrProduct['Data'], $bWrite,$newOrderItem)); } if (isset($arrPost['EventID'])) { self::CheckForOverBooking($arrPost['EventID']); } FileUtils::BuskerLog(' End new orderitems'); // get all the participant information and remove the order items from the order //need TO Haddle Edit Booking if(!$this->NewPlayerOnly && $bEditMode && $bWrite && $newOrderItem){ $doEditingOrderItem = $this->CurrentlyEditingOrderItem(); $newOrderItem->PurchserID = $doEditingOrderItem->PurchaserID; if($doEditingOrderItem && $doEditingOrderItem->ProductID == $product->ID){ $product->EditBooking($doEditingOrderItem, $newOrderItem); if (!empty($doEditingOrderItem->AddedByID)) //keep the old record for added by $newOrderItem->AddedByID = $doEditingOrderItem->AddedByID; foreach($requiredOrderItems as $orderItem) { $orderItem->Sequence = $doEditingOrderItem->Sequence; $orderItem->write(); } $newOrderItem->Sequence = $doEditingOrderItem->Sequence; $newOrderItem->write(); } } } } if($iIsAvailable == 110) $strErrorMessage .= 'This Resource is close in this time period '; } } else $iIsAvailable = 116; FileUtils::BuskerLog('Availablecode='.$iIsAvailable); LogEntry::log('Availablecode='.$iIsAvailable,999); $this->PerformWaitingListOrderCheck(false); if (!$this->NewPlayerOnly) { $doEditingOrderItem = $this->CurrentlyEditingOrderItem(); foreach($alRelatedProducts as $relatedProduct) { $relatedProduct->AddItemsToOrder($this, $doEditingOrderItem ? $doEditingOrderItem->Sequence : $iSeq, $arrPost, $bWrite, $newOrderItem ? $newOrderItem : 0); } $this->ReCalculate($bWrite); } FileUtils::BuskerLog(' Run Package Matching'); if($bWrite) NewPackage::PackageMatching($this); FileUtils::BuskerLog(' End Package Matching'); LogEntry::log('FinalAvailablecode='.$iIsAvailable,999); FileUtils::BuskerLog('FinalAvailablecode='.$iIsAvailable); if($iIsAvailable != 100) { $this->Error = ErrorUtils::getErrorMessage($iIsAvailable).'
'.$strErrorMessage; $this->ErrorCode = $iIsAvailable; LogEntry::log('Error='.$this->Error,300); } OrderItemLock::UnLockByProduct($product); if (isset($arrPost['EventID'])) { self::CheckForOverBooking($arrPost['EventID'],true); } return $this; } public static function CheckForOverBooking($eventID = 0, $sendEmail = false) { $siteConfig = SiteConfigOverride::CurrentSiteConfig(); if ($siteConfig->LogOverBooking) { $result = DatabaseUtils::ArrayFromDBQuery(" SELECT COUNT(e.ID) as 'Count' FROM `Event` e LEFT JOIN OrderItem_Events oie ON oie.EventID = e.ID LEFT JOIN OrderItem oi ON oi.ID = oie.OrderItemID LEFT JOIN `Order` o ON o.ID = oi.OrderID LEFT JOIN Resource r ON r.ID = e.ResourceID WHERE o.MainStatus IN ('Provisional','Completed','Pending') AND e.ID = $eventID "); FileUtils::BuskerLog(' LastOverbookingCheck [ ' . $_POST['EventID'] . ' ] - ' . $result[0]['Count']); if ($result[0]['Count'] >= 2) { if ($sendEmail) { $email = new Email( $siteConfig->NoReplyEmailAddress, 'plamen.nankov@bookingive.com; jordan.crew@bookinglive.com; joe.beck@bookinglive.com', 'Overbooking Detected - ' . $siteConfig->Name, 'Hello guys,

Overbooking has been detected with EventID ' . $_POST['EventID'] . '

' . Director::baseURL() ); $email->send(); } } } return; } /** * Remove Waiting list Items from Order * @param $CleanWaitingListOrdersAnyway boolean */ function PerformWaitingListOrderCheck($bCleanWaitingListOrdersAnyway = true){ $bClear = false; if($this->ID){ if($bCleanWaitingListOrdersAnyway || (!$bCleanWaitingListOrdersAnyway && $this->HasNonWaitingListItem())) $bClear = true; if($bClear && $this->IsWaitingListOrder()){ $this->RemoveWaitingListOrderItems(); $this->unsetOrderParam('IsWaitingListOrder'); } } } function MarkAsWaitingList(){ return $this->setOrderParam('IsWaitingListOrder', 1); } function IsWaitingListOrder() { return !$this->ID || $this->getOrderParam('IsWaitingListOrder') == 1; } /** * Check Waiting list Item * @return boolean */ function HasWaitingListItem(){ foreach ($this->OrderItems() as $orderItem) if($orderItem->IsWaitingListItem()) return true; return false; } /** * Check non Waiting list Item * @return boolean */ function HasNonWaitingListItem(){ foreach ($this->OrderItems() as $orderItem) if(!$orderItem->IsWaitingListItem()) return true; return false; } /** * Get Waiting list Item * @return boolean */ function GetWaitingListItem(){ foreach ($this->OrderItems() as $orderItem) if($orderItem->IsWaitingListItem()) return $orderItem; } // Remove Waiting list order Item public function RemoveWaitingListOrderItems(){ foreach ($this->OrderItems() as $orderItem) { if($orderItem->IsWaitingListItem()) $this->removeOrderItems($orderItem); } } /** * Get valid payment setting * * It will get the first OrderItem with event * and will check for PaymentSetting connected * to it's location. * If there is there is not PaymentSetting connected * to the event location, or there are no items with * events it will return the default PaymentSetting * * @return PaymentSetting */ public function GetValidPaymentSetting() { foreach($this->OrderItems() as $orderItem) if ($event = $orderItem->Events()->first()) break; if (!empty($event) && $iLocationID = $event->getRealLocationID()) if ($paymentSetting = PaymentSetting::get()->filter(array( 'LocationID' => $iLocationID, 'MOTO' => MemberExtension::IsStaff() ))->first()) return $paymentSetting; return PaymentSetting::GetDefaultPaymentSetting(); } /** * Get the Event ending time * @param date $dtStartTime, the start time of the event * @param $eventTemplate * @param date $dtDefaultEndTime, null by deault * @return date the ending time of the event */ public static function GetEventEndTime($dtStartTime,$eventTemplate,$dtDefaultEndTime='') { $dtEndTime = $dtDefaultEndTime ? $dtDefaultEndTime : ''; if($eventTemplate->WithinAvailableHours && $eventTemplate->ProductID){ $strStartDate = date(MYSQLDATEQUOTED,$dtStartTime); $dlEvents = DatabaseUtils::ArrayListFromDBQuery(" SELECT EndDateTime FROM Event WHERE Type='Available' AND ProductID = ".$eventTemplate->ProductID." AND DATE(StartDateTime) = ".$strStartDate." ORDER BY StartDateTime "); if($dlEvents->count()) $dtEndTime = strtotime($dlEvents->first()->EndDateTime); } return $dtEndTime; } /** * Remove order items from the order * Check the packages and apply these. * Remove all the events which were created for the order items and the relations * Remove the stcok controls for the Physical items * Delete all the pending participant for the events * And after all the above is done it recalculated the order with respect to the boolean * * @param int $iOrderItemID, the id of the data item to be removed * @param boolean $bRecalculate, true by default */ function removeOrderItems(OrderItem $orderItem, $bRecalculate = true, $bRemoveAllInSameSequence = true){ $orderItems = $bRemoveAllInSameSequence ? $this->OrderItems()->filter('Sequence', $orderItem->Sequence) : [$orderItem]; foreach($orderItems as $orderItemToRemove) { $this->removeOrderItem($orderItemToRemove); } if(!$this->HasWaitingListItem()) { $this->unsetOrderParam('IsWaitingListOrder'); } NewPackage::PackageMatching($this); if($bRecalculate) { $this->ReCalculate(true); } } public function removeOrderItem(OrderItem $orderItemToRemove = null) { $this->setOrderParam("BillingPage", '0'); if($orderItemToRemove) { LogEntry::log('Order::removeOrderItems '. $orderItemToRemove->ID); $orderItemToRemove->RemoveMemberDocuments(); // TODO refactor and move most code into Product // TODO decide if this function will be called with onBeforeDelete or manually $orderItemToRemove->Product()->RemoveOrderItem($orderItemToRemove); $orderItemToRemove->Events()->removeAll(); $orderItemToRemove->removePendingParticipants(); if($physicalItemType = $orderItemToRemove->PhysicalItemType()) StockControl::get()->filter(array( 'PhysicalItemTypesID' => $physicalItemType->ID, 'OrderItemID' => $orderItemToRemove->ID ))->removeAll(); $this->OrderItems()->remove($orderItemToRemove); $orderItemToRemove->delete(); return true; } } /** * Update PhysicalItem order items is the physical item has capacity / stocks * if updates the order item and recalculate the order item * * @param $OrderItemID integer * @param $ProductID integer * @param $PhysicalItemType string * @param $Quantity integer * @return error code integer */ public function UpdatePhysicalItemOrderItem($iOrderItemID, $iProductID, $iPhysicalItemType, $iQuantity){ $iCode = 0; $orderItem = $this->OrderItems('ID = ' . $iOrderItemID)->first(); $product = Product::get()->byId($iProductID); if($product && $product->Type == 'PhysicalItem'){ $physicalItem = $product->PhysicalItem(); $iCode = $physicalItem->CheckPhysicalItemStocks($iPhysicalItemType, $iQuantity, $iOrderItemID); if($iCode == 100){ $stockControl = StockControl::get()->filter(array( 'OrderItemID' => $orderItem->ID, 'Type' => 'Out', 'PhysicalItemTypesID' => $orderItem->PhysicalItemTypeID ))->first(); if($stockControl){ $stockControl->Amount = $iQuantity; $stockControl->PhysicalItemTypesID = $iPhysicalItemType; $stockControl->write(); $orderItem->Quantity = $iQuantity; $orderItem->PhysicalItemTypeID = $iPhysicalItemType; $orderItem->write(); $this->ReCalculate(true); } } } return $iCode; } /** * Returns an array list containing all the products which * are related and which are selected from the booking page * * @param array $arrPost,the details of the product for which the related products are selected * @return ArrayList, the related products to the given product * */ public function SelectedRelatedProducts($arrPost){ $alRet = new ArrayList(); if (ArrayUtils::IsAllSetAndNotEmpty($arrPost,'ProductID') && !ArrayUtils::IsAllSetAndNotEmpty($arrPost,'ProductGroupID')) { if ($product = Product::get()->byID($arrPost['ProductID'])) foreach ($product->GetRelatedProducts('BookingPage') as $product) if ($this->IsRelatedProductSetAndNotZero($arrPost, $product)) $alRet->push($product); } elseif (ArrayUtils::IsAllSetAndNotEmpty($arrPost,'ProductGroupID')) if ($productGroup = ProductGroup::get()->byID($arrPost['ProductGroupID'])) $alRet = $productGroup->addRelatedProducts($this,$arrPost); $alRet->removeDuplicates(); return $alRet; } public function IsRelatedProductSetAndNotZero($arrPost,$product) { $id = $product->ID; if ($product->IsExpandedPricingSchemeEnumeratorEnabled()) { foreach($product->PricingScheme()->SelectablePricingSchemeOptions($product, false) as $alOptions) { if(ArrayUtils::IsSetAndNotEmpty($arrPost, "RelatedProduct_{$id}__{$alOptions->ID}")) { return true; } } } return (isset($arrPost['RelatedProduct_'.$id]) && $arrPost['RelatedProduct_'.$id] != 0) || (isset($arrPost['RelatedProduct_'.$id.'_Quantity']) && $arrPost['RelatedProduct_'.$id.'_Quantity'] != 0); } /** * Returns an array list of the required related products which are * submitted via the booking page * * @param array $arrPost,the details of the product for which the related required products are selected * @return ArrayList the related required products to the given product */ public function SelectedRelatedRequiredProducts($arrPost){ $alRet = new ArrayList(); if(isset($arrPost['RequiredProduct'])){ if ($product = Product::get()->byId($arrPost['ProductID'])) { $requiredOneProdcut = $this->ProductUpsells("ClassName = 'ProductUpsellBookingPageRequiredOne'")->first();//there can be only one! if($requiredOneProdcut) $alRet->push($requiredOneProdcut); } } return $alRet; } /** * Get the next sequence for an Order * * @return int, the next sequence number that is current sequence number + 1 */ public function GetNextSequence(){ $iMaxSeq = 0; foreach($this->OrderItems() as $orderItem) if ($iMaxSeq < $orderItem->Sequence) $iMaxSeq = $orderItem->Sequence; return $iMaxSeq + 1; } /** * Generate check hash of an order * Users Purchaser, OrderItem hashes, and transactions * This helps to determine whether the order has changed with data which * affects the calculations * @return string ,the check hash value of an order */ public function GenerateCheckHash(){ //return implode('/',DatabaseUtils::ArrayFromSQLFile('OrderHash', array('OrderID'=>$this->ID))); $purchaser = $this->Purchaser(); $strCheckHash = '/Order/'.$this->MainStatus.'/Purchaser/' . $purchaser->FirstName .'/'. $purchaser->Surname . '/' . $purchaser->Email; foreach ($this->OrderItems() as $doOrderItem) $strCheckHash .= $doOrderItem->GetCheckHash(); foreach ($this->Transactions("Status IN ('Completed','Authenticated','Refunded')") as $doTransaction) $strCheckHash .= $doTransaction->GetCheckHash(); foreach ($this->Charges() as $chargeItem) $strCheckHash .= $chargeItem->GetCheckHash(); return $strCheckHash; } /** * Recalculates the orders costs * * Calls ReCalculate but forces it to calc by updating the hash and calling write */ public function ForceReCalculate() { $this->CheckHash = ''; $this->ReCalculate(true); } /** * Recalculates the orders costs * This checks the hash of the order and if the previous hash and the current hash doesnt match, * it runs the rest of the function * It makes a order previous version if the order is not a pending order * Also it generates the summary's again and make pending participants of needed. * * * @param bool $bWrite */ public function ReCalculate($bWrite = false, $bRecalculateOrderItems = true) { $strCheckHash = $this->GenerateCheckHash(); $siteConfig = SiteConfigOverride::CurrentSiteConfig(); $orderItems = $this->OrderItems(); $this->extend('updateOrderItemsForRecalculate', $orderItems); if(!$this->NewPlayerOnly) $this->AllParticipantInformation = !$this->IsParticipantInformationIncomplete(); if ($this->CheckHash != $strCheckHash) { $this->CheckHash = $strCheckHash; $bEditMode = $this->IsInEdit(); $bPendingOrEditAndNotEPOS = !$this->EPOS && ($this->MainStatus == 'Pending' || $bEditMode); foreach ($orderItems as $orderItem) { $bOptimizedProduct = in_array($orderItem->Product()->ClassName, array('ProductFixedEvent', 'ProductEventSeries', 'ProductTemplateEvents')); if ($siteConfig->IsProductTemplateOptimized) { $bOptimizedProduct = $orderItem->Product()->ClassName == 'ProductAvailability'; } // TODO currently only those products support the optimization // This is disabled because of the PricingSchemeEnumerationMembershipPeakTimes if($bRecalculateOrderItems /*&& !$bOptimizedProduct*/) $orderItem->ReCalculatePrice(); if($bWrite) { if($orderItem->isChanged()) $orderItem->write(); if($bPendingOrEditAndNotEPOS && $orderItem->ID && !$bOptimizedProduct) $orderItem->MakePendingParticipants($bEditMode); } } if (empty($this->DateRegistered)) $this->DateRegistered = SS_Datetime::now()->getValue(); if (empty($this->OriginalDateRegistered)) $this->OriginalDateRegistered = SS_Datetime::now()->getValue(); $this->TotalCost = 0.0; foreach($orderItems as $orderItem) $this->TotalCost += $orderItem->Cost; $this->TotalTax = 0.0; foreach($orderItems as $orderItem) $this->TotalTax += $orderItem->Tax; $this->ChargesTotal = 0.0; foreach($this->Charges() as $chargeItem) $this->ChargesTotal += $chargeItem->Amount; $this->TotalTransactionValue = 0.0; if ($this->ID || $this->NewPlayerOnly) { $arrRefundTypes = Transaction::GetRefundTypes(); $this->TotalTransactionValue = $this->Transactions() ->filter('Status', array('Completed', 'Refunded')) ->exclude('Type','PackageDiscount') ->exclude('Type',$arrRefundTypes) ->sum('Amount'); $this->TotalTransactionRefund = $this->Transactions() ->filter('Type',$arrRefundTypes) ->sum('Amount'); $this->TotalTransactionValue += Voucher::CalculateAwaitingTransactions($this, $bWrite); // Calculate childcare vouchers UnverifiedTransactionValue $this->UnverifiedTransactionValue = $this->Transactions()->filter(array( 'Status' => 'Completed', 'Type' => 'Voucher', 'Voucher.Status' => 'Verification', 'Voucher.Type' => 'ChildCare', ))->sum('Amount'); } $this->Deposit = $this->ID ? $this->CalculateDeposit() : 0; } if($bWrite) { $this->CalculateProrataPayments($orderItems); } //If hash is not changed there are no changes in the OrderItems but we still need to recalculate the costs if ($this->isInDB()) { $fDiscount = $this->Transactions()->filter(array( 'Status' => array('Completed', 'Refunded'), 'Type' => 'PackageDiscount' ))->sum('Amount'); $arrRefundTypes = Transaction::GetRefundTypes(); $this->TotalTransactionRefund = $this->Transactions() ->filter('Type',$arrRefundTypes) ->sum('Amount'); if ($fDiscount != 0.0) { $this->Deposit = $this->CalculateDeposit(); } $this->FinalTotalCost = $this->TotalCost + $this->TotalTax + $this->ChargesTotal - $fDiscount; $this->AmountDue = $this->TotalAmountDue = $this->AmountDueToPay(); if ($this->AmountDue == 0) { $this->Deposit = 0; } } $this->TotalAmountOverdue = 0; $this->InvoiceAmountDue = 0; $this->WriteTriggeredOnReCalc = false; if ($bWrite && $this->isChanged()) { $this->UpdateStatus(); $this->SkipOnAfterWriteRecalc = true; $this->write(); $this->extend('onRecalculate'); $this->SkipOnAfterWriteRecalc = false; $this->WriteTriggeredOnReCalc = true; } if ($bWrite && $this->MainStatus == 'Completed') $this->updateTimeLineGrouping(); } public function SupportInvoicing() { if ($member = $this->Purchaser()) { return $member->CanPayByInvoice(); } return false; } /** * Group OrderItems in groups for time line * * This grouping is only for ProductFixedEvent */ public function updateTimeLineGrouping() { $arrHashParts = explode('/OrderItem/', $this->CheckHash); unset($arrHashParts[0]); // If there are no OrderItems exit if (count($arrHashParts) == 0) { return; } $arrHashParts[count($arrHashParts)] = StringUtils::GetBeforeFirst($arrHashParts[count($arrHashParts)], '/Trans'); $strOrderEventHash = implode('/OrderItem/', $arrHashParts); if ($this->EventCheckHash == $strOrderEventHash) return; $this->EventCheckHash = $strOrderEventHash; $this->SkipOnAfterWriteRecalc = false; $this->write(); $glOrderItem = new GroupedList($this->OrderItems() ->filter('Product.ClassName', 'ProductFixedEvent') ->leftJoin('OrderItem_Events', 'oie.OrderItemID = OrderItem.ID', 'oie') ->leftJoin('Event', 'e.ID = oie.EventID', 'e') ->sort('e.StartDateTime ASC')); $groupedOrderItems = $glOrderItem->groupBy('ProductID'); $iBookingTimeLineGapInterval = SiteConfigOverride::CurrentSiteConfig()->BookingTimeLineGapInterval; $iDensity = 3; foreach($groupedOrderItems as $iProductID => $dlOrderItems) { $dtCurrentTimePoint = null; $arrOrderItems = array(); $iTimeLineGroupIDCounter = 1; foreach($dlOrderItems as $orderItem) { $event = $orderItem->Events()->first(); if (is_null($dtCurrentTimePoint)) $dtCurrentTimePoint = new DateTime($event->StartDateTime); $dtNextTimePoint = new DateTime($event->StartDateTime); if (empty($iBookingTimeLineGapInterval) || $dtNextTimePoint->diff($dtCurrentTimePoint)->format('%a') > $iBookingTimeLineGapInterval) { //Group found/stop $iTimeLineGroupIDCounter = $this->setOrderItemsTimeLineGroupID($arrOrderItems, $iTimeLineGroupIDCounter, $iDensity <= count($arrOrderItems)); $arrOrderItems = array($orderItem); } else $arrOrderItems[] = $orderItem; $dtCurrentTimePoint = $dtNextTimePoint; } $iTimeLineGroupIDCounter = $this->setOrderItemsTimeLineGroupID($arrOrderItems, $iTimeLineGroupIDCounter, $iDensity <= count($arrOrderItems)); } } private function setOrderItemsTimeLineGroupID($arrOrderItems, $iTimeLineGroupIDCounter, $bGrouped) { foreach($arrOrderItems as $orderItem) { $orderItem->update(array('TimeLineGroupID' => $iTimeLineGroupIDCounter))->write(); if (!$bGrouped) $iTimeLineGroupIDCounter++; } if ($bGrouped) $iTimeLineGroupIDCounter++; return $iTimeLineGroupIDCounter; } public function GetPayableTaxAmount() { $fPayableTax = 0.0; foreach ($this->OrderItems() as $orderItem) { if (floatval($orderItem->CostProrata)) { $fPayableTax += ($orderItem->CostProrata / ($orderItem->Cost + $orderItem->Tax)) * $orderItem->Tax; } else { $fPayableTax += $orderItem->Tax; } } return $fPayableTax; } /** * Loops through the orders items and transactions assigning a proportion of each transaction to each * booking item for use with the AccountingReport. * * Require $orderItems as parameter. This is because OrderItems are processed * by updateOrderItemsForRecalculate hook. * * @param $orderItems */ public function CalculateProrataPayments($orderItems) { if(!$this->isInDB()) return; // Helpers $arrOrderItems = array(); $arrOrderItemProrataCost = array(); foreach($orderItems as $orderItem) { $arrOrderItems[$orderItem->ID] = $orderItem; $arrOrderItemProrataCost[$orderItem->ID] = $orderItem->GetGrossAmount(); } $transactions = $this->Transactions(); // Packages $allDiscounts = $transactions ->filter(array( 'Type' => array('PromotionalCode', 'PackageDiscount'), 'Status' => array('Completed', 'Refunded') ))->toArray(); foreach($allDiscounts as $transaction) { $transactionOrderItemsIDs = $transaction->OrderItems()->column('ID'); foreach(Order::DistributeMoney( $transaction->Amount - $transaction->RefundedAmount, array_intersect_key($arrOrderItemProrataCost, array_combine($transactionOrderItemsIDs, $transactionOrderItemsIDs)) ) as $iOrderItemID => $fAmount) { $arrOrderItemProrataCost[$iOrderItemID] -= $fAmount; } } // Goodwill foreach(self::DistributeMoney( $transactions->filter('Type', 'Goodwill')->sum('Amount'), $arrOrderItemProrataCost ) as $iOrderItemID => $fAmount) $arrOrderItemProrataCost[$iOrderItemID] -= $fAmount; // Vouchers $arrPayableByVoucherOrderItemProrataCost = array(); foreach($orderItems->filter('Product.PayableByChildCareVoucher', true) as $orderItem) $arrPayableByVoucherOrderItemProrataCost[$orderItem->ID] = $arrOrderItemProrataCost[$orderItem->ID]; foreach($transactions->filter(array('Type'=>'Voucher', 'Status'=>['Completed','Refunded'])) as $voucherTransaction) { foreach(Order::DistributeMoney( $voucherTransaction->Amount - $voucherTransaction->RefundedAmount, $arrPayableByVoucherOrderItemProrataCost ) as $iOrderItemID => $fAmount) { $arrOrderItemProrataCost[$iOrderItemID] -= $fAmount; } } // Paid foreach($arrOrderItems as $orderItem) $orderItem->ProrataTransactions()->removeAll(); foreach($transactions ->filter('Type', array( 'Card','Cash','Cheque','PDQ','BACS') ) ->exclude('Status', array('Failed','Aborted', 'Pending',) ) as $transaction) { $arrProportionPaid = self::DistributeMoney( $transaction->Amount - $transaction->RefundedAmount, $arrOrderItemProrataCost); $arrProportionPaidGross = self::DistributeMoney( $transaction->Amount, $arrOrderItemProrataCost); foreach($arrProportionPaid as $iOrderItemID => $dummy) { $orderItem = $arrOrderItems[$iOrderItemID]; $orderItem->ProrataTransactions()->add($transaction, array( 'ProportionPaid' => $arrProportionPaid[$iOrderItemID], 'ProportionPaidGross' => $arrProportionPaidGross[$iOrderItemID] )); $arrOrderItemProrataCost[$iOrderItemID] -= $arrProportionPaid[$orderItem->ID]; } } // Apply prorata costs foreach($arrOrderItemProrataCost as $iOrderItemID => $fCostProrata) { if($fCostProrata < 0) { $fCostProrata = 0.0; } $arrOrderItems[$iOrderItemID]->update(array('CostProrata' => $fCostProrata))->write(); } } public static function DistributeMoney($fAmount, $arrSource) { if(!$fAmount) return array_fill_keys(array_keys($arrSource), 0.0); $arrDistributedAmounts = array(); $fTotalDistributed = 0.0; $fTotalAmount = array_sum(array_values($arrSource)); foreach($arrSource as $key => $fValue) { $fAmountGross = 0; if($fTotalAmount > 0.0) $fAmountGross = floor(($fAmount * ($fValue / $fTotalAmount)) * 100) / 100; $arrDistributedAmounts[$key] = $fAmountGross; $fTotalDistributed = $fTotalDistributed + $fAmountGross; } if($iDifference = round($fTotalDistributed - $fAmount, 2)) { if($iDifference < 0) { $fAdd = 0.01; arsort($arrSource); } else { $fAdd = -0.01; asort($arrSource); } foreach($arrDistributedAmounts as $key => &$value) { if(round($fTotalDistributed, 2) == $fAmount) break; $value += $fAdd; $fTotalDistributed += $fAdd; } } return $arrDistributedAmounts; } /** * Get Booking Time Line Event for the order with respect to an event. * Checks the relative times which are given for the BookingTimeLineEvent objects * for the product of the passed event. * * If $which == 'Next' returns the first item when they * are arranged to the date ascending order * If $which == 'Past' returns the latest passed item if there are any passed events * If $which == 'Any' or something else returns the latest passed item if there * are any passed events or returns the first item when they are arranged to the date ascending order * * This function is called to show summary (espetially the deposit) on basket page. For custom events * the $event parameter is event that is not connected to any OrderItems and is not saved in the database. * This is one of the reasons to pass the $mixedProduct. * * Calulcating the deposit also requires $mixedProduct because multiple products can be attaced to the same * event (this event is Custom event) but having different Deposit TimeLines configured. For example * ProductAvailability and EventUpsell will be connected to the same Custom event. * * @param Event $event * @param string $which enum('Next', 'Past', 'Any') $which - default 'Any' * @param array $arrType * @param int $mixedProduct * @return Bookingtimelineevent object */ function GetBookingTimeLineEvent($event, $which = 'Any', $strType = BookingTimelineEventChangePaymentRequired::class, $mixedProduct = null) { if ($mixedProduct) $product = is_object($mixedProduct) ? $mixedProduct : Product::get()->byID(intval($mixedProduct)); if (empty($product)) { $orderItem = $event->OrderItems()->filter('OrderID', $this->ID)->first(); $product = $orderItem->Product(); } if ($bookingTimeLine = $product->getActiveBookingTimeline()) { return $bookingTimeLine->findTimeLineEvent(intval($event->getRealLocationID()), $strType,strtotime($event->StartDateTime),$which); } return null; } /** * Calculate the deposit amount which needs to be paid, * with respect to the OrderItems and events in the order * * In some cases $eventItemRow should be passed. This will be the * case when there is Custom event and the deposite is calculated * for the summary. In this case the $eventItemRow will not be in * the database and will have no attached OrderItems. * * @param Event $eventItemRow * @return float */ function CalculateDeposit($eventItemRow = null){ $fDeposit = 0.00; if (empty($eventItemRow)) { $eventItemRow = Event::get()->filter('OrderItems.OrderID', $this->ID)->sort('StartDateTime ASC')->first(); } if($eventItemRow) { $arrProcessedProductsIDs = []; foreach(GroupedList::create($this->OrderItems())->groupBy('ProductID') as $iProductID => $alGroup) { if ($timelineEvent = $this->GetBookingTimeLineEvent($eventItemRow, 'Next', BookingTimelineEventChangePaymentRequired::class, $iProductID)) { $fDeposit += $timelineEvent->getDeposit($alGroup); } if($timelineEvent) { $arrProcessedProductsIDs[] = $iProductID; } } if(SiteConfig::current_site_config()->BookingTimeLineDespoitIncludeRemaining && !empty($arrProcessedProductsIDs)) { foreach($this->OrderItems()->exclude('ProductID', $arrProcessedProductsIDs) as $orderItem) { $fDeposit += $orderItem->GetGrossAmount(); } } } return $fDeposit; } /** * Calculate the deposit for the summary * * This is used to calculate the deposit for the summary on the booking page. * It requires this method, because Custom events are not storred in the database * yet and it will create a dummy object (not saved in the database). * * @param array $arrData - this is the data from the form ($_POST) * @return float */ function CalculateDepositForSummary($arrData = array()){ //get deposit for TemplateEvents if ($product = Product::get()->ByID($arrData['ProductID'])) { $event = $product->GetEventsFromPost($arrData); } return empty($event) ? 0.00 : $this->CalculateDeposit($event->first()); } /** * Calculate amount due to pay for an order. * If it is an Invoiced order then it takes the value of all the submitted invoices of the first invoice's cost * depending on the number of invoices. * * @return string amount due */ public function AmountDueToPay(){ $fAmountToPay = 0; if($this->isInDB()) { $siteConfig = SiteConfigOverride::CurrentSiteConfig(); if (!($siteConfig->CancelClearsAmountDue && in_array($this->MainStatus, ['Cancelled','Aborted']))) { $fAmountToPay = $this->FinalTotalCost; $fAmountToPay -= $this->PaidAmount(); if($fAmountToPay < 0) { $fAmountToPay = 0; } } } return number_format($fAmountToPay, 2, '.', ''); } public function TotalAmountDueIncludingUnverified() { $due = $this->TotalAmountDue + $this->UnverifiedTransactionValue; return $due ? number_format($due, 2,'.','') : '0.00'; } /** * Get the purchaser * If a completed order this returns a Member object * else returns the PendingPurchaser object * @return Purchaser, the current purchaser or current pending purchaser */ public function GetPurchaser() { if ($this->MainStatus == 'Pending' && empty($this->PurchaserID) || $this->MainStatus == 'Aborted' && empty($this->PurchaserID) && !empty($this->PendingPurchaserID)) return $this->PendingPurchaser(); return $this->Purchaser(); } /** * Show alternate events for cancel OrderSummaryMyAccountBooking * * TODO this has been reduced to a stub as the previous object * has been removed and this will require rebuilding * * @return ArrayList */ public function ActiveWaitingList(){ return new ArrayList(); } protected function PreparePurchaser($pendingPurchaser) { $purchaser = null; if($pendingPurchaser->Email) $purchaser = Member::get()->filter('Email',$pendingPurchaser->Email)->first(); if(!$purchaser && !MemberExtension::IsStaff() && Member::currentUserID()) $purchaser = MemberExtension::currentUser(); if(!$purchaser) $purchaser = new Member(array('HasConfiguredDashboard' => '1')); $purchaser->write(); if (!$purchaser->InGroup('Purchaser')) $purchaser->Groups()->add(Group::get()->filter('Code','purchaser')->first()); //This is moved to after assigning group so that extensions //can hook on this member being in Purchaser to trigger in onAfterWrite (PDSMSDynamicsMemberExtension) DatabaseUtils::PopulateDataFromObject($pendingPurchaser, $purchaser); $purchaser->write(); $this->PendingPurchaser()->MoveCustomFieldsToMember($purchaser); if ($pendingPurchaser instanceof PendingMember) $pendingPurchaser->MoveUploadedFilesToMemberDocuments($purchaser); $this->PendingPurchaserID = 0; $this->PurchaserID = $purchaser->ID; $this->OrganisationID = $purchaser->Organisation()->Status == 'On' ? $purchaser->OrganisationID : 0; $this->SkipOnAfterWriteRecalc = false; $this->write(); // Promote pending purchaser relations foreach(MemberRelationship::GetRelations($pendingPurchaser, Template::create(['Type' => 'Participant']), $this) as $relation) { $relation->MemberIDa = $purchaser->ID; $relation->write(); } return $purchaser; } /** * Checkout function is used to update the order's status mainly when the payments are completed. * But also this is used for Provisional orders etc. * * This updates the Order's status to the passed Status which is Completed by default. * * Take all the data from the pending members for purchasers and participants and add them in to Member objects. * * Saves the card tokens for the order. * * Sends the messages to purchaser with respect to the Order params set and also will send emails to the * other recipients, and the participants. * * Syncs the order with Xerox, calls the bookinglive basic hooks * * @param string $strTargetStatus, target status of the booking completed by default * @param int $iPurchaserID */ public function Checkout($strTargetStatus='Completed') { $iCount = 15; while (($result = $this->getSetOrderParamValue('CheckoutSessionFlag', 'ON')) == 'ON' && $iCount-- > 0) { sleep(1); } if ($result == 'ON') { LogEntry::log('Order Checkout: CheckoutSessionFlag not released for Order ID ' . $this->ID, 999); return; } $bEditMode = $this->IsInEdit(); if ($this->getOrderParam('AutoTest')) if ($strCheckoutTargetStatus = $this->getOrderParam('CheckoutTargetStatus')) $strTargetStatus = $strCheckoutTargetStatus; if(($this->MainStatus == $strTargetStatus) && !$bEditMode) { $this->setOrderParam('CheckoutSessionFlag', 'OFF'); return; } if($this->IsEmpty()) { $this->setOrderParam('CheckoutSessionFlag', 'OFF'); return; } $pendingPurchaser = $this->GetPurchaser(); LogEntry::log("Checkout($strTargetStatus)" .' - '.StringUtils::Obj2Str($this) .' - '.StringUtils::Obj2Str($pendingPurchaser), 999 ); $purchaser = $this->PreparePurchaser($pendingPurchaser); foreach ($pendingPurchaser->CardTokens() as $cardToken){ $existingCardToken = CardToken::get()->filter(array( 'CardType' => $cardToken->CardType, 'Last4Digits' => $cardToken->Last4Digits, 'ExipiryDate' => $cardToken->ExipiryDate, 'MemberID' => $purchaser->ID ))->first(); if (!$existingCardToken) $purchaser->CardTokens()->add($cardToken); else $cardToken->delete(); } $orderItems = $this->OrderItems(); if($this->MainStatus == 'WaitingList' && $strTargetStatus == 'Completed') foreach($orderItems as $orderItem) $orderItem->Promote(); //if edit order get only the orderitem that is edited if($bEditMode && $this->CurrentlyEditingOrderItem()) { $orderItems = $orderItems->filter('ID', $this->CurrentlyEditingOrderItem()->ID); } if(!$this->NewPlayerOnly) { foreach ($orderItems as $orderItem) { $orderItem->Checkout($strTargetStatus, $bEditMode); } } Order::ApplySimulatedDate(); Voucher::MakeEVouchers($this); Voucher::ProcessVouchers($this); Order::ClearSimulatedDate(); if($this->HasPDFContent()) self::MakePDFContent($this); if ($strTargetStatus == 'WaitingList') $this->WaitingList = true; self::AddAttachmentsAsMemberDocuments($this); $this->MainStatus = $strTargetStatus; // no more edit now if (!$this->NewPlayerOnly){ StartOrder_Controller::EndEditSession(); $this->ReCalculate(!$bEditMode, false); // no need of recalculate on checkout for new players, there is no change } if(empty($this->OriginalCheckoutDate)) { $this->OriginalCheckoutDate = SS_Datetime::now()->getValue(); $this->write(); } if(!$this->WriteTriggeredOnReCalc && $this->isChanged()) $this->write(); $this->UnsetSimulatedDate(); $this->unsetOrderParam('BillingAdminAddedTransactions'); $this->extend('updateCheckout', $this); //send notifications if order is in edit if ($bEditMode) { $this->SendNotification('Amendment', $bEditMode); } if ($purchaser->MarketingPermission) { MailChimp::MailchimpSubscribe($pendingPurchaser->FirstName,$pendingPurchaser->Surname,$pendingPurchaser->Email); } MemberExtension::PushDetailsRequester($purchaser); try { if ($bEditMode) WebHook::process($this,'OrderUpdate'); else { WebHook::process($this->Purchaser(),'NewUser'); WebHook::process($this, 'NewOrder'); } } finally { MemberExtension::PopDetailsRequester(); } if(SiteConfigCategoryZonal::getCurrent()->Enabled) { try { if(ZonalMessage::GetPartyOrderItems($this)->exists()) { MemberExtension::PushDetailsRequester($purchaser); ZonalMessage::CreateNewBookingWithPayment($this)->PrepareNewRequest()->Send(); MemberExtension::PopDetailsRequester(); } } catch(ZonalException $e) { LogEntry::log(get_class($e) .': '. $e->getMessage()); } } Session::clear('ClosedNotifications'); if (!$bEditMode && in_array($strTargetStatus,['Completed','Provisional','WaitingList']) && ($this->EmailSent == 0) && $this->CanSendEmail() ) { $this->EmailSent = 1; $this->write(); } $this->setOrderParam('CheckoutSessionFlag','OFF'); } function HasParticipants() { foreach ($this->OrderItems() as $orderItem) if ($list = $orderItem->ParticipantsForItem()) if ($list->first()) return true; } //TODO combine all three (HasPDFContent,MakePDFContent,AddAttachmentsAsMemberDocuments) //of these functions function HasPDFContent() { foreach ($this->OrderItems() as $orderItem) if ($product = $orderItem->Product()) if (!empty($product->PDFContent)) return true; } public static function MakePDFContent($order) { $member = null; // This might add duplicate document for a member as document might contain merge // tags with specific data that will be later merged with the custom contents $orderPurchaser = $order->Purchaser(); $arrPurchaserProducts = []; foreach ($order->OrderItems() as $orderItem) { if ($product = $orderItem->Product()) { if (!empty($product->PDFContent)) { if ($iEventGroup = $orderItem->EventGroup) { if ($order->OrderItems()->filter(['EventGroup' => $iEventGroup, 'Sequence' => $orderItem->Sequence])->sort('GroupSequence asc')->first()->ID != $orderItem->ID) { continue; } } if ($orderItem->ParentOrderItemID > 0) { if ($event = Event::get()->filter('OrderItems.ID', $orderItem->ParentOrderItemID)->first()) { $member = $event->Participants()->filter('OrderItemID', $orderItem->ParentOrderItemID)->first(); } } else { $member = ($event = $orderItem->Events()->first()) ? $event->Participants()->filter('OrderItemID', $orderItem->ID)->first() : $orderPurchaser; } MemberDocument::createNew( $product->Name . '.pdf', $orderItem, $member, false, null, $product, null, null, true ); if (!isset($arrPurchaserProducts[$product->ID])) { if ($orderPurchaser->ID != $member->ID) { if ($product->ClassName == 'ProductMembership') { continue; } if ($ParentOrderItem = $orderItem->ParentOrderItem()) { if ($ParentOrderItem->Product()->ClassName == 'ProductMembership') { continue; } } MemberDocument::createNew( $product->Name . '.pdf', $orderItem, $orderPurchaser, false, null, $product, null, null, true ); $arrPurchaserProducts[$product->ID] = $product->ID; } } } } } } public static function AddAttachmentsAsMemberDocuments($order) { //product groups will not be added here because when we book a product which is in some product group //the user will recieve this attachement. foreach ($order->OrderItems() as $orderItem) { $arrMember['PurchaserAttachments'] = $purchaser = $order->Purchaser(); if ($event = $orderItem->Events()->first()) { if ($eventMember = $event->Participants()->filter('OrderItemID', $orderItem->ID)->first()) $arrMember['ParticipantAttachments'] = $eventMember; if ($resource = $event->Resource()) { $arrTypes['Resource'] = $resource; if ($location = $resource->Location()) $arrTypes['Location'] = $location; } } $arrTypes['Product'] = $product = $orderItem->Product(); foreach ($arrTypes as $objectType => $type) { foreach ($arrMember as $strAttachementsType => $member) { foreach ($type->$strAttachementsType() as $attachment) { self::AddAttachmentAsMemberDocument($member, $attachment, $orderItem); } } } foreach ($arrMember as $strAttachementsType => $member) { self::AddOrganisationDocumentsAsMemberDocument($member, $orderItem); } //attach the .ics files as member documents foreach ($arrMember as $strAttachementsType => $member) { self::AddIcsFilesAsMemberDocument($member, $orderItem); } } } protected static function AddIcsFilesAsMemberDocument($member,OrderItem $orderItem) { if (SiteConfig::current_site_config()->EnableSendingICSFiles) { if ($orderItem->Events()->first()) { $product = $orderItem->Product(); if (!in_array($product->ClassName,['ProductPhysicalItem','ProductEventUpsell','ProductMembership'])) { $strName = 'Calendar_' . FileUtils::SanitizeFileName($product->Name) . "_" . $member->ID . "" . $orderItem->ID . ".ics"; if (!$member->Documents()->filter(array( 'Name' => $strName, 'OrderItemID' => $orderItem->ID ))->exists()) { CalendarMemberDocument::createNew( $strName, $orderItem, $member, false ); } } } } } protected static function AddOrganisationDocumentsAsMemberDocument($member,OrderItem $orderItem) { if ($organisation = $member->Organisation()) { if($organisation->Status != 'On') { return; } if ($organisation->OrganisationRoles()->first()) { foreach (OrganisationDocument::get()->filter(array( 'OrganisationID' => $organisation->ID )) as $organisationDocument) { $file = $organisationDocument->File(); $file->Title = $organisationDocument->Name; self::AddAttachmentAsMemberDocument($member, $file, $orderItem,$organisation->ID); } } } } protected static function AddAttachmentAsMemberDocument($member, $attachment, OrderItem $orderItem,$organisationID=0) { if (!$member->Documents()->filter(array( 'Name' => $attachment->Title, 'OrderItemID' => $orderItem->ID ))->exists()) { MemberDocument::createNew( $attachment->Title, $orderItem, $member, false, null, null, $attachment, $organisationID ); } } /** * Send an email containing a notification of confirmation to the purchaser */ function SetOrderChangedNotification(){ Message::PrepareAndSendByType('Confirmation',$this); SMSMessage::PrepareAndSendByType('Confirmation', $this); return $this; } /** * Send an email type of Amendment to the purchaser */ public function SendNotification($strType, $bEditMode = false){ Message::PrepareAndSendByType($strType, $this, $bEditMode); if($this->SMSNotification) { SMSMessage::PrepareAndSendByType($strType, $this, $bEditMode); } return $this; } public static function GetOrderItemsGrouped($orderItems) { $arrOrderItems = $orderItems->toArray(); uasort($arrOrderItems, function ($a,$b) { $strA = $strB = '0000-00-00 00:00:00'; if ($aEvent = $a->Events()->first()) { $strA = $aEvent->StartDateTime; } if ($bEvent = $b->Events()->first()) { $strB = $bEvent->StartDateTime; } return strcmp($strA, $strB); }); $arrOrderItemsGroups = array(); foreach($arrOrderItems as $orderItem) { $keyComponents = array( $orderItem->Sequence, $orderItem->ProductID, $orderItem->ChoosenDayParticipant, $orderItem->ChoosenDay, ); if($event = $orderItem->Events()->first()) { if($event->isInDB()) $keyComponents[] = $event->ID; else { $keyComponents = array_merge($keyComponents, array( $event->StartDateTime, $event->EndDateTime, $event->ResourceID, $event->ProductID, )); } } $key = implode('_', $keyComponents); if(!isset($arrOrderItemsGroups[$key])) $arrOrderItemsGroups[$key] = new ArrayList(); $arrOrderItemsGroups[$key]->add($orderItem); } return $arrOrderItemsGroups; } public function GroupOrderItemsForSummary() { $order = $this->duplicate(false); $order->OriginalID = $this->ID; $newOrderItems = array(); foreach(self::GetOrderItemsGrouped($this->OrderItems()) as $alOrderItems) { $newOrderItem = $alOrderItems->shift(); foreach($alOrderItems as $orderItem) { $newOrderItem->Cost += $orderItem->Cost; $newOrderItem->Quantity += $orderItem->Quantity; } $newOrderItems[] = $newOrderItem; } foreach($newOrderItems as $orderItem) { // Unset order item id before adding to order to prevent loading orignal related order items... $id = $orderItem->ID; $orderItem->ID = 0; $order->OrderItems()->add($orderItem); // ... and assign it again to keep all relations to order item like location resource etc $orderItem->ID = $id; } foreach($this->Transactions() as $transaction) $order->Transactions()->add($transaction); foreach($this->OrderNotes() as $orderNote) $order->OrderNotes()->add($orderNote); return $order; } /** * Get the array summary for a controller. * This summary array is stored as YAML in the database and if saved previously it uses that and returns, else it generates the summary and * returns. The summaries are different for each status of the booking, the Pending bookings has a different set, completed has another etc. * * @param integer $Context, boolean $Regenerate, boolean $ReplaceExisting * @return html content */ public function GetSummaryForController($strContext = '',$bRegenerate = false,$bReplaceExisting = false, $strSummaryType = ''){ if ($this->NewPlayerOnly) return ''; $OrderSummary = new OrderSummary(); return $OrderSummary->GetBookingSummary('BookingSummary'.$strContext,$this); } /** * Return a link to edit order item * @return a URL for order item */ public function ReturnToEditLink() { if ($this->IsInDB()) { if($orderItem = $this->CurrentlyEditingOrderItem()) return Director::baseURL() . 'book/edit/o/' . $orderItem->ID; } } /** * If the order can be editeed, then return to that link * @return $Link string */ public function HasReturnToEditLink() { $strLink = $this->ReturnToEditLink(); return !empty($strLink); } public function PurchaserCustomerAdminLink() { if($purchaser = $this->Purchaser()) return Director::baseURL() . 'admin/customers/Purchaser/EditForm/field/Purchaser/item/' . $purchaser->ID . '/edit'; } /** * Take the summary of an order for Email context * @return */ function OrderSummaryEmail(){ return $this->GetSummaryForController('Email'); } /** * Put the order summaries to an array * @return ArrayData contains the summary of an order */ function OrderSummaryCMS(){ return $this->GetSummaryForController('CMSAdmin', true, true); } function OrderSummaryAdminEdit(){ return $this->GetSummaryForController('AdminEdit', true, true); } public function HasErrors() { return !empty($this->Error); } /** *Change the Status of the transaction from Pending to Aborted */ public function AbortCardPendingTransactions(){ foreach($this->Transactions()->filter(array( 'Status' => 'Pending', 'Type' => 'Card') ) as $transaction){ $transaction->Status = 'Aborted'; $transaction->write(); } $this->ReCalculate(true); } /////////////////////// // // Merge Elements /** * Take the reference of the current order * @return varchar, the reference of the current order */ public function OrderReference() { return $this->Reference; } /** * get the summary of the order * @return varchar, the summary of the order */ public function OrderSummary() { return $this->Summary; } /** * get the deposit amount of the order * @return decimal, deposit of the order */ public function DepositAmount() { return $this->Deposit; // return number_format($this->Deposit,2); } /** * Get the total amount of transaction of the order * @return decimal, total amount of transaction */ public function PaidAmount() { return $this->TotalTransactionValue - $this->TotalTransactionRefund; //return number_format($this->TotalTransactionValue,2); } /** * Get the amount to be paid for the order * @return decimal, amount to be paid for the order */ public function DueAmount() { return $this->AmountDue; // return number_format($this->AmountDue,2); } /** * Get the full name of the purchaser * @return string, the full name of the purchaser */ public function PurchaserFullName() { if ($purchaser = $this->GetPurchaser()) return $purchaser->FullName(); } /** * Take the SocialTitle of the purchaser * @return String, the SocialTitle of the purchaser */ public function PurchaserTitle() { if ($purchaser = $this->GetPurchaser()) return $purchaser->SocialTitle; } /** * Get the first name of the purchaser * @return string, the first name of the purchaser */ public function PurchaserFirstName() { if ($purchaser = $this->GetPurchaser()) return $purchaser->FirstName; } public function HasDeposit() { return $this->Deposit > 0; } /** * Get the sur name of the purchaser * @return string ,the sur name of the purchaser */ public function PurchaserSurname() { if ($purchaser = $this->GetPurchaser()) return $purchaser->Surname; } /** * Check whether there is some amount to be paid * @return boolean,true if there is some amount to be paid, false otherwise */ public function IsBalanceToPay(){ return $this->AmountDue > 0; } /** * the tCheck whether total amount for the order has been paid * @return boolean, true if the amount is paid, false otherwise */ public function IsPayFullAmount(){ return $this->IsBalanceToPay(); } /** * Get the location of the order * @return Varchar, the location */ public function OrderLocation(){ return WackyUtils::GetPartyLocation($this); } /** * Get the currency symbol used * @return Vachar, the currency symbol */ public function CurrencySymbol(){ if($strCode = $this->CurrencyCode()) return CurrencyUtils::Symbol($strCode); } /** * Get the currency code * @return ISO 4217 currency code */ public function CurrencyCode(){ return SiteConfigOverride::GetDefaultCurrencyCode(); } /** * Get the address of the purchaser of the order * @return Varchar, the address */ public function PurchaserAddress() { if ($purchaser = $this->GetPurchaser()) return $purchaser->getFullAddress('TEXT'); } /** * Get the address of the purchaser of the order * @return Varchar, the address */ public function PurchaserAddressHTML() { if ($purchaser = $this->GetPurchaser()) return $purchaser->getFullAddress('HTML'); } /** * Get the post code of the purchaser of the order * @return Varchar, the Postcode */ public function PurchaserPostCode() { if ($purchaser = $this->GetPurchaser()) return $purchaser->PostCode; } /** * Get the number of mobile telephone of the purchaser of the order * @return Varchar,the number of mobile telephone */ public function PurchaserMobileTelephone() { if ($purchaser = $this->GetPurchaser()) return $purchaser->MobileTelephone; } public function PurchaserTelephone() { if ($purchaser = $this->GetPurchaser()) return $purchaser->Telephone; } public function PurchaserOrganisationName() { if ($purchaser = $this->GetPurchaser()) return $purchaser->Organisation()->Name; } public function PurchaserID(){ if ($purchaser = $this->GetPurchaser()) return $purchaser->ID; } public function CompanyName(){ return SiteConfigOverride::CurrentSiteConfig()->Name; } public function InvoiceFooter(){ return SiteConfigOverride::CurrentSiteConfig()->InvoiceFooter; } public function CompanyAddress(){ return SiteConfigOverride::CurrentSiteConfig()->getFullAddress('TEXT'); } public function FirstOrderItemID(){ if($orderItem = $this->OrderItems()->first()) return $orderItem->ID; } public function CompanyPhoneNumber() { return SiteConfigOverride::CurrentSiteConfig()->ContactNumber; } public function CompanyVatRegisterNumber() { return SiteConfigOverride::CurrentSiteConfig()->VATRegisterNumber; } /** * Get the number of another telephone of the purchaser of the order * @return Varchar,the number of another telephone */ public function PurchaserTelephoneOther() { if ($purchaser = $this->GetPurchaser()) return $purchaser->TelephoneOther; } /** * Get custom texts for the order, * goes through al lthe order items and the location and merge their CustomEmailText fields in to * an array and returns a ArrayList * @return texts */ public function GetCustomTexts($bHTML = false, $object = null) { $arrText = array(); $arrEventDescriptions = array(); $orderItems = $this->OrderItems(); if(get_class($object) == 'OrderItem') { $orderItems = $orderItems->filter('ID', $object->ID); } foreach ($orderItems as $orderItem) { $arrText = array_merge($arrText, $orderItem->GetCustomTexts()); foreach ($orderItem->Events() as $event) { if ($location = $event->getRealLocation()) { $arrText[] = $location->CustomEmailText; } //proceed with event description if ($event->Description && $event->ShowInEmail) { $dStartDate = date('Y-m-d', strtotime($event->StartDateTime)); $dEndDate = date('Y-m-d', strtotime($event->EndDateTime)); $dEventDateDescription = date('d/m/Y H:i', strtotime($event->StartDateTime)) . ' - ' . ($dStartDate == $dEndDate ? date('H:i', strtotime($event->EndDateTime)) : date('d/m/Y H:i', strtotime($event->EndDateTime))); if (empty($arrEventDescriptions[$event->ProductID . '_' . $event->ID])) { $arrEventDescriptions[$event->ProductID . '_' . $event->ID] = array( 'Dates' => array($dEventDateDescription), 'Description' => $event->Description, 'StartDateTime' => $event->StartDateTime, ); } else { $result = array_search($event->Description, array_column($arrEventDescriptions, 'Description')); if ($result !== false) { $i = 0; foreach ($arrEventDescriptions as $key => $value) { if ($result == $i) { array_push($arrEventDescriptions[$key]['Dates'], $dEventDateDescription); $arrEventDescriptions[$key]['StartDateTime'] = min($arrEventDescriptions[$key]['StartDateTime'], $event->StartDateTime); } $i++; } } } } //end processing event descriptions $arrText[] = $event->Resource()->CustomEmailText; } } if (!empty($arrEventDescriptions)) { uasort($arrEventDescriptions, function ($a, $b) { return strcmp($a['StartDateTime'], $b['StartDateTime']); }); foreach ($arrEventDescriptions as $arrEventDescription) { $arrEventDescription['Dates'] = array_unique($arrEventDescription['Dates']); $arrText[] = PresentationUtils::ParseTemplateWithArray( array( 'Dates' => implode(', ', $arrEventDescription['Dates']), 'Description' => $arrEventDescription['Description'] ), 'EventDescriptionForEmail' ); } } $arrText = array_unique($arrText); $strText = ''; if(!empty($arrText)) foreach($arrText as $str) if(!empty($str)) $strText .= $bHTML ? $str : strip_tags(nl2br($str),'
'); return $strText; } /** * Get list of all BCC emails * * Goes through all OrderItems and get BCC emails * * @return array */ public function GetBCCEmails() { $arrBCCEmails = array(); foreach($this->Orderitems() as $orderItem) $arrBCCEmails = array_merge($arrBCCEmails, $orderItem->GetBCCEmails()); $this->extend('updateGetBCCEmails', $arrBCCEmails); return array_unique($arrBCCEmails); } /** * Returns all the locations where the events are booked for. * @return ArrayList, an array list of locations */ public function LocationsBooked() { $alLocations = new ArrayList(); foreach(Event::get()->filter('OrderItems.OrderID', $this->ID) as $event) { $alLocations->add($event->getRealLocation(true)); } return $alLocations; } /** * CustomText get all the CustomTexts from GetCustomTexts function and parse them in to a table. * @return string, a custom text */ public function CustomText() { $dosCustomTexts = $this->GetCustomTexts(); if ($dosCustomTexts->Count()) { $vd = new ViewableData(); return $vd->customise( new ArrayData(array('CustomTexts' => $dosCustomTexts)) )->renderWith(SSViewer::fromString('<% loop CustomTexts %><% end_loop %>
$Text
 
')); } } /** * Get the products contained in an order item * @param Product $products, the products * @return boolean, true if the given product is in the given order item, false otherwise */ function ContainsProducts($products) { foreach ($this->OrderItems() as $orderItem) if ($products->find('ID',$orderItem->ProductID)) return true; } /** * Get the first location of resource of an event of an order item * @return boolean */ function GetFirstLocation() { foreach ($this->OrderItems() as $orderItem) foreach ($orderItem->Events() as $event) return $event->Resource()->Location(); foreach ($this->OrderItems() as $orderItem) if ($orderItem->LocationID) return Location::get()->byId($orderItem->LocationID); } /** * Get first and last dates of events in an order item * @param int $iSeq * @return array, the start and end date and time */ function GetFirstLastDates($iSeq = 0) { $arrStartEndDates = array(); foreach ($this->OrderItems() as $orderItem) { if ($iSeq && $iSeq != $orderItem->Seq) continue; foreach ($orderItem->Events() as $event) $arrStartEndDates[] = array( 'StartDateTime' => $event->StartDateTime, 'EndDateTime' => $event->EndDateTime ); } //this assumes no overlaps, i.e. 9-10, 10-11 //not 8-11,9-10 if (!empty($arrStartEndDates)) usort($arrStartEndDates, function ($a, $b) { return $a['StartDateTime'] > $b['StartDateTime']; }); return $arrStartEndDates; } /** * Get the purchaser email. * @return String, the purchaser email */ public function PurchaserEmail() { if ($purchaser = $this->GetPurchaser()) return $purchaser->Email; } /** * Get the purchaser mobile. * @return String, the purchaser mobile */ public function PurchaserMobile() { if ($purchaser = $this->GetPurchaser()) return $purchaser->MobileTelephone; } /** * Get the name of the first location of the order * @return Varchar, name of the location */ public function VenueName(){ if($location = $this->GetFirstLocation()) return $location->Name; } /** * Get the telephone number of the first location of the order * @return Varchar, the telephone number */ public function VenueTelephone(){ if($location = $this->GetFirstLocation()) return $location->Telephone; } public function FixedEvent(){ foreach($this->OrderItems() as $orderItem) if($orderItem->Product()->Type == 'FixedEvent') return $orderItem; } public function GetFixedEvent(){ foreach($this->OrderItems() as $orderItem) if($orderItem->Product()->Type == 'FixedEvent') return $orderItem->Events()->first(); } function FixedEventDateTime(){ if($event = $this->GetFixedEvent()){ $dtDatetime = strtotime($event->StartDateTime); return DateUtils::PublicDateFormat($dtDatetime).' '.DateUtils::PublicTimeFormat($dtDatetime); } } function FixedEventDate(){ if($event = $this->GetFixedEvent()) return DateUtils::PublicDateFormat(strtotime($event->StartDateTime)); } function FixedEventTime(){ if($event = $this->GetFixedEvent()) return DateUtils::PublicTimeFormat(strtotime($event->StartDateTime)); } public function FixedEventName(){ foreach($this->OrderItems() as $orderItem) if($orderItem->ProductID && $orderItem->Product()->Type == 'FixedEvent') return $orderItem->Product()->Name; } public function TicketsPurchased(){ foreach($this->OrderItems() as $orderItem){ if($orderItem->Product()->Type == 'FixedEvent') return $orderItem->Quantity; } } function FixedEventVenueName(){ $orderItem = $this->FixedEvent(); if($orderItem && $orderItem->Events()->count()) return $orderItem->Events()->first()->Location()->Name; } function FixedEventVenuePhoneNumber(){ $orderItem = $this->FixedEvent(); if($orderItem && $orderItem->Events()->count()) return $orderItem->Events()->first()->Location()->Telephone; } /** * Foreach used instead of filter as filter can cause error on payment/callback * "filter can't be called on UnsavedRelationList" * @return mixed */ public function CustomerNotes(){ foreach($this->OrderNotes() as $orderNote) if($orderNote->Type == 'CustomerNote') return nl2br($orderNote->Note); } public function SetCustomerNotes($strComments){ $orderNote = $this->OrderNotes()->filter('Type', 'CustomerNote')->first(); if($this->MainStatus == 'Pending' || !$orderNote) $orderNote = new OrderNote(array( 'Type' => 'CustomerNote', 'OrderID' => $this->ID )); $orderNote->Note = $strComments; $orderNote->write(); } /** * Check whether the payment is completed or not * @return boolean, true if the payment is incomplete, false otherwise */ public function IfOutstandingPayment(){ return $this->AmountDue > 0; } /** * returns all the order items which has physical items purchased * @return DataList */ function GetPhysicalItemOrderItems(){ return OrderItem::get()->where(" OrderID = $this->ID AND EXISTS ( SELECT 1 FROM Product WHERE ClassName = 'PhysicalItemProduct' AND OrderItem.ProductID = Product.ID ) "); } /** * Get the active time that is left for a web booking * @param String $strType, a default value 'WebBookingKeepAliveTime' * @return int, the time left in minutes for the active booking */ function GetMinutesLeftBeforeExpire($strType='WebBookingKeepAliveTime'){ if ($this->MainStatus != 'Pending') { return; } $iTimeLeft = intval( strtotime($this->LastEdited) + SiteConfigOverride::CurrentSiteConfig()->WebBookingKeepAliveTime * 60 - strtotime(SS_Datetime::now()->getValue()) ); if($iTimeLeft <= 0){ $this->Abort(); Requirements::customScript(" jQuery(document).ready(function(){ bl.Availability.ClearBasketItemCount(); bl.Availability.GetBasketItemCount() }); "); } return ceil($iTimeLeft / 60); } function IfUsingCredits(){ return false; } /** * Check whether the Participant information is incomplete * @return boolean, true if the Participant information is incomplete,false otherwise */ public function IsParticipantInformationIncomplete() { foreach ($this->OrderItems() as $orderItem) if ($orderItem->IsOrderItemParticipantInformationIncomplete()) return true; } public function CheckIfPartyBooking(){ return WackyUtils::IsPartyBooking($this); } public function addTag($strTag) { if (!($orderTag = OrderTag::get()->filter('Title', $strTag)->first())) { $orderTag = OrderTag::create(array( 'Title' => $strTag )); $orderTag->write(); }; if (!$this->Tags()->filter('Title', $strTag)->exists()) { $this->Tags()->add($orderTag->ID); } } /** * Get details of the physical item options of an order item returns a groups list * @return ArrayList, the physical item options */ public function GetPhysicalItemOptionDetails() { $alTemp = ArrayList::create(); $alOutput = ArrayList::create(); foreach ($this->OrderItems() as $orderItem) { $physicalItemOptions = PhysicalItemOption::get()->filter('ID',explode(',', $orderItem->getField('PhysicalItemOptionCSV'))); if ($physicalItemOptions->first()) { $glPhysicalItemOptions = new GroupedList($physicalItemOptions); $glPhysicalItemOptionList = $glPhysicalItemOptions->groupBy('PhysicalItemTypeID'); foreach ($glPhysicalItemOptionList as $iTypeID => $alPhysicalItemOptions) { $doPhysicalItemType = PhysicalItemType::get()->byId($iTypeID); $alTemp->push(DataObject::create(array( 'TypeName' => $doPhysicalItemType ? $doPhysicalItemType->Name : '', 'ItemID' => $doPhysicalItemType ? $doPhysicalItemType->PhysicalItemID : 0, 'ItemName' => $doPhysicalItemType ? $doPhysicalItemType->PhysicalItem()->Name : '', 'SelectedOptions' => $alPhysicalItemOptions ))); } } } $glOutput = new GroupedList($alTemp); foreach ($glOutput->groupBy('ItemID') as $iItemID => $list) if ($list->first()) if ($doPhysicalItem = PhysicalItem::get()->byId($iItemID)) $alOutput->push(ArrayData::create(array( 'ItemID' => $iItemID, 'ItemName' => $doPhysicalItem->Name, 'Children' => $list ))); return $alOutput; } /** * Get the first template * @return Template, the first template */ function FormTemplate(){ return Template::get()->first(); } /** * Get Guest participants * @return guest participant html content */ function Guests(){ $alParticipants = new ArrayList(); foreach($this->OrderItems() as $orderItem) foreach($orderItem->Events() as $event) $alParticipants->merge($event->Participants()); return StringUtils::GetIDListCSV($alParticipants); } public function LastPaymentDate() { //TODO if still needed to use the "booking timeline" instead } public function CompletedTransactions(){ $completedTransactions = new ArrayList; foreach($this->Transactions() as $transaction) if(in_array($transaction->Status, array('Completed','Refunded'))) $completedTransactions->add($transaction); return $completedTransactions; } public function IsHasCompletedTransactions(){ return $this->CompletedTransactions()->count(); } public function IsCompleted(){ return $this->MainStatus == 'Completed'; } public function IsCancelled(){ return $this->MainStatus == 'Cancelled'; } /** * Get Billing page purchaser details from order * @return Purchaser details array list */ function BillingPagePurchaserDetails(){ $alRet = new ArrayList(); $purchaser = $this->GetPurchaser(); if($purchaser && $purchaserTemplate = Template::get()->filter('Type','Purchaser')->first()){ foreach($purchaserTemplate->TemplateItems() as $templateItem) $alRet->push(new DataObject(array( 'Type' => $templateItem->Type, 'Label' => $templateItem->Label, 'Value' => $purchaser->getField($templateItem->Type) ))); } $alRet->removeDuplicates('Type'); return $alRet; } public function BaseURL() { return Director::absoluteBaseURL(); } public function MyAccountLink(){ return Director::absoluteBaseURL() . '/myaccount'; } public function Thumbnail(){ if($this->Product()->ImageID) return ''; } public function AmountPaid() { return $this->PaidAmount(); } public function MinimumAmountOutstanding() { $fAmountRequired = $this->CalculateDeposit(); return number_format( ($fAmountRequired > $this->TotalTransactionValue) ? $fAmountRequired - $this->TotalTransactionValue : 0.00, 2 ,'.','' ); } public function FullAmountOutstanding() { return number_format($this->AmountDue,2,'.',''); } /** * Get Order Unique Participants * @return Member ArrayList */ public function GetUniqueParticipants(){ $alRet = new ArrayList(); $arrParticipantOrderItemsMap = []; foreach($this->OrderItems() as $orderItem) { foreach($orderItem->AllEventParticipants() as $participant) { $arrParticipantOrderItemsMap[$participant->ID][] = $orderItem; if (!$alRet->find('ID', $participant->ID)) { $alRet->push($participant); } } } foreach($alRet as $participant) { $participant->OrderItems = $arrParticipantOrderItemsMap[$participant->ID]; } return $alRet; } public function GetTotalPayableNow() { $fTotalPayable = ($this->Deposit > 0 ? $this->Deposit : $this->AmountDue) - $this->AmountPaid(); return number_format($fTotalPayable,2,'.',''); } public function StaffMember(){ if($event = $this->GetFixedEvent()) return $event->StaffMembers()->first(); } public function StaffMemberName(){ if($staffMember = $this->StaffMember()) return $staffMember->FullName(); } public function StaffMemberEmail(){ if($staffMember = $this->StaffMember()) return $staffMember->Email; } public function StaffMemberAddress(){ if($staffMember = $this->StaffMember()) return $staffMember->getFullAddress('TEXT'); } public function StaffMemberTelephone(){ if($staffMember = $this->StaffMember()) return $staffMember->MobileTelephone; } public function setOrderParams($arrParams,$bIsTemplate = false){ foreach ($arrParams as $name => $value) $this->setOrderParam($name,$value,$bIsTemplate); return $this; } /** * Set Order Param to Order * @param $name string * @param $value string */ public function setOrderParam($name,$value,$bIsTemplate = false,$customFieldID = 0){ //DB::query('LOCK TABLES OrderParam WRITE, OrderParamPattern WRITE, AuditLog WRITE'); if (!$orderParam = $this->OrderParams()->filter('Name',$name)->first()) { $orderParam = new OrderParam(array('Name' => $name)); } $orderParam->update(array( 'Value' => $value, 'Unset' => 0, 'OrderID' => $this->ID, 'IsTemplate' => $bIsTemplate, 'CustomFieldID' => $customFieldID )); $orderParam->write(); //DB::query('UNLOCK TABLES'); return $this; } public function getOrderParam($name){ if (!$this->isInDB()) return ; $orderParam = $this->OrderParams()->filter(array( 'Name' => $name, 'Unset' => 0 ))->first(); if ($orderParam) return $orderParam->Value; } /** * Return the current value of an OrderParam, or null if not set/unset. * Then set new value as provided. * As an atomic process. * * @param string $name * @param mixed $value * @return mixed */ public function getSetOrderParamValue(string $name, $value) { //DB::query('LOCK TABLES OrderParam WRITE, OrderParamPattern WRITE, AuditLog WRITE'); if ($orderParam = $this->OrderParams()->filter('Name',$name)->first()) { $ret = $orderParam->Unset ? null : $orderParam->Value; } else { $ret = null; $orderParam = OrderParam::create(['Name' => $name]); } $orderParam->update([ 'Value' => $value, 'Unset' => 0, 'OrderID' => $this->ID, ])->write(); //DB::query('UNLOCK TABLES'); return $ret; } public function unsetOrderParam($name){ if ($orderParam = $this->OrderParams()->filter('Name',$name)->first()){ $orderParam->Unset = 1; $orderParam->write(); } } public function unsetOrderParams($arrParams){ foreach($arrParams as $strParam) $this->unsetOrderParam($strParam); } public function registerBasketPageError($msg, $type='error0') { $errors = array(); if ($orderParamValue = $this->getOrderParam('BasketPageErrors')) { //LogEntry::log('***'.StringUtils::Obj2str($orderParam).'***',999); $errors = json_decode($orderParamValue, true); } $errors[] = array( 'type' => 'error', 'message' => $msg ); $this->setOrderParam('BasketPageErrors', json_encode($errors)); } public function ChangeDue() { $fChangeDue = 0.0; if ($val = $this->getOrderParam('ChangeDue')) $fChangeDue = floatval($val); return number_format($fChangeDue,2,'.',''); } public function TillReceipt() { if ($content = Content::get()->filter('Type','TillReceipt')->first()) { $strReceipt = $content->Content; if (strpos($strReceipt,'{$OrderReference}') !== false) $strReceipt = str_replace('{$OrderReference}',$this->Reference,$strReceipt); if (strpos($strReceipt,'{$ChangeDue}') !== false) $strReceipt = str_replace('{$ChangeDue}',$this->ChangeDue(),$strReceipt); if (strpos($strReceipt,'{$TotalCostCurrencyFormatted}') !== false) $strReceipt = str_replace('{$TotalCostCurrencyFormatted}',number_format($this->TotalCost,2,'.',''),$strReceipt); if (strpos($strReceipt,'{$DueAmountCurrencyFormatted}') !== false) $strReceipt = str_replace('{$DueAmountCurrencyFormatted}',number_format($this->AmountDue,2,'.',''),$strReceipt); $siteConfig = SiteConfigOverride::CurrentSiteConfig(); if (strpos($strReceipt,'{$CompanyName}') !== false) $strReceipt = str_replace('{$CompanyName}',$siteConfig->Name,$strReceipt); if (strpos($strReceipt,'{$RegisteredNumber}') !== false) $strReceipt = str_replace('{$RegisteredNumber}',$siteConfig->CompanyNumber,$strReceipt); if (strpos($strReceipt,'{$VATNumber}') !== false) $strReceipt = str_replace('{$VATNumber}',$siteConfig->VATRegisterNumber,$strReceipt); if ($arrParts = StringUtils::GetBeforeBetweenAndAfter($strReceipt,'{$OrderItemLOOP{','}ENDOrderItemLOOP}')) { $strLines = ''; foreach ($this->OrderItems() as $orderItem) { $strLine = $arrParts['Between']; if (strpos($strLine,'{$Quantity}') !== false) $strLine = str_replace('{$Quantity}',1,$strLine); if (strpos($strLine,'{$ProductName}') !== false) $strLine = str_replace('{$ProductName}',$orderItem->Product()->Name,$strLine); if (strpos($strLine,'{$EventCost}') !== false) $strLine = str_replace('{$EventCost}',$orderItem->Cost,$strLine); if ($firstEvent = $orderItem->Events()->first()) { if (strpos($strLine,'{$LocationName}') !== false) $strLine = str_replace('{$LocationName}',$firstEvent->Resource()->Location()->Name,$strLine); if (strpos($strLine,'{$EventStartDateDateFormatted}') !== false) $strLine = str_replace('{$EventStartDateDateFormatted}',$firstEvent->Resource()->Location()->Name,$strLine); if (strpos($strLine,'{$EventStartDateTimeFormatted}') !== false) $strLine = str_replace('{$EventStartDateTimeFormatted}',$firstEvent->Resource()->Location()->Name,$strLine); if (strpos($strLine,'{$EventEndDateTimeFormatted}') !== false) $strLine = str_replace('{$EventEndDateTimeFormatted}',$firstEvent->Resource()->Location()->Name,$strLine); } $strLines .= $strLine; } $strReceipt = $arrParts['Before'].$strLines.$arrParts['After']; } if ($arrParts = $arrParts = StringUtils::GetBeforeBetweenAndAfter($strReceipt,'{$CompletedTransactionsLOOP{','}ENDCompletedTransactionsLOOP}')) { $strLines = ''; foreach ($this->Transactions() as $transaction) { $strLine = $arrParts['Between']; if (strpos($strLine,'{$TransactionName}') !== false) $strLine = str_replace('{$TransactionName}',$transaction->Type,$strLine); if (strpos($strLine,'{$AmountCurrencyFormatted}') !== false) $strLine = str_replace('{$AmountCurrencyFormatted}',$transaction->Amount,$strLine); $strLines .= $strLine; } $strReceipt = $arrParts['Before'].$strLines.$arrParts['After']; } return $strReceipt; } return 'no till template'; } /** * Get EventID and Available Slots string * @return string */ public function EPOSEventUpdate() { $strEventUpdate = ''; $arrEvents = array(); foreach ($this->OrderItems() as $orderItem) { foreach ($orderItem->Events() as $event) { if (!in_array($event->ID,$arrEvents)) { $strEventUpdate .= $event->ID.':'.$event->NumberOfSlotsAvailable().','; $arrEvents[] = $event->ID; } } } return $strEventUpdate; } public function GetEditURL() { return 'admin/orders/Order/EditForm/field/Order/item/' . $this->ID . '/edit'; } public function GetWaitinglistQuantitySum() { //A waitling list can only be for one product, but might have quantity //so just isolate how many are on each event (should be equal) and then return one of those numbers (first one) $arrEvents = array(); foreach ($this->OrderItems() as $orderItem) { if ($event = $orderItem->Events()->first()) { $id = $event->ID; if(isset($arrEvents[$id])) $arrEvents[$id]++; else $arrEvents[$id] = 1; } } return array_shift($arrEvents); } public function SortedOrderItems(){ LogEntry::log('Order::SortedOrderItems'); return $this->OrderItems()->sort('Sequence', 'ASC'); } public function ChildCareVouchers() { return Voucher::get()->filter(array( 'Status' => 'Verification', 'ID' => $this->Transactions()->filter(array( 'Type' => 'Voucher', 'Status' => 'Completed' ))->column('VoucherID') )); } public function GetMaximumVouchersAmount() { $fMaximumVouchersAmount = 0.0; foreach ($this->OrderItems() as $orderItem) if ($orderItem->Product()->PayableByChildCareVoucher) $fMaximumVouchersAmount += $orderItem->CostProrata; return $fMaximumVouchersAmount; } public function HasDiscount() { return $this->CompletedTransactions() ->filter('Type', 'PackageDiscount') ->limit(1) ->count(); } public function TotalDiscount() { $fTotalAmount = 0; foreach($this->CompletedTransactions() as $transaction) if($transaction->IsPackageTransaction()) $fTotalAmount += $transaction->Amount; return $fTotalAmount; } public function LastCompletedTransaction() { return new ArrayList(array( $this->CompletedTransactions()->sort('Created','DESC')->first() )); } public function LastOrderItem() { return new ArrayList(array( $this->OrderItems()->sort(array( 'Sequence' => 'ASC', 'Created' => 'DESC' ))->first() )); } public function ExistingID() { if($this->exists()) return $this->ID; return $this->OriginalID; } /** * Check if order's MainStatus is 'Completed' * * If no order is passed will use current * order. * * @param Order $order - default null * @return Order|false */ public static function IsCurrentOrderCompleted($order = null){ if (!$order) $order = Order::GetCurrentOrder(); return $order->MainStatus == 'Completed' ? $order : false; } public function FinalTotalCostAdminFormat() { return $this->CurrencySymbol() . $this->FinalTotalCost; } public function getStartDate() { $dtEarliestDate = false; foreach($this->OrderItems() as $orderItem) { foreach($orderItem->Events() as $event) { $dtEventStart = strtotime($event->StartDateTime); if ($dtEarliestDate == false) $dtEarliestDate = $dtEventStart; if ($dtEventStart < $dtEarliestDate) $dtEarliestDate = $dtEventStart; } } return $dtEarliestDate ? date(DateUtils::PublicDateFormatter().' ' . DateUtils::PublicTimeFormatter(), $dtEarliestDate) : ''; } public function getEndDate() { $dtLatestDate = false; foreach($this->OrderItems() as $orderItem) { foreach($orderItem->Events() as $event) { $dtEventEnd = strtotime($event->EndDateTime); if ($dtLatestDate == false) $dtLatestDate = $dtEventEnd; if ($dtEventEnd > $dtLatestDate) $dtLatestDate = $dtEventEnd; } } return $dtLatestDate ? date(DateUtils::PublicDateFormatter().' ' . DateUtils::PublicTimeFormatter(), $dtLatestDate) : ''; } /** * Return Order as JSON * * Set additional relations you want to be returned. * Returned structure: * All info about the object (order) * _Relations - contains all the relations * every relation has 'data' key - relations data * every relation has 'type' key - List (if has many relation of this type) or Object (if has only one relation) * * example: Array ( * [ClassName] => Order * [Created] => 2015-09-01 11:33:00 * [LastEdited] => 2015-09-02 15:35:29 * [ID] => 8 * ........ * [RecordClassName] => Order * [_Relations] => Array ( * [OrderItems] => Array ( * [data] => Array ( * [0] => Array(...) * [1] => Array(...) * ) * [type] => List * ) * [Purchaser] => Array ( * [data] => Array(...) * [type] => Object * ))) * * @param array $arrRelations - list of all additional relation * @return string */ public function GetAsJSON($arrRelations = array('OrderItems','Purchaser','Transactions')) { $arrOrderData = $this->toMap(); $arrOrderData['_Relations'] = array(); foreach($arrRelations as $strRelation) { if (is_subclass_of($this->$strRelation(), 'DataList')) { $arrOrderData['_Relations'][$strRelation]['data'] = array(); $arrOrderData['_Relations'][$strRelation]['type'] = 'List'; foreach($this->$strRelation() as $relation) $arrOrderData['_Relations'][$strRelation]['data'][] = $relation->toMap(); } else { $arrOrderData['_Relations'][$strRelation]['data'] = $this->$strRelation()->toMap(); $arrOrderData['_Relations'][$strRelation]['type'] = 'Object'; } } return json_encode($arrOrderData); } public function AllowedStatuses() { $arrStatuses = $this->dbObject('MainStatus')->enumValues(); $this->extend('updateAllowedStatuses', $arrStatuses); return $arrStatuses; } //TODO can remove? public function IfStatusCompleteOrProvisional () { return in_array($this->MainStatus, array('Completed','Provisional')); } //TODO can remove? public function ActionCanAddPayment() { return $this->IfOutstandingPayment() && $this->IfStatusCompleteOrProvisional(); } //TODO can remove? public function ActionCanComplete() { return $this->MainStatus == 'Provisional'; } public function OrderItemsForSummary() { $al = new ArrayList(); foreach($this->OrderItems() as $item) { if ($item->Product()->ClassName == 'ProductEventSeries' && !$item->IsMainOrderItem()) continue; $al->push($item); } return $al; } public function GetUniqueParticipantsFromPendingMember() { $alPendingParticipants = ArrayList::create(); foreach($this->OrderItems() as $orderItem) { $alPendingParticipants->merge($orderItem->PendingParticipants()); } $alPendingParticipants->removeDuplicates(); $arrExclude = array(); foreach($alPendingParticipants as $pendingParticipant) { if($alPendingParticipants->find('ID', $pendingParticipant->OtherPendingMemberID)) $arrExclude[] = $pendingParticipant->ID; if($template = Template::get()->find('ID', $pendingParticipant->TemplateID)) { if(!$template->CollectsData()) { $arrExclude[] = $pendingParticipant->ID; } } } return $alPendingParticipants->exclude('ID', $arrExclude); } // Summary Merge functions public function Participants() { return $this->isInDB() ? ($this->MainStatus == 'Pending' ? $this->GetUniqueParticipantsFromPendingMember() : $this->GetUniqueParticipants()) : $this->GetParticipantsBasedOnOrderItems(); } protected function GetParticipantsBasedOnOrderItems() { $alParticipants = ArrayList::create(); $orderItems = $this->OrderItems(); foreach($orderItems as $orderItem) { if($orderItem->ChoosenDayParticipant) $alParticipants->add(ArrayData::create(array( 'ID' => $orderItem->ChoosenDayParticipant, 'DetailsSet' => false, 'ParticipantIndex' => $orderItem->ChoosenDayParticipant, 'ParticipantOrderItems' => ArrayList::create(), 'CurrencySymbol' => $this->CurrencySymbol(), 'OrderItemsTotalCost' => 0, ))); } $alParticipants->removeDuplicates('ID'); foreach($alParticipants as $participant) { foreach(self::ParticipantOrderItems($orderItems, $participant->ParticipantIndex) as $orderItem) { $orderItemData = self::OrderItemToDataList($orderItems, $orderItem); $orderItemData->EventsCount = 0; foreach(self::GetChildEventOrderItems($orderItems, $orderItem) as $item) { $orderItemData->ChildEventOrderItems->add(self::OrderItemToDataList($orderItems, $item)); $participant->OrderItemsTotalCost += $item->Cost; $orderItemData->EventsCount++; } if($orderItem->IfEventSeries()) $orderItemData->EventsCount = 1; $orderItemData->HasEventChildItems = $orderItemData->ChildEventOrderItems->count() > 0; foreach(self::GetChildOrderItems($orderItems, $orderItem) as $item) { $orderItemData->ChildOrderItems->add(self::OrderItemToDataList($orderItems, $item)); $participant->OrderItemsTotalCost += $item->Cost; } $participant->ParticipantOrderItems->add($orderItemData); } } return $alParticipants; } protected static function OrderItemToDataList($orderItems, $orderItem) { return ArrayData::create(array( 'ClassForSummaryRow' => 'PrimaryItem', 'ProductName' => $orderItem->ProductName(), 'FirstStartDate' => self::OrderItemFirstStartDate($orderItems, $orderItem), 'LastStartDate' => self::OrderItemLastStartDate($orderItems, $orderItem), 'HasEventChildItems' => false, 'ChildEventOrderItems' => ArrayList::create(), 'IsUpsellProduct' => $orderItem->IsUpsellProduct(), 'ChoosenDate' => $orderItem->ChoosenDate(), 'ChildOrderItems' => ArrayList::create(), 'EventStartDate' => $orderItem->EventStartDate(), 'EventEndDate' => $orderItem->EventEndDate(), 'IsMultidaysEvent' => $orderItem->IsMultidaysEvent(), 'EventCost' => $orderItem->EventCost(), 'hasLocation' => $orderItem->hasLocation(), 'LocationName' => $orderItem->LocationName(), 'IsWaitingListItem' => $orderItem->IsWaitingListItem(), 'IfEventSeries' => $orderItem->IfEventSeries(), 'EventsCount' => 0, )); } protected static function GetChildOrderItems($orderItems, $orderItem) { $alChildOrderItems = ArrayList::create(); foreach($orderItems as $item) if($item->Product()->ClassName == 'ProductEventUpsell' && $item->ChoosenDayParticipant == $orderItem->ChoosenDayParticipant) $alChildOrderItems->add($item); return $alChildOrderItems; } protected static function GetChildEventOrderItems($orderItems, $orderItem) { $arrChildOrderItems = array(); foreach($orderItems as $item) if($item->ProductID == $orderItem->ProductID && $item->ChoosenDayParticipant == $orderItem->ChoosenDayParticipant) { $arrChildOrderItems[] = $item; } usort($arrChildOrderItems, function($orderItem1, $orderItem2) { if(($event1 = $orderItem1->Events()->first()) && ($event2 = $orderItem2->Events()->first())) return strtotime($event1->StartDateTime) > strtotime($event2->StartDateTime) ? 1 : -1; return 0; }); return ArrayList::create($arrChildOrderItems); } protected static function IsOrderItemPrimary($orderItems, $orderItem) { if($primaryOrderItem = self::GetChildEventOrderItems($orderItems, $orderItem)->first()) return $primaryOrderItem === $orderItem; } protected static function ParticipantOrderItems($orderItems, $participantIndex) { $alOrderItems = ArrayList::create(); foreach($orderItems as $orderItem) if($orderItem->ChoosenDayParticipant == $participantIndex && $orderItem->Product()->ClassName != 'ProductEventUpsell' && self::IsOrderItemPrimary($orderItems, $orderItem)) { $alOrderItems->add($orderItem); } return $alOrderItems; } protected static function OrderItemFirstStartDate($orderItems, $orderItem) { if($firstOrderitem = self::GetChildEventOrderItems($orderItems, $orderItem)->first()) if($event = $firstOrderitem->Events()->first()) return is_object($event) ? $event->StartDateTime : Event::get()->byID($event)->StartDateTime; } protected static function OrderItemLastStartDate($orderItems, $orderItem) { if($lastOrderitem = self::GetChildEventOrderItems($orderItems, $orderItem)->last()) if($event = $lastOrderitem->Events()->last()) return is_object($event) ? $event->StartDateTime : Event::get()->byID($event)->StartDateTime; } public function PhysicalItems() { $alPhysicalOrderItems = ArrayList::create(); foreach($this->OrderItems() as $orderItem) if($orderItem->Product()->ClassName == 'ProductPhysicalItem') $alPhysicalOrderItems->add($orderItem); $newOrderItems = ArrayList::create(); foreach(self::GetOrderItemsGrouped($alPhysicalOrderItems) as $alOrderItems) { $newOrderItem = $alOrderItems->shift(); $newOrderItem->Quantity = 1; foreach($alOrderItems as $orderItem) { $newOrderItem->Cost += $orderItem->Cost; $newOrderItem->Quantity++; } $newOrderItems->add($newOrderItem); } return $newOrderItems; } public static function PurchaserOnlyProducts($product) { return !in_array($product->ClassName, array('ProductFixedEvent', 'ProductEventSeries', 'ProductEventUpsell', 'ProductAppointmentEvent','ProductTemplateEvents')); } public function PurchaserOrderItems() { $alPhysicalOrderItems = ArrayList::create(); foreach($this->OrderItems() as $orderItem) if(self::PurchaserOnlyProducts($orderItem->Product())) $alPhysicalOrderItems->add($orderItem); $newOrderItems = ArrayList::create(); foreach(self::GetOrderItemsGrouped($alPhysicalOrderItems) as $alOrderItems) { $newOrderItem = $alOrderItems->shift(); $newOrderItem->Quantity = 1; foreach($alOrderItems as $orderItem) { $newOrderItem->Cost += $orderItem->Cost; $newOrderItem->Quantity++; } $newOrderItems->add($newOrderItem); } return $newOrderItems; } public function SystemTakesPayments() { return SiteConfigOverride::CurrentSiteConfig()->SystemTakesPayments; } /** * Get all feature order items of a given order * * @param Order $order * @return DataList */ public static function getOrderItemsInTheFeature(Order $order) { return OrderItem::filterFeatureOrderItems($order->OrderItems()); } /** * Check if order has order items in the feature * * @return DataObject */ public function hasOrderItemsInTheFeature() { return self::getOrderItemsInTheFeature($this)->first(); } /** * Is simultaneous booking * * Booking is simultaneous if it's by the same user or admin is booking behalf of the same user * and IP or SessionID is different. * * When user login new record in MemberLoginLogs is created. If staff user starts a booking * then BookingOnBehalfOfMemberID is set to the real purchaser and new record in MemberLoginLogs is created. * * @return bool */ public static function IsSimultaneousBookings() { //If SiteConfig::OneSimultaneousBookingProcessPerUser == 0 it's safe to return false if (!SiteConfigOverride::CurrentSiteConfig()->OneSimultaneousBookingProcessPerUser) return false; if (!$member = MemberExtension::currentUser()) return false; $memberLoginLog = MemberLoginLog::get() ->filter('PurchaserID', MemberExtension::IsStaff($member) ? $member->BookingOnBehalfOfMemberID : $member->ID) ->sort('RegisteredDateTime', 'DESC') ->sort('ID', 'DESC') ->first(); if (!$memberLoginLog) $memberLoginLog = MemberLoginLog::RegisterMemberLogin(); // if ($memberLoginLog->IP != ServerUtils::IPAddress() || $memberLoginLog->SessionID != session_id()) { if ($memberLoginLog->UserAgent != ServerUtils::getUserBrowserInfo() || $memberLoginLog->SessionID != session_id()) { LogEntry::log("[Order::IsSimultaneousBookings({$member->ID})] SimultaneousBooking: expected ({$memberLoginLog->IP}, {$memberLoginLog->SessionID} \n {$memberLoginLog->UserAgent}) but get (" . ServerUtils::IPAddress() .", " . session_id() . "\n" . ServerUtils::getUserBrowserInfo() . ")" . "\n\n[Order::IsSimultaneousBookings({$member->ID})] Member: " . print_r($member->toMap(), true) . "\n\n[Order::IsSimultaneousBookings({$member->ID})] MemberLoginLog: " . print_r($memberLoginLog->toMap(), true) . "\n\n[Order::IsSimultaneousBookings({$member->ID})] Is Staff: " . intval(MemberExtension::IsStaff($member)) . "\n\n[Order::IsSimultaneousBookings({$member->ID})] BookingOnBehalfOfMemberID: {$member->BookingOnBehalfOfMemberID}", 999 ); return true; } return false; } /** * Get the purchaser ID * * If current user is Staff will get the purchaser ID * from the order parameters. * * If purchaser ID can not be found will throw error. * * @param Member $member * @param boolean $bThrowError - if true will throw user error otherwise will return null * @return int */ public static function getRealPurchaserID(Member $member = null, $bThrowError = true, Order $order = null) { if (!$member) $member = MemberExtension::currentUser(); if (!MemberExtension::IsStaff($member)) { if ($member) return $member->ID; else if ($bThrowError) user_error('BR-0003: Purchaser not found !', E_USER_ERROR); else return ''; } if ($order = $order ?? Order::GetCurrentOrder('', false)) { if ($order->IsInEdit()) { return $order->PurchaserID; } if ($iMemberID = $order->getOrderParam('RebookPurchaser')) { return $iMemberID; } } if ($bThrowError) user_error('BR-0001: Current member is staff but OrderParam RebookPurchaser not set !', E_USER_ERROR); return null; } /** * Get all orders of a given purchaser with events that are in the feature * * MainStatus is in array('Completed','Pending','Provisional'), * * @param int $iPurchaserID - if not passed will use Order::getRealPurchaserID() * @return DataList */ public static function GetOrdersWithFeatureEvents($iPurchaserID = null) { if (empty($iPurchaserID)) $iPurchaserID = Order::getRealPurchaserID(); return Order::get() ->filter(array( 'PurchaserID' => $iPurchaserID, 'MainStatus' => array('Completed','Pending','Provisional'), ))->innerJoin('OrderItem', 'oi.OrderID = "Order".ID', 'oi') ->innerJoin('OrderItem_Events', 'oie.OrderItemID = oi.ID', 'oie') ->innerJoin('Event', 'e.ID = oie.EventID', 'e') ->where("e.StartDateTime > '" . SS_Datetime::now()->Format(MYSQLDATETIME) . "'" ); } public function AddPromoCode($strCode): ArrayList { $packageFilters = PackageFilter_PromoCodeTriggered::get()->filter('PromoCodeTrigger',$strCode); if(!$packageFilters->exists()) { throw new PackagePromoCodeNotFoundException; } $arrInitialParams = $this->GetPromoCodes(); $this->SetPromoCodes(array_merge($arrInitialParams, [$strCode])); $matchedFilters = new ArrayList; foreach(NewPackage::PackageMatching($this) as $package) { foreach($packageFilters as $packageFilter) { if($package->ID == $packageFilter->NewPackageID) { $matchedFilters->add($packageFilter); } } } if($matchedFilters->exists()) { $matchedFilters->removeDuplicates('ID'); return $matchedFilters; } // At this point promo code wasnt applied so remove it from the Order $this->SetPromoCodes($arrInitialParams); return $matchedFilters; } public function RemovePromoCode($strCode) { if(!PackageFilter_PromoCodeTriggered::get()->find('PromoCodeTrigger',$strCode)) { throw new PackagePromoCodeNotFoundException; } $arrParams = $this->GetPromoCodes(); $index = array_search($strCode, $arrParams); if($index !== false) { unset($arrParams[$index]); } $this->SetPromoCodes($arrParams); NewPackage::PackageMatching($this); } public function GetPromoCodes() { $arrParams = []; if($strParams = $this->getOrderParam('TriggeredPromoCode')) { $arrParams = unserialize($strParams); } return $arrParams; } protected function SetPromoCodes($arrCodes) { $this->setOrderParam('TriggeredPromoCode',serialize(array_unique($arrCodes))); return $this; } public function HasResourceManagementProduct() { return SiteConfigOverride::CurrentSiteConfig()->EnableResourceManagement && $this->GetResourceManagementOrderItems()->first(); } public function GetResourceManagementOrderItems() { $alResourceManagementOrderItems = ArrayList::create(); foreach($this->OrderItems() as $orderItem) if($orderItem->Product()->ClassName == 'ProductResource') $alResourceManagementOrderItems->add($orderItem); return $alResourceManagementOrderItems; } public function CanCheckIn() { if($this->HasResourceManagementProduct() && $this->IsCheckedOut()) foreach($this->UniqueProductResourceOrderItems() as $orderItem) { $resource = $orderItem->Events()->first()->Resource(); if($orderItem->GetRealQuantity() - ($resource->CheckedInQuantity($this) + $resource->WrittenOffQuantity($this)) > 0 ) return true; } } public function CanCheckOut() { return $this->HasResourceManagementProduct() && !$this->IsCheckedOut(); } public function IsCheckedOut() { foreach($this->GetResourceManagementOrderItems() as $orderItem) if($orderItem->Events()->first()->CheckedOut) return true; } public function IsCheckedIn() { foreach($this->GetResourceManagementOrderItems() as $orderItem) { $resource = $orderItem->Events()->first()->Resource(); $iProcessed = $resource->CheckedInQuantity($this) + $resource->WrittenOffQuantity($this); if($orderItem->GetRealQuantity() - $iProcessed > 0) return false; } return true; } public function IsPartiallyCheckedIn() { foreach($this->GetResourceManagementOrderItems() as $orderItem) { $resource = $orderItem->Events()->first()->Resource(); $iProcessed = $resource->CheckedInQuantity($this) + $resource->WrittenOffQuantity($this); if($iProcessed > 0 && $iProcessed < $orderItem->GetRealQuantity()) return true; } return false; } public function MainStatusLabel() { return $this->Status ? $this->Status : $this->MainStatus; } /** * Get the custom status based on Order MainStatus * * First it will check the custom lables created by user * if it can not be found it will try to find it based * on the system status. If for some reason this also fail * it will return the passed MainStatus * * @return string */ public function getRealStatus($strLabel) { $strLabel = Convert::raw2sql($strLabel); if ($customStatus = StatusCustomLabel::get()->filter(array( 'StatusSystemLabel.StatusName' => $strLabel, 'Status' => 1, ))->where("StatusCustomLabel.Label = '$strLabel' OR REPLACE(StatusCustomLabel.Label, ' ', '') = '$strLabel'") ->sort('Label', 'ASC')->first()) { return $customStatus->Label; } else if ($customStatus = StatusCustomLabel::get()->filter(array( 'StatusSystemLabel.StatusName' => $strLabel, 'Status' => 1, ))->where("StatusSystemLabel.StatusName = '$strLabel' OR REPLACE(StatusSystemLabel.StatusName, ' ', '') = '$strLabel'") ->sort('Label', 'ASC')->first()) { return $customStatus->Label; } else { return $strLabel; } } public function UpdateStatus() { $this->Status = ''; if($this->MainStatus == 'Aborted') { if($this->Transactions()->filter(array('Type' => 'Card', 'Status' => 'Aborted'))->first()) $this->Status = $this->getRealStatus('Payment Failure'); } else if($this->MainStatus == 'Completed'){ if($this->HasResourceManagementProduct()) { if($this->IsPartiallyCheckedIn()) $this->Status = $this->getRealStatus('Partially Checked In'); else if($this->CanCheckIn() || $this->CanCheckOut()) $this->Status = $this->IsCheckedOut() ? $this->getRealStatus('Checked Out') : $this->getRealStatus('Awaiting Checkout'); } if($this->AmountDueToPay() != 0) $this->Status = $this->TotalAmountOverdue == 0 ? $this->getRealStatus('Balance Outstanding') : $this->getRealStatus('Payment Overdue'); if($this->isInDB() && $this->ChildCareVouchers()->first()) $this->Status = $this->getRealStatus('Childcare Pending'); if($this->Invoiced) $this->Status = $this->getRealStatus('Invoiced'); } if(empty($this->Status)) $this->Status = $this->getRealStatus($this->MainStatus); } public function UniqueProductResourceOrderItems() { $arrOrderItems = array(); foreach($this->GetResourceManagementOrderItems() as $orderItem) { if($event = $orderItem->Events()->first()) if($resource = $event->Resource()) if(!isset($arrOrderItems[$resource->ID])) $arrOrderItems[$resource->ID] = $orderItem; } return ArrayList::create(array_values($arrOrderItems)); } public function CheckoutResource($resource) { foreach($this->OrderItems()->filter('Events.ResourceID', $resource->ID) as $orderItem) { $event = $orderItem->Events()->first(); $orderItem->Events()->add($event, array('CheckedOut' => SS_Datetime::now()->getValue())); } } public function WriteoffResource($resource, $iCount) { if($resource && $iCount > 0) $resource->AddStockChange(-$iCount, $this); } public function CheckinResource($resource, $iCount) { if($resource && $iCount > 0) $resource->Type == 'Lending' ? $this->CheckinLendingResource($resource, $iCount) : $this->CheckinDistributableResource($resource, $iCount); } protected function GetResourceOrderItemsAwaitingForCheckin($resource) { return $this->OrderItems() ->filter('Events.ResourceID', $resource->ID) ->where('OrderItem_Events.CheckedIn IS NULL'); } protected function CheckinDistributableResource($resource, $iCount) { $orderItems = $this->GetResourceOrderItemsAwaitingForCheckin($resource); if($orderItem = $orderItems->first()) { $arrExtraFields = array(); $event = $orderItem->Events()->first(); if($iDifference = ($event->DistributableQuantity - $iCount)) { // Partial check in $newEvent = $event->duplicate(false)->write(); $orderItem->Events()->add($newEvent, array( 'CheckedIn' => SS_Datetime::now()->getValue(), 'CheckedOut' => $event->CheckedOut, 'DistributableQuantity' => $iCount, )); $arrExtraFields = array('DistributableQuantity' => $iDifference); } else { // Full check in $arrExtraFields = array('CheckedIn' => SS_Datetime::now()->getValue()); } $orderItem->Events()->add($event, $arrExtraFields); } } protected function CheckinLendingResource($resource, $iCount) { foreach($this->GetResourceOrderItemsAwaitingForCheckin($resource) ->limit($iCount) as $orderItem) $orderItem->Events()->add($orderItem->Events()->first(), array( 'CheckedIn' => SS_Datetime::now()->getValue(), )); } public function ResourceReturnDateTime() { if($orderItem = $this->GetResourceManagementOrderItems()->first()) if($event = $orderItem->Events()->first()) return $event->EndDateTime; } public function ResourceCollectionDateTime() { if($orderItem = $this->GetResourceManagementOrderItems()->first()) if($event = $orderItem->Events()->first()) return $event->StartDateTime; } public function AddChildCareVoucher($strAgent, $fAmount) { if(MathUtils::CurrencyLessOrEqual($fAmount, $this->GetMaximumVouchersAmount())) { if(!$voucherProvider = VoucherProvider::get()->filter(array( 'Type' => 'ChildCare', 'OrganisationName:nocase' => $strAgent, ))->first()) { $voucherProvider = VoucherProvider::create(array( 'Type' => 'ChildCare', 'OrganisationName' => $strAgent, )); $voucherProvider->write(); } $iVoucherID = Voucher::create(array( 'Type' => 'ChildCare', 'Status' => 'Verification', 'VoucherProviderID' => $voucherProvider->ID, ))->write(); $transaction = Transaction::CreateNew( 'Voucher', $this->ID, $fAmount, $voucherProvider->Name ); $transaction->VoucherID = $iVoucherID; $transaction->write(); $this->ReCalculate(true); } } public function GetUnpaidCreditCardChargeItems() { return $this->Charges("CreditCardChargePointID > 0 AND Status='Unpaid'"); } /** * Get Order and Purchaser details as array for template processing * * @return array */ public function GetOrderAndPurchaserDetailsAsArray() { return array_merge($this->GetOrderDetailsAsArray(), $this->GetPurchaserDetailsAsArray()); } /** * Get order details as array for template processing * * @return array */ public function GetOrderDetailsAsArray() { return array( 'Reference' => $this->Reference, 'PurchaseDate' => DateUtils::ConvertDateToPublicFormat($this->DateRegistered), 'Status' => _t('BookingLive.OrderStatus'.$this->Status,$this->Status), 'CustomerNotes' => $this->CustomerNotes(), 'Staff' => $this->Staff(), 'NotificationByEmail' => $this->getOrderParam('NotificationByEmail') == '1', 'HasPendingTransactions' => $this->HasPendingTransactions(), ); } /** * Get purchaser details as array for template processing * @return array */ public function GetPurchaserDetailsAsArray() { if ($this->MainStatus == 'Pending' && $this->PendingPurchaserID) { if ($this->Source == 'Staff' && $this->PurchaserID) { return $this->Purchaser()->GetPurchaserDetailsAsArray(); } $Purchsaser = $this->PendingPurchaser(); return [ 'PurchaserEmail' => $Purchsaser->Email, 'PurchaserName' => $Purchsaser->FullName(), ]; } elseif ($this->PurchaserID) { return $this->Purchaser()->GetPurchaserDetailsAsArray(); } return []; } /** * Return order total as array for template processing * * @param boolean $bIsHTML * @param boolean $IncludeDeleteButtons * @return array */ public function GetOrderTotalAsArray($bIsHTML = true, $IncludeDeleteButtons = false) { return array( 'SubTotal' => OrderSummary::SubTotalContent($this, $bIsHTML, $IncludeDeleteButtons), 'ProductCharges'=> OrderSummary::ProductChargesContent($this, $bIsHTML, $IncludeDeleteButtons), 'Discounts' => OrderSummary::DiscountContent($this, $bIsHTML, $IncludeDeleteButtons), 'Transactions' => OrderSummary::TransactionContent($this, $bIsHTML, $IncludeDeleteButtons), 'Tax' => SiteConfig::current_site_config()->DisplayTax ? OrderSummary::TaxContent($this, $bIsHTML, $IncludeDeleteButtons) : '', 'OrderTotal' => OrderSummary::TotalContent($this, $bIsHTML, $IncludeDeleteButtons), 'FinalTotalCost' => number_format($this->FinalTotalCost,2,'.',''), 'Deposit' => OrderSummary::TotalDepositContent($this, $bIsHTML, $IncludeDeleteButtons), 'CurrencySymbol' => $this->CurrencySymbol(), ); } public function OrderEmailIcon() { if (empty($_POST['IsInXLSExport2016'])) { $strNotificationStatus = $this->getOrderParam('NotificationByEmail'); if ($strNotificationStatus !== '0' && $strNotificationStatus !== '1') { $strNotificationStatus = $this->EmailNotification; } $color = 'rgba(2, 47, 71, 0.3)'; if ($strNotificationStatus == '1') { $color = 'rgba(2, 47, 71, 1)'; } return "
"; } } public function CustomStatusesActionDropDown() { $customStatuses = CustomStatusesField::CustomLabels($this->MainStatus); $fActions = ActionsDropdownField::create('CustomStatuses'); $notFound = true; foreach ($customStatuses->filter('Status',1) as $customStatus) { if ($customStatus->Label == $this->Status) { $fActions->addMainAttributes(array( 'label' => $customStatus->Label, 'style' => 'background-color: '.$customStatus->Color.' !important', 'class' => 'WhiteTextColor' )); $notFound = false; } $fActions->addAction($customStatus->Label, array( 'class' => 'WhiteTextColor', 'data-link' => '#', 'style' => 'background-color: '.$customStatus->Color.' !important', 'data-id' => $this->ID )); } if ($notFound) { $notFound = true; foreach ($customStatuses->filter(array( 'Status' => 1, 'StatusSystemLabel.StatusName' => $this->Status )) as $customStatus) { $notFound = false; $fActions->addMainAttributes(array( 'label' => $customStatus->Label, 'style' => 'background-color: '.$customStatus->Color, 'class' => 'WhiteTextColor' )); } if ($notFound) { if ($customStatus = StatusCustomLabel::get()->filter(['StatusSystemLabel.StatusName' => $this->Status])->first()) { $fActions->addMainAttributes(array( 'label' => $customStatus->Label, 'style' => 'background-color: '.$customStatus->Color, 'class' => 'WhiteTextColor' )); } } } return $fActions->forTemplate(); } public function OrderSMSIcon() { if (empty($_POST['IsInXLSExport2016'])) { $iNotificationStatus = $this->SMSNotification; $color = ' rgba(2, 47, 71, 0.3)'; if ($iNotificationStatus) $color = 'rgba(2, 47, 71, 1)'; return "
"; } } public function SetSimulatedDate($strDateTime, $bApply = false) { if($strDateTime) { $this->setOrderParam('SimulatedDate', date(MYSQLDATETIME, strtotime($strDateTime))); if($bApply) $this->ApplySimulatedDate(); } return $this; } public function GetSimulatedDate() { if($simulatedDate = $this->getOrderParam('SimulatedDate')) return $simulatedDate; } public function OverridePricingDate($strPricingDate) { if($strPricingDate) $this->setOrderParam('OverridedPricingDate', date(MYSQLDATETIME, strtotime($strPricingDate))); return $this; } public function GetOverridedPricingDate() { if($overridePricingDate = $this->getOrderParam('OverridedPricingDate')) return $overridePricingDate; } public function UnsetSimulatedDate() { $this->unsetOrderParam('SimulatedDate'); $this->ClearSimulatedDate(); return $this; } public function UnsetOverridedPricingDate() { $this->unsetOrderParam('OverridedPricingDate'); return $this; } public static function ApplySimulatedDate($order = null) { if(!$order) $order = Order::GetCurrentOrder('', false); if($order && $strVirtualDate = $order->getOrderParam('SimulatedDate')) SS_Datetime::set_mock_now($strVirtualDate); return $order; } public static function ClearSimulatedDate() { SS_Datetime::clear_mock_now(); } public function IsSuperAdd() { return $this->getOrderParam('SuperAddOrderID'); } public function SuperAdd($iSuperAddOrderID) { $orderCombineTo = Order::get()->byID($iSuperAddOrderID); $iSeq = $orderCombineTo->GetNextSequence(); foreach ($this->OrderItems() as $item) { $item->PendingParticipantsToParticipants(SiteConfigOverride::CurrentSiteConfig()->AutoCheckin); $item->Sequence += $iSeq; $item->OrderID = $orderCombineTo->ID; $item->write(); } Voucher::MakeEVouchers($orderCombineTo); foreach ($this->Transactions() as $transaction) { $transaction->OrderID = $orderCombineTo->ID; $transaction->write(); } $orderCombineTo->setOrderParam('SuperAddedOrderDetails', 'This order is super added from : '. $this->getTitle(). ' at '. SS_Datetime::now()->getValue()); $orderCombineTo->ReCalculate(true); OrderNote::create(array( 'Type' => 'AdminNote', 'Note' => 'A temporary order was created ('. $this->getTitle().') to recalculate the basket after items where added', 'OrderID' => $orderCombineTo->ID ))->write(); $this->MainStatus = 'Cancelled'; $this->write(); OrderNote::create(array( 'Type' => 'AdminNote', 'Note' => 'The purpose of cancellation of this order is that, this has been combined to order : '. $orderCombineTo->getTitle(), 'OrderID' => $this->ID ))->write(); WebHook::process($orderCombineTo,'OrderUpdate'); } public function iCalFeedLink() { return Director::absoluteURL('icalfeed?r='.$this->Reference, true); } public function AddOrderNote($strNote, $strType = 'AdminNote') { $this->OrderNotes()->add(OrderNote::create(array( 'Type' => $strType, 'Note' => $strNote, ))); } public function CanSendEmail() { $nbep = $this->getOrderParam('NotificationByEmail'); if ($nbep === '0' || $nbep === '1') { return $nbep == '1'; } return $this->EmailNotification; } public function CanSendSMS() { return $this->SMSNotification; } public static function GetCurrentTaskFlowOrder($taskFlow,$strSessionID='',$bCreateOrder=true, $bRecoverAbortedIfNoItems=true) { $order = Order::GetCurrentOrder($strSessionID,$bCreateOrder,$bRecoverAbortedIfNoItems); $currentTaskFlow = $order->CurrentTaskFlow(); if ($currentTaskFlow && $currentTaskFlow->ID != $taskFlow->ID) { $order->Cancel = true; $order->write(); $order = Order::GetCurrentOrder('',true,false); } $order->SetTaskFlowOrder($taskFlow); return $order; } public function GetPrerequisites() { $al = new ArrayList(); foreach($this->OrderItems() as $orderItem){ $product = $orderItem->Product(); if($product && $product->exists()) { $al->merge($product->Prerequisites()); foreach ($product->ProductGroups() as $productGroup) { $al->merge($productGroup->Prerequisites()); } } } $al->merge(Prerequisite::get()->filter('Default',1)); return $al; } public function SwitchPurchaser(Member $member){ $this->update(array( 'SessionID' => session_id(), 'PendingPurchaserID' => 0, 'PurchaserID' => $member->ID, ))->write(); $this->ReCalculate(true); //Maybe there is a package that will apply now NewPackage::PackageMatching($this); } public function HasPendingTransactions() : bool { return $this->Transactions()->filter('Status', 'Pending')->exists(); } }